Rustプログラムの基本構造を完全解説!ファイル構成とエントリーポイントを初心者向けに徹底ガイド
生徒
「Rustのプログラムってどういう構造になっているんですか?他の言語と違うところはありますか?」
先生
「Rustプログラムはmain関数をエントリーポイントとした明確な構造を持っています。ファイル構成もCargoというツールで管理されるので、初心者でも整理されたコードを書きやすいですよ。」
生徒
「Cargoって何ですか?プログラムの実行に必要なんですか?」
先生
「Cargoは、Rustのビルドツール兼パッケージマネージャです。依存関係の管理からコンパイル、テストまで一括で処理できます。それでは基本構造から詳しく見ていきましょう!」
1. Rustプログラムの最小構成とmain関数の役割
Rustプログラムの最小構成は、main関数を持つ単一のソースファイルです。Rustコンパイラは、プログラムの実行開始点としてmain関数を探し、ここから処理が始まります。これをエントリーポイントと呼びます。
以下は、最もシンプルなRustプログラムの例です。
fn main() {
println!("Hello, Rust!");
}
このコードでは、fn main()がプログラムのエントリーポイントです。println!マクロで文字列を出力しています。Rustでは、マクロは名前の最後に!が付くのが特徴です。
main関数は必ずfn main()という形式で記述し、戻り値を指定しない場合は()(ユニット型)が暗黙的に返されます。この関数がプログラム全体の実行フローを制御する中心的な存在となります。
2. Cargoプロジェクトのファイル構成と基本構造
実際の開発では、Cargoを使ってプロジェクトを作成するのが一般的です。Cargoは、Rustのビルドツールおよびパッケージマネージャで、プロジェクトの構造を標準化し、依存ライブラリの管理を簡単にします。
Cargoで新しいプロジェクトを作成するには、ターミナルで次のコマンドを実行します。
cargo new my_project
このコマンドを実行すると、次のようなディレクトリ構造が自動生成されます。
my_project/
├── Cargo.toml
└── src/
└── main.rs
Cargo.tomlは、プロジェクトの設定ファイルで、パッケージ名やバージョン、依存関係などのメタ情報を記述します。src/main.rsが実際のソースコードファイルで、ここにmain関数を記述します。
Cargo.tomlファイルの基本的な内容は次のようになります。
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
[dependencies]
editionはRustのエディションを指定するもので、言語仕様のバージョンを示します。現在は2021が主流です。[dependencies]セクションには、外部ライブラリ(クレート)の依存関係を追加します。
3. main.rsファイルの役割とソースコードの配置
src/main.rsは、実行可能バイナリプロジェクトのメインソースファイルです。Cargoは、このファイルに記述されたmain関数をエントリーポイントとしてコンパイルします。
複数のソースファイルを使う場合でも、main.rsが起点となり、他のモジュールを読み込んで使用します。以下は、変数とループを使ったシンプルなプログラム例です。
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
for num in numbers {
println!("数値: {}", num);
}
}
このコードでは、vec!マクロでベクタ(動的配列)を作成し、forループで各要素を出力しています。Rustのprintln!マクロでは、{}を使って変数を埋め込むことができます。
main.rs以外にも、src/ディレクトリ内に複数の.rsファイルを配置してモジュール化することが可能です。これにより、コードを機能ごとに分割して管理しやすくなります。
4. プログラムのビルドと実行の流れ
Cargoプロジェクトをビルドして実行するには、プロジェクトのルートディレクトリで次のコマンドを使用します。
cargo build
cargo run
cargo buildは、プロジェクトをコンパイルしてバイナリファイルを生成します。生成されたバイナリはtarget/debug/ディレクトリに配置されます。開発中はデバッグビルドが使用され、最適化は最小限に抑えられます。
cargo runは、ビルドと実行を一度に行う便利なコマンドです。コードに変更があれば自動的に再ビルドされてから実行されます。
リリース用に最適化されたビルドを作成する場合は、次のコマンドを使用します。
cargo build --release
リリースビルドでは、コンパイル時間は長くなりますが、実行速度が大幅に向上します。生成されたバイナリはtarget/release/ディレクトリに配置されます。
5. モジュールシステムとファイル分割の基本
Rustでは、モジュールシステムを使ってコードを複数のファイルに分割できます。プロジェクトが大きくなっても、機能ごとにファイルを分けることで保守性が向上します。
例えば、src/utils.rsという新しいファイルを作成し、そこに関数を定義します。
pub fn greet(name: &str) {
println!("こんにちは、{}さん!", name);
}
pubキーワードを付けることで、この関数を他のモジュールから使用できるようにします。次に、main.rsからこのモジュールを読み込みます。
mod utils;
fn main() {
utils::greet("太郎");
}
mod utils;という宣言で、utils.rsファイルをモジュールとして読み込みます。::はパス区切り文字で、モジュール内の関数にアクセスするために使用します。
さらに複雑な構造では、src/utils/というディレクトリを作り、その中にmod.rsまたは複数の.rsファイルを配置することで、階層的なモジュール構成を実現できます。
6. ライブラリプロジェクトとバイナリプロジェクトの違い
Rustプロジェクトには、バイナリプロジェクトとライブラリプロジェクトの2種類があります。これまで説明してきたのはバイナリプロジェクトで、実行可能なプログラムを作成します。
ライブラリプロジェクトは、他のプロジェクトから利用されるコードを提供するためのものです。ライブラリプロジェクトを作成するには、次のコマンドを使用します。
cargo new my_library --lib
このコマンドで生成されるプロジェクト構造は次のようになります。
my_library/
├── Cargo.toml
└── src/
└── lib.rs
バイナリプロジェクトのmain.rsに対して、ライブラリプロジェクトではlib.rsがエントリーポイントになります。lib.rsにはmain関数は不要で、代わりに公開する関数やモジュールを定義します。
同じプロジェクト内でバイナリとライブラリを両方持つことも可能です。その場合、src/main.rsとsrc/lib.rsの両方を配置し、バイナリ側からライブラリの機能を利用する構成にします。
7. 関数の定義と呼び出しの基本構造
Rustでは、fnキーワードを使って関数を定義します。関数はプログラムの処理を機能ごとにまとめるための基本単位です。関数には引数と戻り値を指定できます。
以下は、引数を受け取って計算結果を返す関数の例です。
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let result = add(10, 20);
println!("結果: {}", result);
}
fn add(a: i32, b: i32) -> i32という定義で、2つのi32型の引数を受け取り、i32型の値を返す関数を作成しています。戻り値は->の後に型を指定します。
関数の最後の式がセミコロンなしで書かれている場合、その値が戻り値となります。この例ではa + bがそのまま返されます。明示的にreturnキーワードを使うこともできますが、Rustでは最後の式を戻り値とするスタイルが一般的です。
関数を適切に分割することで、コードの再利用性とテストのしやすさが向上します。各関数は単一の責任を持つように設計することが推奨されます。
8. Cargo.tomlでの依存関係管理とクレートの追加
実用的なプログラムを作成する際には、外部ライブラリ(クレート)を使用することがよくあります。Rustのクレートは、crates.ioというレジストリで公開されており、誰でも利用できます。
例えば、乱数を生成するrandクレートを使用したい場合、Cargo.tomlの[dependencies]セクションに次のように追加します。
[dependencies]
rand = "0.8"
この設定を追加したら、cargo buildを実行すると自動的にrandクレートがダウンロードされてコンパイルに含まれます。ソースコードでは次のように使用できます。
use rand::Rng;
fn main() {
let mut rng = rand::thread_rng();
let random_number: u32 = rng.gen_range(1..=100);
println!("ランダムな数値: {}", random_number);
}
use文で外部クレートの機能をインポートします。この例では、rand::Rngトレイトをインポートし、gen_rangeメソッドで1から100までのランダムな整数を生成しています。
Cargoは依存関係のバージョン管理も自動的に行い、Cargo.lockファイルに正確なバージョン情報を記録します。これにより、チーム開発でも全員が同じバージョンのライブラリを使用できます。
9. テストコードの配置とテスト実行の基本
Rustには、テスト機能がビルトインで用意されており、Cargoを使って簡単にテストを実行できます。テストコードは、通常は同じファイル内に#[cfg(test)]モジュールとして配置します。
以下は、簡単なテストの例です。
fn multiply(a: i32, b: i32) -> i32 {
a * b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multiply() {
assert_eq!(multiply(3, 4), 12);
assert_eq!(multiply(-2, 5), -10);
}
}
#[test]属性を付けた関数がテスト関数として認識されます。assert_eq!マクロで期待値と実際の値が等しいかを検証します。テストを実行するには、次のコマンドを使用します。
cargo test
このコマンドを実行すると、プロジェクト内のすべてのテストが実行され、結果が表示されます。テストが失敗した場合は、どのテストが失敗したかが明確に示されます。
統合テストはtests/ディレクトリに配置することもできます。これにより、ライブラリの公開APIを外部から使用する形でテストできます。テスト駆動開発を実践する際にも、Rustのテスト機能は非常に便利です。
10. ドキュメンテーションコメントとコードの可読性向上
Rustでは、ドキュメンテーションコメントを使ってコードの説明を記述できます。これらのコメントは、cargo docコマンドで自動的にHTMLドキュメントとして生成されます。
ドキュメンテーションコメントは///または//!で記述します。///は関数やモジュールの直前に書き、その要素を説明します。//!はファイルやモジュール全体を説明する際に使用します。
/// 2つの整数を加算する関数
///
/// # 引数
/// * `a` - 1つ目の整数
/// * `b` - 2つ目の整数
///
/// # 戻り値
/// 2つの整数の合計
fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let sum = add_numbers(15, 25);
println!("合計: {}", sum);
}
このようにドキュメンテーションコメントを記述しておくと、cargo doc --openコマンドで自動生成されたドキュメントをブラウザで確認できます。チーム開発では特に、このドキュメント機能が大きな価値を持ちます。
コードの可読性を高めるために、適切なコメントと命名規則を守ることが重要です。Rustコミュニティでは、スネークケース(小文字とアンダースコア)を関数名や変数名に使い、キャメルケースを型名に使うのが標準的です。
ドキュメンテーションには、使用例を```で囲んで記述することもでき、それらの例はcargo test実行時にテストとしても実行されます。これにより、ドキュメントとコードの整合性が保たれます。