コード分割はプログラムを簡潔にわかりやすく保つためにもとても重要なものです。
どのプログラミング言語を使っていてもコード分割は常に心がけたいことですが、Rustではどのようにコード分割すれば良いのでしょうか?
今回はRustでコード分割するうえで必要な知識と実際にコード分割する方法を紹介します。
- Rustでコード分割するうえで必要な知識
- コード分割手法
- 分割したコードの使用方法
基本構造
まず初めにコード分割するうえで必要となるRustの基本構造について見ていきましょう。
小さなものから順にモジュール、クレート、パッケージ、ワークスペースというものがあります。
モジュール
モジュールはコードのまとまりです。
言語にもよりますが、例えば名前空間(namespace
)が似ています。
実装としては以下のようにmod
キーワードを使用します。
mod foo {
mod bar {
fn test() {}
}
}
クレート
クレートについて公式では以下のように記載されています。
クレートはバイナリかライブラリのどちらかです。
パッケージとクレート – The Rust Programming Language
要するに他言語におけるパッケージやライブラリと同じ認識で良いかと思います。(厳密には違う部分もあるかもしれませんが。。)
クレートには2種類ありそれぞれ以下のようなものとなります。
- バイナリクレート
-
単体で実行可能なもの(
.exe
など) - ライブラリクレート
-
単体では実行できないもの(
.lib
,.dll
,.a
など)
構造的にはsrc/main.rs
,src/lib.rs
,src/bin/*.rs
を頂点(クレートルート)としたモジュールツリー(一連のコードのまとまり)となっています。
構造のサンプル
「構造」や「モジュールツリー」と字面だけ見てもわかりにくいのでサンプルを作ってみます。
ディレクトリ/ファイル構造としては以下のようなものとします。
sample/
|-- Cargo.lock
|-- Cargo.toml
`-- src/
|-- lib.rs
|-- module1.rs
|-- module2.rs
|-- module1/
| |-- module11.rs
| `-- module12.rs
`-- module2/
|-- module21.rs
`-- module22.rs
コードは以下のようにします。
ここでは実装の詳細は気にせずにモジュールのつながりのみ意識します。
mod module1;
mod module2;
...
pub mod module11;
pub mod module12;
pub mod foo {}
pub mod bar {}
pub mod module21;
pub mod module22;
pub mod hoge {}
pub mod fuga {}
すると今回作成したものは以下のようなクレートとなります。
- ライブラリクレート
lib.rs
をクレートルートとする- モジュールツリーは以下に示す構造となる
crate root
├── module1
│ ├── module11
│ │ └── foo
│ └── module12
│ └── bar
└── module2
├── module21
│ └── hoge
└── module22
└── fuga
パッケージ
パッケージとは以下のような特徴を持つものです。
- 提供する機能のまとまり
- 1つ以上のクレートを持つ
cargo new
を実行してできるものCargo.toml
が存在するディレクトリをルートとしたまとまり
試しにcargo new
してパッケージを作ってみましょう。
% cargo new my-package
Created binary (application) `my-package` package
ディレクトリ構造をみると以下のようになっています。
my-package/
├── Cargo.toml
└── src/
└── main.rs
これがパッケージとなります。
上記は単純な例ですが、パッケージであるためには以下のルールに従う必要があります。
- 少なくとも1つ以上のクレート(バイナリクレートもしくはライブラリクレート)を持つ
- バイナリクレートは複数持つことができる
- ライブラリクレートは最大で1つまで持つことができる
この説明だけだとパッと理解しにくいかと思います。
そのため実際に作成できるパッケージのパターンを下記の記事にて紹介していますので合わせてご覧ください。
ワークスペース
ワークスペースは複数のパッケージをまとめて管理するためのものです。
Cargo.lock
や出力ディレクトリ(target
)がひとつになるなどの利点がありますが、詳細は下記をご覧ください。
本記事の内容においてはパッケージを分割するために利用するものという理解でOKです。
実際の使用方法は後述のパッケージ分割にて紹介します。
コード分割の方法
ここまでのことを踏まえて以下の2点をやってみましょう。
- コード分割/ファイル分割する
- 分割したコードを別のコードから利用する
モジュール分割
分割
モジュール分割の方法はRust 2018エディションより前か後かで若干変わっています。
本記事ではRust2018以降の方式でやります。
下記コマンドでライブラリクレートを含むパッケージを作成します。
cargo new sample --lib
- 既に
lib.rs
が存在する場合は新たに作成する必要はありません。 - バイナリクレート(
main.rs
)でもOKです。
src
ディレクトリ配下にmy_module
ディレクトリを作成します。
sample/
|-- Cargo.toml
`-- src/
|-- lib.rs
`-- my_module/
my_module
ディレクトリ配下にfoo.rs
bar.rs
を追加します。
sample/
|-- Cargo.toml
`-- src/
|-- lib.rs
`-- my_module/
|-- bar.rs
`-- foo.rs
pub fn show() {
println!("foo!");
}
pub fn show() {
println!("bar!");
}
src
ディレクトリ配下にmy_module.rs
を作成します。
sample/
|-- Cargo.toml
`-- src/
|-- lib.rs
|-- my_module.rs
`-- my_module/
|-- bar.rs
`-- foo.rs
pub mod foo;
pub mod bar;
呼び出し
分割したモジュールを使うには以下のようにmod
キーワードでモジュールを参照します。
mod my_module;
pub fn show() {
my_module::foo::show();
my_module::bar::show();
}
use
を使用することでパスを短縮して使用することもできます。
mod my_module;
use my_module::{foo, bar};
pub fn show() {
foo::show();
bar::show();
}
クレート分割
分割
クレートを分割する場合は上記で説明したパッケージの構成ルールに従う必要があります。
ここでは例としてバイナリクレート+ライブラリクレートにコードを分割してみます。
下記コマンドでバイナリクレートを保持するパッケージを作成します。
cargo new sample
src
ディレクトリにlib.rs
を追加します。
pub fn foo() {
println!("foo!");
}
以下のファイル構造となっていることを確認します。
sample/
|-- Cargo.toml
`-- src/
|-- lib.rs
`-- main.rs
呼び出し
バイナリクレート(main.rs
)からライブラリクレート(lib.rs
)を使用してみます。
この場合はmain.rs
から以下のようにuse
を使用するだけでOKです。
use sample::foo;
fn main() {
foo();
println!("Hello, world!");
}
上記のuse sample::foo
のsample
部分はパッケージ名に依存します。
正確にはCargo.toml
の以下となります。
[package]
name = "sample"
version = "0.1.0"
edition = "2021"
パッケージ分割
分割
パッケージを分割するためにはワークスペースを利用します。
空のディレクトリを作成します。
ここではmy-workspace
という名前で作成します。
この段階でのファイル構造は以下のようになります。
my-workspace/
サンプルとして以下のようなパッケージを作成します。
- package1
-
バイナリクレートを持つパッケージ
- package2
-
ライブラリクレートを持つパッケージ
下記コマンドでサクッと作成しましょう。
cargo new package1
cargo new package2 --lib
この段階でのファイル構造は以下のようになります。
my-workspace/
|-- package1/
| |-- Cargo.toml
| `-- src/
| `-- main.rs
`-- package2/
|-- Cargo.toml
`-- src/
`-- lib.rs
my-workspace/Cargo.toml
を以下の内容で新規作成します。
[workspace]
members = [
"package1",
"package2",
]
呼び出し
ではpackage1
からpackage2
を実際に使用してみましょう。
package1
からpackage2
を使用するためにpackage1/Cargo.toml
に依存関係を追加します。
[package]
name = "package1"
version = "0.1.0"
edition = "2021"
[dependencies]
package2 = { path = "../package2" }
後はuse
を用いて今いるスコープに使用したい関数などを取り込むだけです。
use package2::add;
fn main() {
let result = add(2, 3);
println!("result={}", result);
}
別パッケージの呼び出しの際にextern crate
という構文を使用しているものが見られますが、Rust2018以降は基本的に使用しなくてOKなものです。
詳細は下記をご参照ください。
最後に
Rust特有の言葉に戸惑うこともありますが基本的な考えは他言語と一緒です。
構造も少し複雑で手数が多いなという印象ですが、堅牢なコードを書くためにはこれくらいしっかいしているほうが安心感があります。