Rust
Rustはモダンなシステムプログラミング言語であり、安全性とパフォーマンスを両立させた特徴を持ちます。本書では、Rustの基本から応用までを網羅し、実践的なプログラミングスキルを身につけることを目指します。初めてのプログラミング言語としても、既存のプログラミングスキルを強化するためにも最適です。Rustの特徴である所有権システムや並行性モデルについても詳しく解説し、堅牢で効率的なプログラムを作成するための知識を提供します。これから一緒にRustの世界を探索し、その魅力を存分に体験していきましょう。
はじめに
[編集]Rust(ラスト)は、高性能かつ安全な並行処理を実現するために設計されたマルチパラダイム汎用プログラミング言語です[1]。 Rustの構文はC++に似ていますが、ボローチェッカーを使用して参照を検証し、メモリ安全性を保証することができます。Rustのボローチェッカーは、Rustコンパイラによって提供される静的解析ツールであり、所有権システムに基づいて、コード内の不正なボロー(借用)を検出するために使用されます。 Rustは、ガベージコレクターを使用せずにメモリ安全性を実現し、オプションでリファレンスカウントによる管理も可能です。 Rustは、システムプログラミング言語と呼ばれ、関数型プログラミングの要素を取り入れつつ、低レベルのメモリ管理メカニズムも提供しています。 そのため、高度な制御を必要とするアプリケーションの開発に最適です。Rustを使うことで、実行時にエラーを発生させることなく、安全で信頼性の高いコードを作成することができます。
コンパイラーにおいてセルフホスティングは、最初にコンパイルされたコンパイラーを使用して、同じ言語で書かれたソースコードをコンパイルすることで実現されます。そして、その新しいコンパイラーを使用して、さらに新しいソースコードをコンパイルし、それを繰り返すことで、より新しいバージョンのコンパイラーを作成することができます。
セルフホスティングは、ソフトウェアの信頼性や安定性を高めるために使用されることがあります。また、コンパイラーを自分自身でコンパイルすることは、その言語やコンパイラーの開発者がその言語やコンパイラーの知識と理解を深めるのにも役立ちます。クイックツアー
[編集]Rustはメモリ安全性、並行性、パフォーマンスの向上に焦点を当てたモダンなプログラミング言語です。
以下のRustのクイックツアーで、基本的な概念とコード例を紹介します。
- 基本構文:
- Rustプログラムは
main
関数から始まります。println!
マクロを使って標準出力に文字列を出力できます。 - hello.rs
fn main() { println!("Hello, world!"); }
- 実行結果
Hello, world!
- hello.rsは、Playgroundに作った、このプログラムへのリンクになっています。
- Rustプログラムは
- データ型:
- Rustには整数、浮動小数点数、真偽値などの基本データ型があります。
let age: i32 = 25; let salary: f64 = 50000.50; let is_rust_fun: bool = true; let message: &str = "Hello, Rust!";
- 制御構造:
if
、else if
、else
文で条件分岐ができます。while
ループやfor
ループで繰り返し処理ができます。let num = 10; if num > 0 { println!("Positive"); } else if num < 0 { println!("Negative"); } else { println!("Zero"); } for i in 0..5 { println!("Iteration {}", i); }
- 関数:
- 関数は
fn
キーワードを使って宣言します。 fn add(a: i32, b: i32) -> i32 { a + b } fn main() { let result = add(5, 3); println!("Sum: {}", result); }
- 関数は
- 所有権システム:
- Rustは所有権ベースのメモリ管理を採用しており、値の所有権が明確に定義されています。
fn main() { let s1 = String::from("Hello"); let s2 = s1; // s1の所有権がs2に移動する(所有権の転送) // println!("{}", s1); // エラー!s1はもう有効ではない println!("{}", s2); // 正常に動作 }
- 構造体とメソッド:
- 構造体はデータをまとめるためのカスタム型で、メソッドを持つことができます。
struct Car { model: String, year: u32, } impl Car { fn display_info(&self) { println!("Model: {}, Year: {}", self.model, self.year); } } fn main() { let my_car = Car { model: String::from("Toyota"), year: 2022, }; my_car.display_info(); }
ここでは、Rustの基本的な構文とコンセプトを簡単に紹介しました。
rustcのバージョン確認
[編集]やや力技ですが、Rustのコンパイラ rustc のバージョンをコードから確認できます。
- version.rs
fn main() { let version = std::process::Command::new("rustc") .arg("--version") .output() .expect("Failed to get Rust version"); if version.status.success() { let stdout = String::from_utf8_lossy(&version.stdout); println!("Rust version: {}", stdout); } else { eprintln!("Failed to retrieve Rust version information"); } }
- 実行結果
Rust version: rustc 1.81.0-nightly (b5b13568f 2024-06-10)
このコードは、Rustのプログラム内でrustc --version
コマンドを実行し、その結果(Rustコンパイラのバージョン情報)を取得しています。
std::process::Command::new("rustc")
:Command
構造体を使って新しいコマンドを作成しています。ここではrustc
というコマンドを実行するよう指定しています。.arg("--version")
:rustc
コマンドに--version
引数を渡しています。これにより、Rustコンパイラのバージョン情報を取得するよう指示しています。.output()
:Command
を実行して、その結果を取得します。ここでは--version
を指定したrustc
コマンドを実行し、その出力を取得しています。.expect("Failed to get Rust version")
: コマンドの実行が失敗した場合にエラーメッセージを表示します。if version.status.success() { ... } else { ... }
: 実行結果のstatus
をチェックして、コマンドが正常に終了したかどうかを確認します。もし成功していた場合は、コマンドの出力結果(Rustコンパイラのバージョン情報)を取得し、それを標準出力に表示します。もし失敗していた場合は、エラーメッセージを標準エラー出力に表示します。
このコードは、Rustのプログラム内で外部コマンドを実行してその出力を取得する方法を示しています。具体的には、Rustコンパイラのバージョン情報を取得してそれを表示する例です。
コメント
[編集]Rustのコメントには、C言語/C++と同じく一行コメントと範囲コメントがあります。
- 一行コメント
//
から行末までがコメントと見なされます。- 範囲コメント
/*
から*/
までがコメントと見なされます。- ネストは許されません。
- コメントの例
/* * プログラムのエントリーポイントは、main 関数です。 * 関数定義は fn から始まります。 */ fn main() { println!("Hello, world!"); // println! は関数ではなくマクロで、マクロは識別子の末尾に ! が付きます。 }
変数と型
[編集]Rustでは、変数を宣言する際にはデフォルトでimmutable(不変)です。変更可能な変数を宣言するには、mut
キーワードを使用します。変数の型はコンパイラによって推論されることが一般的ですが、型を明示的に指定することもできます。
例えば、変数の宣言と型指定は以下のようになります:
- decl.rs
fn main() { // 型推論による変数の宣言 let x = 5; // 整数型 i32 として推論される let y = 3.14; // 浮動小数点型 f64 として推論される println!("x = {x}, y = {y}"); // 型を明示的に指定する let z: i64 = 100; // 64ビット整数型 i64 println!("z = {z}"); }
Rustの基本的なデータ型には以下があります:
- 整数型 (
i8
,i16
,i32
,i64
,i128
,u8
,u16
,u32
,u64
,u128
など) - 浮動小数点型 (
f32
,f64
) - 論理値型 (
bool
) - 文字型 (
char
) - ポインタ型
- タプル型
- 配列型
- 列挙型
- 構造体型
- 文字列型 (
&str
,String
)
Rustは静的型付け言語であり、変数の型はコンパイル時に確定されます。型の安全性に対する厳格なチェックを行うため、コンパイル時に型の整合性が確認されます。これにより、メモリの安全性やスレッドセーフなコードを書く際の支援が期待できます。
変数とミュータブル・イミュータブル
[編集]Rustでは、変数を宣言するにはキーワード letを使います。 ディフォルトではイミュータブル(Immutable;宣言後には代入不能)な変数が宣言されます。 ミュータブル(Mutable;宣言後に代入可能)な変数を宣言するには、追加のキーワード mut を使います。
- hello-variables.rs
fn main() { let hello : &str = "Hello, world!"; println!("{}", hello); }
- 実行結果
Hello, world!
- 2行目の
let hello : &str = "Hello, world!";
が変数宣言です[2]。- &str(文字列のスライスのリファレンス)を型とする変数 hello を宣言し、"Hello, world!"で初期化しています。
- Rustには強力な型推論があり多くの場合不要ですが、
let 変数名 : 型名
の書式で型を伴い変数宣言することも出来ます。
mut をつけない場合には変数に「代入不能」と聞くと、C言語などを知っている人は「定数」を思い浮かべるかもしれませが、 Rustにおいて「定数」は, const 宣言された定数や, static 宣言されかつ mut で修飾されていない変数が相当します。
型推論
[編集]Rust では、変数宣言が初期値を伴っていた場合、変数の型を省略することができ、初期値の型が変数の型になります。
- hello-type-inference.rs
fn main() { let hello = "Hello, world!"; println!("{hello}"); }
- 実行結果
- 上に同じ
イミュータブル
[編集]Rust では、値が一度変数に let で束縛されると変更できません。これをイミュータブルと言います。
- hello-immutable.rs
fn main() { let hello : &str = "Hello, world!"; println!("{hello}"); hello = "Hello, rust!"; println!("{hello}"); }
- コンパイル結果
error[E0384]: cannot assign twice to immutable variable `hello` --> src/main.rs:4:5 | 2 | let hello : &str = "Hello, world!"; | ----- | | | first assignment to `hello` | help: consider making this binding mutable: `mut hello` 3 | println!("{hello}"); 4 | hello = "Hello, rust!"; | ^^^^^^^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable For more information about this error, try `rustc --explain E0384`. error: could not compile `playground` (bin "playground") due to 1 previous error
- イミュータブルな変数には、代入できないというコンパイルエラーです。
ミュータブル
[編集]代入可能、すなわちミュータブルにするためには、変数宣言にあたり let に続けて mut をつけます。
- hello-mutable.rs
fn main() { let mut hello : &str = "Hello, world!"; println!("{hello}"); hello = "Hello, rust!"; println!("{hello}"); }
- 実行結果
Hello, world! Hello, rust!
同じ変数名での宣言
[編集]同一スコープで同じ変数名での宣言は可能です。 同じ型である必要はありません。ミュータブルであるかイミュータブルであるかも問いません。 同じ変数名での宣言によって、それまで変数に束縛されていた値への参照がなくなります。
- 同じ変数名での宣言
fn main() { let hello : &str = "Hello, world!"; println!("{}", hello); let hello = 154649; println!("{}", hello); }
- 実行結果
Hello, world! 154649
定数
[編集]Rustには2種類の定数があり、どちらもグローバルスコープを含む任意のスコープで宣言することができます。また、どちらも明示的な型を持っている必要があります。
- const: 不変の値
- static: 静的寿命を持つミュータブルな値 静的寿命は推論されるので、指定する必要はありません。
- 2種類の定数
const HELLO : &str = "Hello, world!"; static LANGUAGE: &str = "Rust"; fn main() { println!("{HELLO}"); println!("{LANGUAGE}"); }
- 実行結果
Hello, world! Rust
コードを書き換えてconst宣言や(ミュータブルな)static宣言された変数に代入をしようとすると、エラーになります。
パターン
[編集]- pattern.rs
fn main() { let (mut x, mut y) = (5, 29); println!("x={x} y={y}"); (x, y) = (y, x); println!("x={x} y={y}"); }
- 実行結果
x=5 y=29 x=29 y=5
このコードは、x
と y
の値を交換するRustの機能を示しています。
- 最初の行では、
x
に 5 を、y
に 29 を代入しています。 - 次の行では、
println!
マクロでは、交換後のx
とy
の値を表示しています。 - 次の行では、
(x, y) = (y, x);
という操作を行っています。これは、タプルを使って複数の変数に同時に値を代入しています。この場合、(y, x)
というタプルの中身を(x, y)
に順番に代入しています。これにより、x
の値にy
の値が入り、y
の値にx
の値が入ります。これによってx
とy
の値が交換されます。 - 最後の
println!
マクロでは、交換後のx
とy
の値を表示しています。
このコードは、Rustのタプルを使った多値代入の機能を示しています。
データ型
[編集]Restには豊富なデータ型(Data Types)があり、それらを組み合わせて新しい型を作ることができます[3]。
スカラー型(Scalar Types)
[編集]スカラー型は単一の値を表します。Rustには、整数、浮動小数点数、論理値、文字という4つの主要なスカラ型があります。
整数型(Integer Types)
[編集]Rustの整数型は、符号の有無とビット幅から12種類のバリエーションがあります。
Rustの整数型 型名 説明 i8 符号付き8ビット整数 u8 符号なし8ビット整数 i16 符号付き16ビット整数 u16 符号なし16ビット整数 i32 符号付き32ビット整数 u32 符号なし32ビット整数 i64 符号付き64ビット整数 u64 符号なし64ビット整数 i128 符号付き128ビット整数 u128 符号なし128ビット整数 isize 符号付きでポインタと同じサイズの整数 usize 符号なしでポインタと同じサイズの整数
- isizeとusizeのビット幅はプロセッサーのアーキテクチャーによって定義され、32ビットプロセッサーならば32、64ビットプロセッサーならば64です。
整数リテラル(Integer literals)
[編集]リテラル(Literals)とは、プログラミングのソースコードで使用される、数値や文字列などのデータを直接表現したものです。
様々な整数リテラル 基数 表現 10 19_800 16 0xbadbeef 8 0o777 2 0b101_111_011 バイト(u8のみ) b'Q'
- 例
fn main() { println!("{:?}", 19_800); println!("{:x}", 0xbadbeef); println!("{:o}", 0o777); println!("{:b}", 0b101_111_011); println!("{}", b'Q'); }
- 実行結果
19800 badbeef 777 101111011 81
- 数値リテラルには、123u8 の様に型名をタイプサーフィックス(type suffix)として補うことで、ビット幅を明記できます(オプショナル)。
- 指定されない場合は(バイト以外は)i32が仮定されます(isizeではありません)。
- 数値リテラルには、読みやすさのため 9_281_636 のように、アンダースコア _ を補うことができます(オプショナル)。
なお、{:x}
の{x}
部分はプレースホルダーのファーマッティング・トレイツです。「x」なら16進数、oなら8進数、bなら2進数で出力します。
浮動小数点数型(Floating-Point Types)
[編集]Rustには、浮動小数点数を表現するための2つの主要な型があります。それぞれの型は、IEEE-754規格に従っています。
f32
: 32ビットの単精度浮動小数点数型です。精度は約6桁です。f64
: 64ビットの倍精度浮動小数点数型です。精度は約15桁です。
Rustでは、浮動小数点数リテラルを書く場合、デフォルトで f64
型になります。例えば、3.14
という浮動小数点数リテラルは、f64
型の数値になります。
以下は、f32
型と f64
型の浮動小数点数の使用例です。
fn main() { // デフォルトでは f64 型になる浮動小数点数 let my_float1 = 3.14; // f64 型 // サイズを明示して f32 型にする let my_float2: f32 = 2.718; // f32 型 // 浮動小数点数同士の計算 let sum = my_float1 + f64::from(my_float2); // f64 型にキャストして計算 println!("Sum: {}", sum); // f64 型の結果が出力される }
- 実行結果
Sum: 5.857999935150147
浮動小数点数は、数値計算や科学的な計算など、精度が求められる場面で使用されます。しかし、浮動小数点数の性質や精度による注意が必要な場面もありますので、注意深く扱う必要があります。
論理値型(The Boolean Type)
[編集]Rustにおける論理値型の型名は bool
で、真の値は true
、偽の値は false
です。この型は非常に基本的で、条件分岐やブール演算などで使用されます。
以下は bool
型の使用例です。
fn main() { let is_rust_cool = true; // 真の値を持つ変数 let is_java_cool = false; // 偽の値を持つ変数 if is_rust_cool { println!("Rust is cool!"); // 条件が true の場合に実行される } else { println!("Rust is not cool."); // 条件が false の場合に実行される } // 論理演算 let result = is_rust_cool && is_java_cool; // 論理積 (AND) の例 println!("Result of logical AND: {}", result); // false が出力される }
- 実行結果
Rust is cool! Result of logical AND: false
bool
型は条件式の評価や論理演算に広く使用され、プログラムの流れを制御するための基本的な手段として重要な役割を果たします。
文字型(The Character Type)
[編集]Rustの文字型 char
はUnicodeの単一の文字を表し、32ビットで符号化されます。Unicodeのサロゲートペアを含む広範な範囲の文字を表現できます。
以下のコードは、char
型を使用してUnicode文字やサロゲートペアを扱う例です。
fn main() { // 単一のUnicode文字の表現 let unicode_char = '😊'; // 笑顔の絵文字 (U+1F60A) println!("Unicode char: {}", unicode_char); // サロゲートペアの表現 let surrogate_pair = '\u{1F601}'; // 涙の絵文字 (U+1F601) println!("Surrogate pair: {}", surrogate_pair); // char型のサイズを取得 println!("Size of char: {} bytes", std::mem::size_of::<char>()); }
- 実行結果
Unicode char: 😊 Surrogate pair: 😁 Size of char: 4 bytes
ここでは、'😊'
という絵文字や \u{1F601}
というサロゲートペアを char
型として表現しています。絵文字やサロゲートペアも正しく表示されることを確認できます。また、std::mem::size_of::<char>()
を使って char
型のサイズを表示しています。
文字列型(The String Type)
[編集]Rustには2つの主要な文字列型があります。
&str
型 (文字列スライス):- メモリ内のデータへの不変の参照を表します。
- UTF-8でエンコードされた文字列を参照します。
- 文字列リテラルや他のデータ構造の一部として使用されます。
let string_slice: &str = "Hello, Rust!"; // 文字列リテラルから作成された文字列スライス
String
型:- ヒープ上に確保された可変の文字列データを持ちます。
- 動的に変更可能で、文字列の追加や削除、変更が可能です。
let mut string_object = String::from("Hello"); // String型のインスタンスを生成 string_object.push_str(", Rust!"); // 文字列を追加
これらの型は互いに相互変換できます。例えば、&str
からString
への変換はto_string()
メソッドを使用できます。
let my_string: String = "Hello".to_string(); // &strからStringへの変換
また、String
から&str
への変換は、&
演算子を使用して参照を取得します。
let my_string: String = String::from("Hello"); let string_ref: &str = &my_string; // Stringから&strへの変換
文字列操作に関しては、String
型が動的に変更可能で柔軟性があり、&str
型は主に静的な文字列の参照として使用されます。
- 様々な文字列リテラル
- string.rs
fn main() { println!("{:?}", "hello"); // エスケープされた文字列 println!("{:?}", r#"hello"#); // エスケープされないraw文字列 println!("{:?}", b"hello"); // エスケープされたバイト文字列 println!("{:?}", br#"hello"#); // エスケープされないrawバイト文字列 }
- 実行結果
"hello" "hello" [104, 101, 108, 108, 111] [104, 101, 108, 108, 111]
"{:?}"
はデバッグ用のフォーマット指定子で、デバッグ表示用の形式で出力します。r#"..."#
はエスケープされない raw 文字列リテラルで、内部のエスケープが無視されます。b"..."
はバイト文字列リテラルで、ASCII文字のバイト値の配列を示します。br#"..."#
はエスケープされない raw バイト文字列リテラルです。
これらのリテラルは、異なる用途で利用されることがあり、それぞれの特性や振る舞いが異なります。
ユニット型(The Unit Type)
[編集]Rustにおけるユニット型は()
で表されます。ユニット型は特別な型であり、単一の値 ()
だけから成り立ちます。主に2つの用途があります:
- 関数の戻り値としての利用: 副作用のない関数や手続きにおいて、何も返す必要がない場合にユニット型
()
が使用されます。fn do_something() { // 何らかの処理 } fn main() { let result = do_something(); // 戻り値は () になる println!("result = {:?}", result); }
- 構造体のフィールドとしての利用: 構造体のフィールドとしてユニット型を持つことで、その構造体のインスタンスが存在することを示す場合に使用されます。
#[derive(Debug)] struct MarkerUnit; fn main() { let marker = MarkerUnit; // ユニット型を持つ構造体のインスタンス化 println!("marker = {:?}", marker); }
ユニット型は一見すると何も持たない型ですが、プログラムの構造を表現するために重要な役割を果たしています。特に関数の戻り値として使用されることが多いです。
複合型(Compound Types)
[編集]複合型(Compound Types)は、複数の値を1つの型にまとめることができます。 Rustにはタプルとアレイという2つのプリミティブな複合型があります。
タプル型(The Tuple Type)
[編集]タプル(The Tuple)は、さまざまな型の値を1つの複合型にまとめる一般的な方法です。 タプルの長さは固定されており、一度宣言すると大きくしたり小さくしたりすることはできません。
配列型(The Array Type)
[編集]複数の値の集まりを持つもう一つの方法として、配列(The Array)があります。 タプルとは異なり、配列の各要素は同じ型でなければなりません。 Rustの配列は、タプルと同じく長さが固定されています。
プレースホルダー
[編集]println! マクロなどの文字表示マクロでは、文字列中の {
}
の位置に指定された書式で展開します[4]。
C言語の標準関数 printf() と機能は似ていますが書式は大きく異なり、Pythonのf文字列との共通点が多いです。
フォーマッティング・トレイツ(Formatting traits)
[編集]{}
を指定すればその型の自然なフォーマット(fmt::Display; 100 ならば "100")で文字列化されます。
しかし、基数を変えた16進数や8進数や2進数などでフォーマットをしたいときはフォーマッティング・トレイツ(Formatting traits)を使います[5]。
- Formatting traits
fn main() { println!("{:?}", 100); println!("{:x}", 100); println!("{:o}", 100); println!("{:b}", 100); println!("{}", 100); }
- 実行結果
100 64 144 1100100 100
フォーマッティング・トレイツは、Rustプログラミング言語におけるデータのフォーマット(整形)方法を定義するためのトレイト(trait)です。これらのトレイトは、std::fmt
モジュールに定義されており、std::fmt::Display
やstd::fmt::Debug
など、様々なフォーマット方法を提供します。
一般的なフォーマッティング・トレイツは以下のようになります:
- Display:
std::fmt::Display
トレイトは、{}
(中括弧)を使ったシンプルな人間が読みやすい形式での表示を提供します。これは、println!
マクロやformat!
マクロで使われます。 - Debug:
std::fmt::Debug
トレイトは、{:?}
を使ったデバッグ目的の表示を提供します。これは、デバッグ情報を表示する際に便利で、println!("{:?}", variable)
のように使用されます。 - Binary:
std::fmt::Binary
トレイトは、{:#b}
を使ってバイナリ表現での表示を提供します。 - Octal:
std::fmt::Octal
トレイトは、{:#o}
を使って8進数表現での表示を提供します。 - LowerHex / UpperHex:
std::fmt::LowerHex
およびstd::fmt::UpperHex
トレイトは、それぞれ{:#x}
および{:#X}
を使って16進数表現での表示を提供します。
これらのトレイトは、カスタム型をフォーマットする方法を指定するために、対応するメソッドを実装することで利用されます。
例えば、Display
トレイトを実装することで、自分で定義した型を{}
を使って表示することができます。
以下は、簡単な構造体Person
にDisplay
トレイトを実装して、{}
を使ってカスタム型を表示する例です。
use std::fmt; // 構造体を定義 struct Person { name: String, age: u32, } // Displayトレイトの実装 impl fmt::Display for Person { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // カスタムフォーマットを定義する write!(f, "Name: {}, Age: {}", self.name, self.age) } } fn main() { let person = Person { name: String::from("Alice"), age: 30, }; // {}を使ってPerson型を表示 println!("Person Details: {}", person); }
- 実行結果
Person Details: Name: Alice, Age: 30
この例では、Person
という構造体を定義し、fmt::Display
トレイトを実装しています。fmt
モジュール内のFormatter
型を利用して、write!
マクロを呼び出しています。main
関数内でprintln!
マクロを使ってPerson
型を表示しています。これにより、Person
型を{}
を使って表示する方法が示されています。
所有権システム
[編集]Rustの所有概念や借用、参照などは、安全性と並行性を重視したユニークな特徴を持っています。それぞれの概念をコードとともに解説してみましょう。
所有概念
[編集]Rustの所有概念は、言語の中核的な概念であり、メモリ管理とリソースの安全な取り扱いを実現するための重要な要素です。所有概念は、変数がリソースの所有権を持ち、そのリソースはスコープを抜ける際に自動的に解放されるという原則に基づいています。
コピーとムーブ
[編集]Rustのコピーとムーブの動作は、変数がスタック上のデータかヒープ上のデータを参照しているかによって異なります。プリミティブ型のようなスタック上のデータはコピーされますが、ヒープ上のデータは所有権がムーブされます。
- コピー可能なデータ
fn main() { let x = 5; let y = x; // xの値がコピーされる(コピー) println!("x: {}, y: {}", x, y); // 正常に出力される }
この例では、x
は整数型(i32
)などのプリミティブ型で、スタック上に格納されます。x
の値は単純なコピーが作成され、y
にも同じ値が代入されます。そのため、両方の変数を利用できます。
- ムーブされるデータ
fn main() { let s1 = String::from("Hello"); let s2 = s1; // s1の所有権がs2に移動(ムーブ) // println!("{}", s1); // コンパイルエラー!所有権が移動したためs1は利用できない println!("{}", s2); // 正常に出力される }
この例では、String::from("Hello")
で作成した文字列はヒープ上にあり、s1
の所有権はs2
にムーブされます。そのため、s1
を参照しようとするとコンパイルエラーが発生しますが、s2
は正常に出力できます。
この違いは、スタック上のデータの単純なコピーと、ヒープ上のデータの所有権のムーブの挙動に起因しています。スタック上のデータは安価にコピーできますが、ヒープ上のデータは所有権が移動されます。
理解を助けるために、コピーとムーブの違いを表にまとめてみましょう。
コピーとムーブの違い !コピー(Copy) ムーブ(Move) プリミティブ型(整数型、bool型など) コピーされる コピーされる ヒープ上のデータ(String型、Vec型など) コピーされる 所有権が移動される
- コピー(Copy)
- スタック上に配置されるデータや、Copyトレイトを実装した型のデータは、単純な代入などでコピーされます。元の変数が保持する値が複製され、両方の変数が別々の値を持ちます。
- ムーブ(Move)
- ヒープ上に配置されるデータや、Copyトレイトを実装していない型のデータは、所有権がムーブされます。変数間での代入などで、所有権が移動された後は、元の変数はその値を参照できなくなります。
これらの違いは、Rustの所有概念において重要です。特に、ムーブによってデータの所有権が移動することで、メモリ安全性と競合を回避することが可能になります。
借用と参照
[編集]所有権を移動させずにデータを利用するために、借用と参照が利用されます。
- 借用と参照の例
fn main() { let s1 = String::from("Hello"); // s1を借用する(イミュータブルな参照) let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len); // 正常に出力される } fn calculate_length(s: &String) -> usize { s.len() // sを参照して長さを返す }
calculate_length
関数では、&String
型の引数を受け取り、String
への参照(イミュータブルな参照)を使用しています。そのため、s1
の所有権を関数に渡すことなく、値を参照しています。
ミュータブルな参照
[編集]また、ミュータブルな参照を使って可変の値を変更できます。
- ミュータブルな参照の例
fn main() { let mut s = String::from("Hello"); change_string(&mut s); println!("{}", s); // "Hello, goodbye"が出力される } fn change_string(s: &mut String) { s.push_str(", goodbye"); }
&mut String
型のミュータブルな参照を使って、change_string
関数で文字列を変更しています。
これらのコード例は、Rustの所有概念や借用、参照についての基本的な概念を示しています。これらの機能は、安全性と並行性を強調したプログラミングを実現するための強力なツールです。
関数のパラメータにした変数は関数にムーブされる
[編集]関数のパラメータに渡された変数は、所有権(Ownership)がその関数にムーブ(Move)されることがRustの所有権システムにおいて基本的な挙動です。このことを以下のコードを用いて詳しく説明しましょう。
fn main() { let original = String::from("Hello, Rust!"); // String型の変数originalを作成 let moved = move_ownership(original); // originalがmove_ownership関数にムーブ // コンパイルエラー! originalはもう有効でない println!("original: {}", original); // 正常に動作! movedはmove_ownership関数から返された値 println!("moved: {}", moved); } fn move_ownership(input: String) -> String { // inputの処理を行う(ここでは何もしない) input }
このコードで示されているポイントは以下です:
main
関数内で、String
型の変数original
を生成し、その所有権を保持しています。move_ownership
関数を呼び出す際、変数original
が関数の引数として渡されます。このとき、original
の所有権はmove_ownership
関数にムーブされ、もはやmain
関数内でoriginal
を使用することはできません。move_ownership
関数内で、input
パラメータ(ここではString
型)の処理が行われ、その値がinput
を含むスコープからムーブされずに、move_ownership
関数から返されます。main
関数内で、original
を使用しようとすると、コンパイルエラーが発生します。それに対して、moved
変数はmove_ownership
関数から返された値であり、その所有権はムーブされていないため、正常に使用できます。
この挙動は、Rustの所有権システムの一部であり、変数を関数に渡すと、所有権がムーブされることで、データ競合や予測不能な挙動を防ぐのに役立ちます。つまり、Rustはコピーが高コストなデータ(例: String
やファイルハンドル)の二重所有権を防ぎ、メモリ安全性を確保するために所有権のムーブを使用します。
クローン
[編集]所有権を共有するために、clone
メソッドを使用してデータの複製を作成できます。
この複製は新しい所有者を持ち、元のデータを変更しても影響を受けません。
fn main() { let data1 = vec![1, 2, 3]; let data2 = data1.clone(); // データの複製 println!("{:?}", data1); // data1は所有権を保持 println!("{:?}", data2); // data2も所有権を持つ }
- 所有権の基本ルール:
- Rustの所有権には3つの基本的な規則があります:
- 各値は所有者(Owner)を持ちます。
- 各値はいつかは所有者からスコープを抜けて消去されます。
- 各値の所有権は、1つの所有者にしか属しません。
- Rustの所有権には3つの基本的な規則があります:
- 所有権の転送:
- 所有権は値の転送として理解され、変数の代入時に所有権が移動します。これにより、元の変数は無効になり、新しい変数が所有権を持つことになります。
- クローン:
clone
メソッドを使用してデータのコピーを作成し、所有権を共有できます。これにより、複数の変数が同じデータを持つことが可能となります。
- 借用:
- 所有権を譲渡せずにデータにアクセスするための仕組みで、不変な借用(immutable borrowing)と可変な借用(mutable borrowing)があります。借用は所有権を一時的に保持し、データへのアクセスを提供します。
- 所有権と関数:
- 関数に値を渡すと、所有権が関数にムーブされます。関数が所有権を持つと、元の変数は無効になり、関数内で変数を使用できます。これにより、データの複製やコピーを回避できます。
- 所有権の安全性と競合の防止:
- 所有権システムはメモリ安全性と競合の防止に役立ち、コンパイル時のエラーを通じてプログラムの安全性を確保します。データ競合やメモリリークを防ぎ、プログラマに信頼性の高いコードを書かせます。
- ライフタイム:
- 所有権システムと関連するライフタイムの概念は、借用や参照のスコープを制御し、メモリリークを防止します。ライフタイム注釈(lifetime annotations)を使用して、データの有効な範囲を指定します。
識別子
[編集]Rustの変数の識別子(Identifier)について説明します。識別子は変数、関数、モジュール、および他のRustプログラムの要素に名前を付けるために使用される名前です。
Rustの識別子にはいくつかの規則があります:
- 識別子はアルファベット、数字、アンダースコア(_)の組み合わせからなります。
- 識別子はアルファベット(大文字または小文字)またはアンダースコアで始まる必要があります。数字で始まることはできません。
- 識別子はキーワード(予約語)として定義されている単語を使用できません。たとえば、
let
、fn
、match
などのキーワードは識別子として使用できません。 - 識別子は大文字と小文字を区別します。つまり、
my_variable
とMy_Variable
は異なる識別子です。
一般的な識別子の例:
my_variable
:変数の名前calculate_sum
:関数の名前MyStruct
:構造体の名前my_module
:モジュールの名前MAX_VALUE
:定数の名前
Rustの識別子はわかりやすく、意味のある名前を使うことが推奨されます。また、Rustのコーディング規約に従うことも重要です。識別子はプログラムの可読性と保守性に影響を与えるため、適切な名前を選択することが重要です。
スコープ
[編集]Rustにおいて、スコープ(Scope)は変数やリソースの有効な範囲を指します。スコープ内で定義された変数は、そのスコープ内でのみアクセス可能であり、スコープを抜けると変数は自動的に解放されます。スコープの概念は、メモリリークや予期しない副作用を防ぐために非常に重要です。
Rustには主にブロックスコープと関数スコープの2つのタイプのスコープがあります。
ブロックスコープ
[編集]ブロックスコープは、一連のコードが中括弧 {}
で囲まれた場合に発生します。中括弧内の変数は、そのブロックスコープ内でのみ有効です。
以下はブロックスコープの例です:
fn main() { let outer_variable = 42; { let inner_variable = 10; println!("inner_variable: {}", inner_variable); } // inner_variableのスコープはここで終了 // inner_variableにはアクセスできない // println!("inner_variable: {}", inner_variable); println!("outer_variable: {}", outer_variable); }
この例では、inner_variable
は中括弧内でのみ有効であり、外部からアクセスできません。しかし、outer_variable
は中括弧の外部でもアクセス可能です。
関数スコープ
[編集]関数スコープは、関数内で定義された変数がその関数内でのみ有効であることを示します。
以下は関数スコープの例です:
fn main() { let outer_variable = 42; fn inner_function() { let function_variable = 5; println!("function_variable: {}", function_variable); } inner_function(); // inner_function内での変数にアクセスできる // function_variableにはアクセスできない // println!("function_variable: {}", function_variable); println!("outer_variable: {}", outer_variable); }
function_variable
は inner_function
内でのみ有効で、inner_function
を呼び出すことでアクセスできます。
スコープと変数の所有権
[編集]Rustの所有権システムは、スコープと密接に関連しています。変数が特定のスコープ内で有効である場合、そのスコープを抜けると変数の所有権は無効になります。これにより、リソースの安全な解放が保証され、メモリリークやダングリングポインタの問題が回避されます。
スコープと所有権の組み合わせは、Rustのプログラムの安全性と信頼性を高める重要な要素です。スコープをうまく活用し、不要な変数の寿命を短くしてリソースを効率的に管理できるようにしましょう。
ライフタイム
[編集]Rustのライフタイムは、Dangling Reference(無効な参照)を防ぐための重要な概念です。これは、参照が有効なデータを参照している間、そのデータが存在していることを保証するために導入されています。
Rustのライフタイムは、参照が指すデータが存在するスコープを表現し、コンパイラによって所有権やライフタイムのルールに違反しないように検証されます。これにより、コンパイラがコードを解析し、参照が有効な間のみデータにアクセスできることを保証します。
Dangling Referenceは、プログラムが無効なメモリ領域を参照しようとすることで発生する問題です。例えば、参照が指すデータがスコープを抜けた後に破棄されてしまった場合、その参照は無効なものとなります。Rustのライフタイムは、このような問題を静的に(コンパイル時に)検出し、Dangling Referenceを防ぐための仕組みとして機能します。
ライフタイムの概念により、コンパイラは参照の寿命をトラッキングし、参照が有効なデータを参照していることを保証します。これにより、実行時にDangling Referenceが発生する可能性を排除し、メモリの安全性を確保します。
ライフタイムを使用してDangling Referenceを回避する例を示します。以下の例では、ライフタイムを使用して参照が有効な間のみデータにアクセスできるように制御しています。
struct MyStruct<'a> { data: &'a str, } fn main() { let string_data = String::from("Hello, Rust!"); { let my_struct; // my_structのスコープを新たに定義 { // `string_data`の参照を`MyStruct`に渡す my_struct = MyStruct { data: &string_data }; // `my_struct`が有効なスコープ内でデータを表示 println!("{}", my_struct.data); } // ここで`my_struct`のスコープが終了する // この時点で`my_struct`の参照はスコープ外になるため、 // `string_data`が無効になる前に`my_struct`が使用されることはない } // `string_data`のスコープが終了する // ここで`string_data`はスコープ外になる // したがって、`my_struct`の参照も同時に無効になる }
この例では、MyStruct
にstring_data
への参照を渡し、my_struct
が有効なスコープ内でデータを使用しています。その後、my_struct
が有効なスコープを抜ける前にstring_data
のスコープが終了するため、Dangling Referenceが発生することはありません。
Rustのライフタイムシステムは、このようにスコープと参照の寿命をトラッキングし、参照が有効な間のみデータへのアクセスを許可します。これにより、Dangling Referenceをコンパイル時に防ぐことができます。
ライフタイムの役割
[編集]- 有効性の保証: ライフタイムは、参照が有効である期間を定義します。これにより、コンパイラは参照が無効化されたり、有効でないメモリを参照しようとする問題を検知できます。
- 参照のスコープ: ライフタイムは、参照が使用可能なスコープを定義します。参照はそのスコープ内でのみ有効であり、スコープ外では無効になります。
ライフタイムの宣言
[編集]- 引数や返り値: 関数の引数や返り値にライフタイム注釈を追加することで、参照の有効期間を示します。
- 構造体とメソッド: 構造体やメソッドの定義でライフタイムを使うことがあります。特に、参照を持つ構造体の定義においてライフタイムは重要です。
struct MyStruct<'a> { data: &'a str, } impl<'a> MyStruct<'a> { fn get_data(&self) -> &'a str { self.data } }
ライフタイムの省略
[編集]- Rustでは、ライフタイムの一部は省略されることがあります。コンパイラが推論可能な場合は、ライフタイムの注釈を省略できます。ただし、省略ルールには厳密な規則があります。
'static
ライフタイム
[編集]'static
はプログラム全体のライフタイムを示します。プログラムの実行中に常に存在し続けるデータやリソースに使用されます。
static GLOBAL_VALUE: i32 = 100; fn main() { let local_value: &'static i32 = &GLOBAL_VALUE; }
ライフタイムはRustの主要な安全性の一部であり、メモリ安全性を保証するための強力なツールです。適切に理解して利用することで、メモリリークや無効な参照による問題を防ぐことができます。
スコープとライフタイムの関係:
[編集]スコープは、Rustにおけるライフタイム概念に密接に関連しています。ライフタイムは基本的には参照の有効期間を表し、その有効期間はスコープによって定義されます。
- スコープ: コード内の特定の部分で変数が有効である期間を示します。変数がスコープに入ると有効になり、スコープを抜けると無効になります。スコープは波括弧
{}
で囲まれたブロックや、関数の本体などで定義されます。
{ let variable = 10; // 変数のスコープ開始 // ... ここで variable が有効 ... } // 変数のスコープ終了
- ライフタイム: 参照が有効な期間を示します。変数への参照を作成する際にそのライフタイムが重要になります。参照はそのライフタイム内でのみ有効であり、スコープを抜けると無効になります。
fn print_string(s: &str) { println!("{}", s); } fn main() { let string = String::from("Hello"); print_string(&string); // string の参照が関数のスコープ内で有効 } // string のスコープ終了、参照も無効になる
ライフタイムとスコープは互いに関連しており、変数が有効な期間がスコープによって定義されると、その変数への参照の有効期間も同様にスコープによって制限されます。このようにして、Rustのコンパイラは参照の有効性を追跡し、安全性を確保します。
所有権とライフタイムの関係:
[編集]所有権とライフタイムは、Rustのメモリ管理における重要な概念ですが、やや異なる側面を持っています。
- 所有権
- Rustの所有権システムは、変数に対するデータの所有権を定義します。変数が特定のデータを所有している場合、その変数がスコープを抜けるとデータも解放されます。
- 所有権ルールにより、特定のデータは同時に複数の所有者を持つことができません。一つのデータに対する所有権は一つの変数にしか与えられません。
fn main() { let s1 = String::from("Hello"); let s2 = s1; // s1の所有権がs2にムーブ(転送)される // s1は所有権を失い、スコープを抜けることができる println!("{}", s2); // s2は有効 } // s2のスコープ終了、s2の所有権が終了しデータが解放される
- ライフタイム
- ライフタイムは、参照が有効である期間を示します。参照はそのライフタイム内でのみ有効であり、スコープ外では無効になります。
- 所有権とライフタイムは密接に関連しており、所有権が終了した後は、そのデータへの参照も無効になります。
fn print_string(s: &str) { println!("{}", s); } fn main() { let string = String::from("Hello"); print_string(&string); // stringの参照が関数のスコープ内で有効 } // stringのスコープ終了、参照も無効になる
所有権とライフタイムは共に、Rustがメモリの安全性を確保するための要素です。所有権システムはメモリの解放を自動的に行い、ライフタイムは参照の有効性を管理し、無効な参照が存在しないようにします。両者が組み合わさることで、Rustはメモリリークやダングリングポインタ(無効な参照)を防止し、安全で信頼性の高いコードを作成することができます。
<!ーー RcやBoxやDropもここか? ーー>
制御構造
[編集]Rust では、if
やfor
などの制御構造も式です。
分岐
[編集]Rust は、if と match の2つの分岐構文を持ちます。
if
[編集]ifは、条件式に基づき分岐し、分岐先の式を評価します。
ifの値は、分岐先の式の値です。
elseを省略し条件式が偽であった場合のifの値は ()
です。
- 構文
if-expr := if 条件式 '{' 式1 '}' [ else '{' 式2 ] '}'
- 条件式に整数を使うと
fn main() { let i = 0; if i { println!("zero"); } }
- コンパイルエラー
error[E0308]: mismatched types --> src/main.rs:4:8 | 4 | if i { | ^ expected `bool`, found integer For more information about this error, try `rustc --explain E0308`.
- Rustでは、ifに限らず、条件式は、bool 型でなければいけません。
- if.rs
fn main() { let i = 0; if i == 0 { println!("零"); } else { println!("非零"); } let s = if i == 0 { "ゼロ" } else { "非ゼロ" }; println!("{}", s); }
- 実行結果
零 ゼロ
- 気をつけたいのは、式の値が参照されるifでは、それぞれの式(ここでは
"ゼロ"
と"非ゼロ"
)にセミコロン;
を付けてはいけない点です。- もし セミコロン
;
をつけると、ブロックの値は、Unit 型()
になります。 - ifの、分岐先の2つの式の型は同じでなければいけません。
- else節が省略された場合は、Unit 型を返さなければいけません。
- 式の最後に、セミコロン
};
が必要ということです[6]。
- 式の最後に、セミコロン
- もし セミコロン
- また、
let s = if i == 0 {
の文末の};
のセミコロンも忘れがちです。 - Rust では if に限らず、コードブロックは式の一種で値を返します[7]。その場合、コードブロックの最後の式がコードブロックの値となりセミコロン
;
は不要で、もし、;
をつけると;
の次の式(=空文)の値()
がコードブロックの値になります。
この特徴は、関数型プログラミングを意識したものですが、同時に後に説明する所有権の移譲で重要な役割を果たします。
if と else の連鎖
[編集]- if-else.rs
fn main() { let n = 0.0 / 0.0; if n < 0.0 { println!("負の数"); } else if n > 0.0 { println!("正の数"); } else if n == 0.0 { println!("零"); } else { println!("{n}"); } }
- 実行結果
NaN
このコードの中で、次のことが行われています:
n
に NaN(Not a Number)を代入しています。0.0 / 0.0
は浮動小数点数のゼロで割り算を表し、NaN を返します。if
文でn
の値に応じて条件分岐しています。n
が負の数より小さい場合は、"負の数" と出力します。n
が正の数より大きい場合は、"正の数" と出力します。n
がゼロと等しい場合は、"零" と出力します。- それ以外の場合は、
n
の値を{n}
という形式で出力します。
しかし、Rustの浮動小数点数型では、NaN は <
や >
、==
などの比較演算子によって、他の数値との大小や等価性を比較することはできません。なぜなら NaN は "not a number" であり、数値としての大小関係が定義されていないためです。
そのため、このコードでは n
が NaN である場合、どの条件にも合致せず、最後の else
ブロックが実行されることになります。このブロックでは、n
の値そのものを {n}
という形式で出力しようとしています。
Some()
[編集]Rustでは、C言語のNULLに相当する値は None です。 通常の変数は None の代入は受付けませんが、Some() を使うと None を取り得る変数が宣言できます[8]。
- some.rs
fn main() { let mut x = Some(0); println!("{:?}({})", x, type_of(x)); x = None; println!("{:?}({})", x, type_of(x)); } fn type_of<T>(_: T) -> &'static str { std::any::type_name::<T>() }
- 実行結果
Some(0)(core::option::Option<i32>) None(core::option::Option<i32>)
Some() 及び None は、標準ライブラリ std::option で定義されていますが、初期化済みなので Use することなく、接頭辞Option::なしで使えます。
if let
[編集]Rustには、if let
という特別な制御構造があります。if let
は、match
式を簡略化するために使用されます。if let
式は、単一のパターンを使用して、変数が指定された値にマッチした場合にのみ式を実行します。if let
を使用すると、冗長なマッチングのコードを削減できます。
以下は、if let
を使用した例です。
- if-let.rs
fn main() { let mut v = vec![2, 3, 5]; if let Some(x) = v.pop() { println!("x = {}", x) } else { println!("done!") } if let Some(x) = v.pop() { println!("x = {}", x) } else { println!("done!") } if let Some(x) = v.pop() { println!("x = {}", x) } else { println!("done!") } if let Some(x) = v.pop() { println!("x = {}", x) } else { println!("done!") } if let Some(x) = v.pop() { println!("x = {}", x) } else { println!("done!") } }
- 実行結果
x = 5 x = 3 x = 2 done! done!
.pop()
メソッドは、ベクターから最後の要素を取り出し、それを返します。ただし、ベクターが空である場合はNone
を返します。- 最初の行では、
vec![2, 3, 5]
という3つの要素を持つベクターを作成しています。 - その後、5回の
if let
式を使用して、ベクターから要素を取り出し、それを表示するか、ベクターが空の場合にはdone!
と表示します。 - 各
if let
式では、変数x
を定義しています。v.pop()
の返り値がSome
であれば、ベクターの最後の要素が変数x
に束縛され、println!
マクロを使用してx
を表示します。- そうでない場合、すなわち、ベクターが空である場合は、
else
節が実行されて、done!
が表示されます。
match
[編集]match
は、値とパターンをマッチングして、対応するコードブロックを実行します。これは、複数の条件に基づいて処理を行う場合に非常に便利です。例えば、列挙型(enum)を使った状況では、match
は特に有用です。
enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u32 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 }, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } }
上記の例では、Coin
型の列挙子を受け取り、それぞれのコインに対する価値を返す関数が定義されています。match
はCoin
の値を取り、各列挙子に対応するコードブロックを実行します。例えば、Coin::Penny
の場合、"Lucky penny!"というメッセージが表示され、1が返されます。
match
は、パターンに加えてif
条件も組み合わせて使用できます。
fn main() { let some_value = Some(5); match some_value { Some(x) if x < 5 => println!("Less than 5: {}", x), Some(x) => println!("Value: {}", x), None => println!("No value"), } }
この例では、Some
型の値を持つ変数をmatch
で処理しています。if
条件はSome
型であり、かつその値が5未満の場合にマッチします。それ以外の場合、単に値を表示します。
match
はRustの強力なツールであり、パターンマッチングにより、安全性と表現力を向上させます。
反復
[編集]Rustには、以下のような反復構文があります。
それぞれの構文について、詳しく解説していきます。
for
[編集]Rust の for は、指定された範囲内の値を反復処理するために使用されます。通常、配列、ベクトル、範囲、またはイテレータなどの反復可能オブジェクトに対して使用されます。
- 構文
for_expression = "for" loop_variable "in" expression "{" statement* "}"; loop_variable = pattern; expression = (expression_binop | expression_unop | expression) ; pattern = identifier | "_" | literal ;
for_expression
はforループを表し、loop_variable
は反復処理のために使用される変数、expression
は反復処理の対象となるデータのソース、statement
はループ内で実行される文を表します。pattern
は、ループ変数の型と一致するパターンを表します。identifier
は、識別子の名前を表します。literal
は、文字列、数値、真偽値などのリテラル値を表します。- forループは、
loop_variable
によって定義された変数にexpression
で指定されたデータソースの値を順番に割り当て、それぞれの値に対してstatement
を実行します。 identifier
は識別子を、literal
はリテラルをしめします。
Range
[編集]Rustにおけるrange
は、範囲を表す型です。範囲の生成には2つの主要な方法があります。
- ..(半開区間)
start..end
の形式で使用され、start
からend
の手前(end
は含まれない)までの範囲を生成します。
例えば、1..5
は1から4までの範囲を生成します。
- ..= (閉区間)
start..=end
の形式で使用され、start
からend
までの閉区間を生成します(end
も含まれます)。
例えば、1..=5
は1から5までの範囲を生成します。
これらの範囲はfor
ループの反復やイテレータの作成など、さまざまな場面で使用されます。例えば、for
ループやイテレータを使って範囲内の要素を処理することができます。
- for-in-range.rs
fn main() { // 半開区間の使用例 for i in 1..5 { println!("{i}"); // 1, 2, 3, 4が出力される } // 閉区間の使用例 for i in 1..=5 { println!("{i}"); // 1, 2, 3, 4, 5が出力される } }
範囲は整数や文字など、多くの型で使用できます。範囲の使用はイテレーションや特定の範囲内の操作を行う際に便利です。
iter()
[編集]Rust
におけるiter()
は、コレクション(ベクター、配列、ハッシュマップなど)をイテレート可能な形に変換するメソッドです。イテレータは、コレクション内の要素を1つずつ処理するための仕組みを提供します。
基本的な使い方は以下のようになります:
- iter.rs
fn main() { let vec = vec![1, 2, 3, 4, 5]; // ベクターのイテレータを作成する let mut iter = vec.iter(); // イテレータを使って要素に順番にアクセスする while let Some(value) = iter.next() { println!("{value}"); } }
- 実行結果
1 2 3 4 5
iter()
メソッドは、イテレータを作成するための最も基本的な手段ですが、さまざまな応用もあります。
.map()
: イテレータを他の形に変換する場合に使われます。たとえば、各要素に対して関数を適用して新しいイテレータを作成します。fn main() { let vec = vec![1, 2, 3, 4, 5]; // 各要素を2倍する新しいイテレータを作成する let doubled_iter = vec.iter().map(|x| x * 2); for value in doubled_iter { println!("{value}"); } }
.filter()
: 条件に一致する要素のみを含む新しいイテレータを作成します。fn main() { let vec = vec![1, 2, 3, 4, 5]; // 偶数の要素だけを含む新しいイテレータを作成する let even_iter = vec.iter().filter(|&x| x % 2 == 0); for value in even_iter { println!("{value}"); } }
.fold()
: イテレータ内の要素を畳み込んで単一の値に集約します。fn main() { let vec = vec![1, 2, 3, 4, 5]; // 要素の合計を計算する let sum = vec.iter().fold(0, |acc, &x| acc + x); println!("合計: {sum}"); }
.reduce()
: イテレータ内の要素を畳み込んで単一の値に集約します。fn main() { let vec = vec![1, 2, 3, 4, 5]; // 要素の合計を計算する let sum = vec.into_iter().reduce(|acc, x| acc + x); println!("合計: {:?}", sum); }
これらはiter()
を基盤として利用する機能の一部です。Rust
のイテレータは非常に強力で、関数型プログラミングの概念を取り入れながら、効率的で安全なコードを記述するための重要な手段となっています。
rev()
[編集]iter()
メソッドに加えて、rev()
メソッドを使用すると、要素を逆順で取り出すこともできます。
- iter-rev.rs
fn main() { let v = vec![1, 3, 5, 7, 11]; for x in v.iter().rev() { println!("x = {}", x) } }
- 実行結果
x = 11 x = 7 x = 5 x = 3 x = 1
- このRustのコードは、
for
ループを使用して、ベクトルv
の要素を反復処理し、各要素の値を出力するものです。ただし、v
の要素を逆順に処理します。 v.iter().rev()
は、v
のイテレータを取得し、そのイテレータを逆順にするためのメソッドです。これにより、ベクトルの最後の要素から始まり、最初の要素まで逆順に反復処理します。for
ループの本体では、println!
マクロを使用して、現在のx
の値を表示しています。{}
はプレースホルダーであり、その場所に変数の値が挿入されます。この例では、{}
の中にx
を指定して、ループの反復ごとに変化するx
の値を表示しています。
enumerate()
[編集]enumerate()
メソッドを使用すると、要素のインデックスと値を同時に取り出すこともできます。
- コード例
let v = vec![1, 2, 3]; for (i, val) in v.iter().enumerate() { println!("{}: {}", i, val); }
- このコードでは、
v.iter().enumerate()
メソッドを使用して、v
のイテレータを作成し、各要素のインデックスと値を同時に反復処理しています。for
ループの本体では、変数i
を使ってインデックス、val
を使って値を表示しています。
zip()
[編集]zip()
メソッドを使用すると、複数のイテレータを同時に取り出すことができます。
- コード例
let v1 = vec![1, 2, 3]; let v2 = vec!["one", "two", "three"]; for (i, val) in v1.iter().zip(v2.iter()) { println!("{}: {}", i, val); }
- このコードは、
zip()
メソッドを使用して、2つのベクトルを同時に反復処理する方法を示しています。 - 最初の行で、整数のベクトル
v1
と文字列のベクトルv2
を定義しています。 - 次に、
for
ループを使用して、v1.iter()
とv2.iter()
のイテレータを同時に取り出します。このとき、(i, val)
というタプルの形式で、それぞれのイテレータから次の要素を取り出します。 println!
マクロの中で、{}
にi
とval
をそれぞれ表示しています。{}
の中にある:
は区切り文字で、i
とval
を区別するために使用されています。
while
[編集]Rustにおいて、while
は指定した条件式がtrue
である限り、ブロック内のコードを繰返し実行する制御構文です。
- while.rs
fn main() { let mut i = 0; while i < 5 { println!("i = {}", i); i += 1 }; }
- 実行結果
i = 0 i = 1 i = 2 i = 3 i = 4
- このプログラムは、0から4までの数字を順番に表示するプログラムです。whileループを使用して、条件式
i < 5
がtrue
である間、ループを継続します。ループの各イテレーションでは、println!
マクロを使用して、変数i
の現在の値を表示します。i
の値は、ループ本文の最後でi += 1
によって1ずつ増加します。条件式i < 5
がfalse
になったとき、ループが終了します。最終的に、0から4までの数字が順番に表示されます。
while let
[編集]Rustのwhile let
は、ループ処理の一種で、パターンマッチングを行い、パターンにマッチする値をループ内で取り出しながらループを繰り返します。
while let
は、match
構文の糖衣構文で、一つの値を取り出してパターンマッチングを行い、パターンにマッチする場合は値を取り出し、マッチしない場合はループを終了します。
- while-let.rs
fn main() { let mut n = Some(0); while let Some(i) = n { n = if i > 5 { None } else { println!("i = {}", i); Some(i + 1) } } }
- 実行結果
i = 0 i = 1 i = 2 i = 3 i = 4 i = 5
- このコードは、
n
というOption<i32>
型の変数を定義し、初期値をSome(0)
として設定します。そして、while
ループを使ってn
の値を取り出しながら、それがNone
でなければループを続けます。 while let
の条件式では、n
がSome(i)
とパターンマッチされ、i
に値がバインディングされます。このパターンマッチにより、n
がSome(i)
でなければループは終了します。- ループ本体では、
i
が5
を超える場合は、n
をNone
に更新してループを終了します。そうでなければ、i
を表示して、Some(i + 1)
をn
に代入してループを継続します。 - このプログラムの出力は、0 から 5 までの数値が順番に表示されます。最後に
None
が表示されます。
while let とベクトル
[編集]Rustのwhile let
は、反復可能な値に対して、パターンマッチングを行いながら反復処理を行うことも出来ます。ベクトルに対してwhile letを使うと、ベクトルの末尾から要素を取り出しながら反復処理を行うことができます。
- while-let-again.rs
fn main() { let mut v = vec![1, 3, 5, 7, 11]; while let Some(x) = v.pop() { println!("x = {}", x) } }
- 実行結果
x = 11 x = 7 x = 5 x = 3 x = 1
- このコードは、可変長配列
v
に値を追加し、whileループでその配列から値を取り出し、それを表示するものです。 - まず、可変長配列
v
にvec![1, 3, 5, 7, 11]
という値を設定しています。その後、while let
式を使って、v.pop()
の戻り値がSome(x)
である限り、ループが継続されます。v.pop()
は、v
の最後の要素を取り出し、その要素があればSome
で包んで返し、なければNone
を返します。 while let
式では、取り出した値がSome(x)
である場合に、println!("x = {}", x)
を実行してx
の値を表示します。- つまり、このコードは
v
の末尾から要素を取り出しながら、取り出した要素の値を表示するという処理を続け、最後に配列が空になったらループを終了します。
- pseudo-do-while.rs
fn main() { let mut i = 100; while { println!("{}", i); i += 1; i < 10 } {} }
- 実行結果
100
- whileの条件式が省略されたかのように見えますが、4,5,6行目を含むブロックが、while の条件式となり値はブロックの最後の6行目の式の値です。
- 7行目の {} がループ本体です。
- loopを使った例
fn main() { let mut i = 100; loop { println!("{}", i); i += 1; if !(i < 10) { break; } } }
- loop のブロックの最後に脱出条件を書いた方がわかりやすいかもしれません。
loop
[編集]loop
は、明示的な条件式の指定がない限り、無限に繰り返されます。
- loop-and-break.rs
fn main() { let mut i = 0; let result = loop { if i > 3 { break 100; } println!("i = {}", i); i += 1; }; println!("result = {}", result); }
- 実行結果
i = 0 i = 1 i = 2 i = 3 result = 100
- 最初に、変数
i
を0に初期化します。そして、loop
構文で、i
が3より大きくなるまで繰り返します。 - 各反復中に、
if
文が使用されています。if
の条件式は、i
が3より大きくなった場合にはbreak 100;
が実行され、loop
から脱出します。そうでない場合は、println!()
関数を使用して、i
の値を出力し、i
を1増やします。 - 最後に、
loop
から脱出した後にresult
変数に格納される値は、break
文の引数で指定された100
です。そして、println!()
関数を使用して、result
の値を出力します。 - つまり、このコードは、
i
を0から3まで順に増やしながら、各値を出力し、i
が3より大きくなったら100を返し、その後にresult
の値を出力するという処理を行っています。
continue
[編集]continue
を実行すると、ループのブロック内の残りの処理がスキップされ、反復処理が続行されます。
- continue.rs
fn main() { let mut i = 0; let result = loop { if i > 10 { break 100; } if i % 2 == 0 { i += 1; continue; } println!("i = {}", i); i += 1; }; println!("result = {}", result); }
- 実行結果
i = 1 i = 3 i = 5 i = 7 i = 9 result = 100
- このコードは、0から10までの奇数を出力するプログラムです。
- まず、
i
変数を定義して0で初期化し、result
変数を宣言しています。次に、loop
キーワードでループを開始します。 - ループ本体では、
if
文を使ってi
が10より大きくなったら、break
キーワードを使ってループを抜けます。これにより、11以上の奇数が出力されることはありません。 - 次に、
i
が偶数である場合は、continue
キーワードを使ってループの先頭に戻ります。これにより、偶数はスキップされます。 - 最後に、
println!
マクロを使って現在のi
の値を出力し、i
を1増やします。 - ループが終了したら、最後に
result
の値を出力します。break
キーワードが値を返すため、このループはresult
に100を設定します。
- 所有権と借用: Rustでは、各値には所有者があり、値のライフサイクルを追跡する必要があります。また、他のコードに値を渡すときは、その値を一時的に借りる必要があります。これらの概念が理解しづらい場合があります。
- ライフタイム: Rustでは、各値にはライフタイムがあり、その値が使用される期間を決定します。ライフタイムの概念が不明瞭になりがちで、特に複雑なデータ構造を扱う場合に問題が発生することがあります。
- パフォーマンスの最適化: Rustは高速な実行時パフォーマンスを提供しますが、それを達成するためには、手動でメモリ管理や最適化を行う必要があります。これらの最適化は、Rustの型システムと他の言語との違いを理解する必要があります。
- コンパイラエラー: Rustのコンパイラは非常に厳格で、コードがコンパイルできない場合があります。エラーメッセージは詳細で役に立ちますが、初めて見た場合は驚くかもしれません。
パターンマッチング
[編集]パターンマッチングの概要
[編集]パターンマッチングの基本概念
[編集]パターンマッチングは、プログラミング言語において特定のパターンとデータを照合し、条件に基づいた処理を行う手法です。この手法は、コードの可読性や柔軟性を高めるために広く使用されます。パターンマッチングの基本的な概念は、与えられたデータが特定のパターンに一致するかどうかを検査し、それに応じた処理を行います。
Rustにおけるパターンマッチングの役割と重要性
[編集]Rustでは、パターンマッチングは非常に重要な機能です。パターンマッチングは、異なる条件に基づいてコードをブロックに分割し、それぞれの条件に対して適切な処理を行うことができます。これにより、コードの複雑さが軽減され、可読性が向上します。また、Rustの型システムとの統合により、安全性やエラーハンドリングの向上も実現されます。
基本的なパターン
[編集]リテラルパターン
[編集]リテラルパターンは、値そのものとマッチさせるための最も基本的なパターンです。以下の例では、x
の値が1
の場合にマッチします。
let x = 1; match x { 1 => println!("one"), _ => println!("not one"), }
特定の値とのマッチング
[編集]リテラルパターンは、数値や文字列、真理値などの様々なリテラル値とマッチさせることができます。
let x = 'c'; match x { 'a' => println!("apple"), 'b' => println!("banana"), 'c' => println!("cherry"), // この行が実行される _ => println!("other"), }
変数パターン
[編集]変数パターンは、値を新しい変数にバインドするためのパターンです。以下の例では、x
の値が5
の場合にy
に5
がバインドされます。
let x = 5; match x { y => println!("x is {}", y), // この行が実行され、y = 5 }
値を変数にバインドするパターン
[編集]変数パターンは、値をパターン内の変数にバインドすることができます。これは、値を後で使用したり、条件分岐に利用したりするのに便利です。
let x = 10; match x { 0 => println!("x is zero"), y if y > 0 => println!("x is positive: {}", y), // この行が実行される y => println!("x is negative: {}", y), }
ワイルドカードパターン
[編集]ワイルドカードパターン(_
)は、任意の値とマッチします。これは、特定の値を無視したい場合や、残りのパターンを捕捉したい場合に便利です。
let x = 42; match x { 0 => println!("x is zero"), _ => println!("x is something else"), // この行が実行される }
任意の値とのマッチング
[編集]ワイルドカードパターンを使えば、任意の値とマッチさせることができます。これは、値を確認する必要がない場合や、デフォルトの処理を実行したい場合に役立ちます。
let x = 123; match x { 0 => println!("x is zero"), _ => println!("x is not zero"), // この行が実行される }
列挙型とパターンマッチング
[編集]列挙型の定義と使い方
[編集]Rustには、列挙型と呼ばれる特別な型があります。列挙型は、いくつかの列挙値のいずれかを取ることができる型です。以下の例では、Direction
という列挙型を定義しています。
enum Direction { Up, Down, Left, Right, }
列挙型の値を作成するには、列挙型名とコロン(:
)を使用します。
let up = Direction::Up; let down = Direction::Down;
列挙型に対するパターンマッチングの活用
[編集]列挙型とパターンマッチングを組み合わせると、非常に強力なコードを書くことができます。以下の例では、Direction
列挙型の値に対してパターンマッチングを行っています。
enum Direction { Up, Down, Left, Right, } fn get_direction_name(dir: Direction) -> &'static str { match dir { Direction::Up => "上", Direction::Down => "下", Direction::Left => "左", Direction::Right => "右", } } fn main() { let up = Direction::Up; let down = Direction::Down; println!("up: {}", get_direction_name(up)); // 上 println!("down: {}", get_direction_name(down)); // 下 }
この例では、get_direction_name
関数が列挙型Direction
の値に対してパターンマッチングを行い、対応する文字列を返しています。
構造体とタプルのパターンマッチング
[編集]構造体の定義と使い方
[編集]Rustでは、構造体を使ってデータを表現することができます。構造体は、フィールドと呼ばれる複数の値を持つことができます。以下の例では、Person
という構造体を定義しています。
struct Person { name: String, age: u32, }
構造体のインスタンスを作成するには、構造体名とフィールド値を指定します。
let person = Person { name: String::from("Alice"), age: 30, };
タプルの定義と使い方
[編集]タプルは、異なる型の値を含むことができる集合体です。タプルは括弧()
で囲んで定義します。
let tuple = (1, 3.14, "hello");
タプルの要素にアクセスするには、インデックスを使用します。
let x = tuple.0; // 1 let y = tuple.1; // 3.14 let z = tuple.2; // "hello"
構造体とタプルに対するパターンマッチングの活用
[編集]構造体やタプルに対してパターンマッチングを行うことができます。これは、データの構造に基づいて処理を行う場合に非常に便利です。
struct Person { name: String, age: u32, } fn print_person_info(person: Person) { match person { Person { name, age } => println!("名前: {}, 年齢: {}", name, age), } } fn main() { let alice = Person { name: String::from("Alice"), age: 30, }; print_person_info(alice); }
この例では、print_person_info
関数がPerson
構造体のインスタンスに対してパターンマッチングを行い、名前と年齢を出力しています。
タプルに対してもパターンマッチングを行うことができます。
fn print_tuple_info(tuple: (u32, f64, &str)) { match tuple { (x, y, z) => println!("x: {}, y: {}, z: {}", x, y, z), } } fn main() { let tuple = (42, 3.14, "hello"); print_tuple_info(tuple); }
この例では、print_tuple_info
関数がタプル(u32, f64, &str)
に対してパターンマッチングを行い、その要素を出力しています。
パターンガード
[編集]パターンガードの概要
[編集]パターンガードは、パターンマッチングに条件を追加するための機能です。パターンに一致する値に対して、追加の条件を指定することで、より柔軟な処理を行うことができます。
パターンガードの実装方法と使いどころ
[編集]パターンガードは、パターンに矢印(<code>=></code>)と条件式を追加することで実装します。条件式が真の場合のみ、パターンに一致したものとして処理されます。
let x = 10; match x { 2 | 5 | 10 if x % 2 == 0 => println!("x は偶数です"), _ => println!("x は奇数です"), }
この例では、x
が 2、5、10 のいずれかで、かつ偶数の場合は、"x は偶数です" と出力されます。
パターンガードは、様々な場面で活用することができます。
- 特定の条件を満たす値のみを処理したい場合
- エラー処理を行う場合
- 複雑なパターンを処理する場合
ネストしたパターン
[編集]ネストしたパターンの例
[編集]ネストしたパターンは、複数のパターンを組み合わせたパターンです。パターンガードと組み合わせることで、より複雑な条件を処理することができます。
enum Color { Red, Green, Blue, } struct Point { x: i32, y: i32, color: Color, } let point = Point { x: 10, y: 20, color: Color::Red }; match point { Point { x, y, color: Color::Red } => println!("赤い点が ({}, {}) にあります", x, y), Point { x, y, color } => println!("({}, {}) に {} 色の点がいます", x, y, color), }
この例では、point
が Point { x, y, color: Color::Red }
の形式の構造体である場合のみ、"赤い点が ({}, {}) にあります" と出力されます。point
が Point { x, y, color }
の形式の構造体である場合は、"({}, {}) に {} 色の点がいます" と出力され、color
には point
の color
フィールドの値が格納されます。
ネストしたパターンの利点と注意点
[編集]ネストしたパターンを使用すると、複雑な条件を処理しやすくなります。しかし、パターンが複雑になりすぎると、コードが読みづらくなる可能性があります。
エラーハンドリングとパターンマッチング
[編集]ResultやOptionとの組み合わせ
[編集]Result
や Option
などの型とパターンマッチングを組み合わせることで、エラーハンドリングを効率的に行うことができます。
let result = read_file("filename.txt"); match result { Ok(contents) => println!("ファイルの内容: {}", contents), Err(error) => println!("エラー: {}", error), }
この例では、read_file
関数が成功した場合のみ、ファイルの内容が出力されます。失敗した場合は、エラーメッセージが出力されます。
エラーハンドリングにおけるパターンマッチングの有用性
[編集]パターンマッチングを使用すると、エラー処理をより簡潔かつ分かりやすく記述することができます。また、エラーの種類ごとに異なる処理を行うこともできます。
高度なパターンマッチング
[編集]パターンマッチングにおける複雑なパターンの扱い方
[編集]パターンマッチングは、複雑なパターンを処理するために様々な機能を提供しています。
- パターンガード
- パターンに一致する値に対して、追加の条件を指定することができます。
- ネストしたパターン
- 複数のパターンを組み合わせたパターンを作成することができます。
- パターン分解
- パターンに一致した値を複数の変数に格納することができます。
- ガード付きパターン
- パターンに一致する値に対して、条件分岐を実行することができます。
これらの機能を組み合わせることで、複雑なデータ構造を効率的に処理することができます。
複数のパターンに一致する場合の処理方法
[編集]複数のパターンに一致する場合は、パターンガードやネストしたパターンを使用して、どのパターンに一致するかを判別する必要があります。
マクロ
[編集]Rustのマクロは、macro_rules!
を使ったマクロとproc_macro
を使ったプロシージャマクロの2種類があります。macro_rules!
を使ったマクロはパターンマッチングを用いて簡易的なマクロを定義します。一方、proc_macro
を使ったプロシージャマクロは、Rustのコードを受け取り、変換したり、新しいコードを生成するためのより柔軟なマクロです。
macro_rules!
を使ったマクロ
[編集]macro_rules!
は、パターンに基づいてマッチングし、そのパターンに一致した場合に指定されたコードを生成する簡易なマクロを定義します。
例えば、vec!
マクロは、可変長のベクタを生成するマクロです。これはmacro_rules!
を使って次のように実装されています。
macro_rules! vec { // パターンマッチで要素を取得して新しいベクタを生成 ( $( $x:expr ),* ) => { { let mut temp_vec = Vec::new(); $(temp_vec.push($x);)* temp_vec } }; } fn main() { // `vec!`マクロを使ってベクタを生成 let my_vec = vec![1, 2, 3, 4]; // 生成されたベクタを表示 println!("{:?}", my_vec); }
これは、vec![1, 2, 3]
を使うと、[1, 2, 3]
というベクタを生成します。このマクロは、$( $x:expr ),*
のパターンに一致して、指定された式($x
)をベクタに挿入するコードを生成します。
proc_macro
を使ったプロシージャマクロ
[編集]macro_rules!
を使わずにマクロを定義する方法もあります。これは、プロシージャマクロ(proc_macro
)を使用した方法です。proc_macro
は、macro
キーワードによって定義される関数の一種で、Rustのコードを受け取り、そのコードを操作して変換することができます。
例えば、vec!
マクロをmacro_rules!
ではなく、プロシージャマクロとして定義する場合は、proc_macro
を使います。
use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, Expr, parse_quote}; #[proc_macro] pub fn my_vec(input: TokenStream) -> TokenStream { // 入力をパース let input_expr: Expr = parse_macro_input!(input); // 入力を取得して新しいベクタを生成するコードを生成 let expanded = quote! { { let mut temp_vec = Vec::new(); temp_vec.push(#input_expr); temp_vec } }; // TokenStreamに変換して返す TokenStream::from(expanded) }
この例では、proc_macro
としてmy_vec
という新しいマクロを定義しています。my_vec
は、proc_macro
の関数として定義され、Rustのコードを受け取り、それを操作して新しいコードを生成します。
このプロシージャマクロを使うと、次のようにマクロを呼び出すことができます。
fn main() { let my_vec = my_vec!(42); println!("{:?}", my_vec); // [42] }
この例では、my_vec!
マクロを使用して、引数として42
を渡しています。このマクロは、引数を含むベクタを生成するもので、my_vec!(42)
は[42]
というベクタを生成します。
この方法では、proc_macro
を使用して、macro_rules!
を使わずに独自のマクロを定義できます。ただし、この方法ではproc_macro
と関連するライブラリ(syn
、quote
など)を使用する必要があります。
マクロ関数
[編集]マクロ関数は、macro_rules!
マクロを使って定義されます。
これは、マクロのパターンとそれに対する置換を定義するマクロです。
マクロの呼び出し元は、パターンにマッチする式を渡し、置換が適用されたコードが生成されます。
- マクロ関数の例
macro_rules! say_hello { () => { println!("Hello, world!"); }; } fn main() { say_hello!(); }
- 実行結果
Hello, world!
- 解説
- 上記の例では、
say_hello!()
マクロを定義しています。これは、空の引数リストに対してprintln!("Hello, world!")
を生成するマクロです。
マクロ属性
[編集]マクロ属性はRustのコンパイル時にコードを修飾するために使用されます。 通常は、関数や構造体、列挙型、フィールドなどに適用されます。 マクロ属性を使用することで、コンパイル時に生成されるコードに追加の情報を提供できます。
- マクロ属性の例
#[derive(Debug)] struct MyStruct { my_field: i32, } fn main() { let my_struct = MyStruct { my_field: 42 }; println!("{:?}", my_struct); }
- 実行結果
MyStruct { my_field: 42 }
- 解説
- 上記の例では、
#[derive(Debug)]
マクロ属性を使用しています。これは、MyStruct
にDebug
トレイトを実装するために必要なコードを生成するマクロ属性です。println!
マクロでmy_struct
を表示する際に、Debug
トレイトのメソッドを呼び出して情報を表示することができます。
クレートとは
[編集]- Rustのパッケージ単位: Rustでは、コードのパッケージングや再利用のために「クレート」(Crate)という単位が使われます。1つのクレートには、1つ以上の関連するモジュールやデータ型が含まれます。
- Cargoによる管理: クレートは、RustのパッケージマネージャであるCargoによって管理されます。Cargoは、クレートの作成、ビルド、依存関係の解決などを自動化するツールです。
クレートの構造
[編集]典型的なRustのクレートは、次のような構造を持ちます:
my_crate/ ├── src/ │ ├── main.rs │ ├── lib.rs │ └── other_module.rs ├── Cargo.toml └── README.md
- src/: クレートのソースコードを含むディレクトリ。
- src/main.rs: クレートを実行するためのエントリポイント。
- src/lib.rs: ライブラリとしてコンパイルされる場合に使用されるエントリポイント。
- src/other_module.rs: 他のモジュール。
- Cargo.toml: クレートのメタデータ、依存関係、ビルド構成などを含むファイル。
- README.md: クレートのドキュメント。
クレートの作成
[編集]新しいクレートを作成する手順は以下の通りです:
cargo new
コマンドを使用して新しいクレートのディレクトリを作成します。Cargo.toml
ファイルを編集して、クレートのメタデータを定義します。- 必要に応じて、
src/main.rs
や他のモジュールを編集して、クレートのコードを実装します。 cargo run
コマンドを使用してクレートをビルドして実行します。
クレートの公開
[編集]クレートを他の開発者と共有するためには、それをcrates.ioという公式のクレートレジストリに公開する必要があります。公開手順は以下の通りです:
- crates.ioにアカウントを作成します。
cargo login
コマンドを使用してcrates.ioにログインします。- クレートのバージョン番号を更新します。
cargo publish
コマンドを使用してクレートを公開します。
まとめ
[編集]Rustのクレートは、コードのパッケージングや再利用を容易にする重要な概念です。Cargoを使用してクレートを管理し、必要に応じてcrates.ioに公開することで、他の開発者とクレートを共有することができます。 Rustにおいて、パッケージはクレートと呼ばれます。クレートは、Rustのコードやライブラリをパッケージ化して共有できるようにするための仕組みです。クレートは、依存関係を解決するためのメタデータと、コードとその他のファイルから構成されています。
Rustには、crates.io
という公式のクレートレジストリがあり、開発者はそこでクレートを共有したり、他の開発者が作成したクレートを使用したりできます。
代数的データ型
[編集]Rustにおける代数的データ型は、構造体(struct
)と列挙型(enum
)を指します。これらは複雑なデータ構造を表現するのに役立ちます。
構造体(struct)
[編集]構造体は異なる型のフィールドを持つことができ、それぞれのフィールドは名前を持ちます。例えば:
struct Point { x: i32, y: i32, } impl Point { fn new(x: i32, y: i32) -> Self { Self { x, y } } fn print(&self) { println!("x: {}, y: {}", self.x, self.y); } } fn main() { let origin = Point::new(0, 0); origin.print(); }
列挙型(enum)
[編集]列挙型は、いくつかの異なるバリアント(variant)の中から選択することができます。それぞれのバリアントはデータを持つことができます。
enum Shape { Circle(f64), // 半径を持つ Rectangle(f64, f64), // 幅と高さを持つ Square(f64), // 1辺の長さを持つ } impl Shape { fn area(&self) -> f64 { match self { Shape::Circle(radius) => std::f64::consts::PI * radius * radius, Shape::Rectangle(width, height) => width * height, Shape::Square(side) => side * side, } } } fn main() { let shapes = vec![ Shape::Circle(5.0), Shape::Rectangle(10.0, 20.0), Shape::Square(15.0), ]; for shape in &shapes { println!("Area: {}", shape.area()); } }
これらの代数的データ型は、Rustで柔軟なデータ構造を表現する際に役立ちます。
属性(Attribute)
[編集]Rustの属性(Attribute)は、コンパイラに対してコードに対する追加情報や指示を提供するための注釈です。コードに直接書かれる特殊な構文であり、#[...]
の形式で記述されます。これらの属性は、コンパイラやコードの振る舞い、コード生成、最適化、データのレイアウトなどを変更したり、制御したりするために使われます。
以下は、よく使用される属性のいくつかとその目的です:
#[derive(...)]
: 自動導出(Derive Attribute)は、コンパイラに対して構造体や列挙型が特定のトレイトを自動的に実装するよう指示するために使われます。#[derive(Debug)]
などがその例で、Debug
トレイトを自動的に実装するよう指示します。#[cfg(...)]
: コンパイル時の条件を指定します。例えば、#[cfg(target_os = "linux")]
はLinux環境のみで有効にするために使われます。#[allow(...)]
/#[deny(...)]
/#[warn(...)]
: コンパイラの警告レベルを変更します。allow
は特定の警告を許可し、deny
はエラーにするよう指示し、warn
は警告として表示します。#[repr(...)]
: データのレイアウトや表現方法を制御します。例えば、#[repr(C)]
はC言語のレイアウトに合わせることを示し、#[repr(align(N))]
は特定のアライメントを指定します。#[inline]
/#[noinline]
: インライン展開を制御します。inline
は関数をインライン展開可能として指定し、noinline
は展開を禁止します。#[test]
/#[bench]
: ユニットテストやベンチマークテスト用の関数をマークします。テストランナーがこれらの関数を見つけて実行します。#[derive(...)]
、#[macro_use]
、#[crate_name = "some_name"]
、**#[feature(...)]
**など、他にも多くの属性が存在します。
属性は様々な目的に使用され、Rustの柔軟性やコードの特定の振る舞いを制御するための重要なツールです。それぞれの属性は特定のコンテキストで役割を果たし、コードの意図をコンパイラに伝えるのに役立ちます。
// テスト用の関数を定義し、テスト用の属性を追加 #[test] fn test_addition() { assert_eq!(2 + 2, 4); } // main関数を含むコード例 fn main() { println!("Hello, Rust!"); // インライン展開のための属性を持つ関数 #[inline(always)] fn add(a: i32, b: i32) -> i32 { a + b } // インライン展開を促進する属性を持つ関数 #[inline(always)] fn multiply(a: i32, b: i32) -> i32 { a * b } // インライン展開される関数の呼び出し let result_add = add(3, 5); println!("Addition result: {}", result_add); // インライン展開される関数の呼び出し let result_multiply = multiply(4, 6); println!("Multiplication result: {}", result_multiply); }
#[test]
属性:test_addition
関数に付与された#[test]
属性は、この関数をテスト用の関数としてマークしています。この関数は、assert_eq!(2 + 2, 4);
を使って2 + 2が4と等しいかテストします。#[inline(always)]
属性:add
関数とmultiply
関数に付与された#[inline(always)]
属性は、インライン展開を促進するための指示です。インライン展開は、関数呼び出しのオーバーヘッドを削減し、効率的なコード生成を促進します。しかし、always
の指定は、常にインライン展開されるというわけではなく、コンパイラの判断に依存します。main
関数:main
関数はプログラムのエントリーポイントです。この関数内でprintln!
を使って"Hello, Rust!"を表示し、add
関数とmultiply
関数を呼び出しています。それぞれの関数では、インライン展開の指示が付与されています。
このコード例は、属性を使って関数をテスト対象としてマークし、またインライン展開を促進する方法を示しています。ただし、実際にコードを実行する場合は、main
関数内のコードが実行されるため、test_addition
関数はテストコードとして実行されることになります。
Debug
[編集]Debug
トレイトは、println!
マクロで構造体や列挙型の内容を表示するために使用されます。以下は、Debug
トレイトを実装することでprintln!
で構造体の内容を表示する例です。
// Debugトレイトを導出可能にするためにderiveアトリビュートを使用 #[derive(Debug)] struct MyStruct { name: String, age: u32, } fn main() { let my_data = MyStruct { name: String::from("Alice"), age: 30, }; // 構造体の内容を表示する println!("My data: {:?}", my_data); }
- 実行結果
My data: MyStruct { name: "Alice", age: 30 }
この例では、MyStruct
構造体にDebug
トレイトを#[derive(Debug)]
で導出しています。そして、main
関数内でmy_data
をprintln!
で表示する際に、{:?}
フォーマットを使ってDebug
トレイトを呼び出しています。
Debug
トレイトを導出することで、println!
で構造体の内容を簡単に表示することができます。#[derive(Debug)]
を使うことで、デバッグ情報を出力するためのメソッドの実装を手動で書く必要がなくなります。
ジェネリックス
[編集]Rustにおけるジェネリクスは、特定の型に依存せず、複数の型で動作するコードを作成するための重要な機能です。ジェネリクスを使用することで、同じコードを複数の型で再利用したり、型安全性を保ちながら柔軟性を持たせたりすることができます。
基本的なジェネリクスの使用
[編集]// Tというジェネリックな型を持つ関数 fn print_value<T: std::fmt::Display>(value: T) { println!("Value is: {}", value); } fn main() { // 使用例 print_value(10); // Tはi32として推論される print_value(2.73 as f32); // Tはf32として推論される print_value("Hello"); // Tは&strとして推論される }
- この例では、
print_value
関数がジェネリックな型T
を持ち、T
はstd::fmt::Display
トレイトを実装している型に制限されています。std::fmt::Display
トレイトは、{}
でフォーマット可能な型を表します。 - 引数
value
の型はコンパイル時に推論されます。
ジェネリックな構造体
[編集]// ジェネリックな構造体 struct Pair<T, U> { first: T, second: U, } fn main() { // 使用例 let pair_of_int_and_str = Pair { first: 10, second: "Hello" }; println!("first = {:?}, second = {:?}", pair_of_int_and_str.first, pair_of_int_and_str.second); }
Pair
構造体は2つの異なる型を持つことができます。使用する際に具体的な型を指定することで、ジェネリックな構造体を作成できます。
ジェネリックなトレイト
[編集]// ジェネリックなトレイト trait Printable { fn print(&self); } // TがPrintableトレイトを実装していることを要求する関数 fn print_trait<T: Printable>(item: T) { item.print(); } // 使用例 struct MyType; impl Printable for MyType { fn print(&self) { println!("Printing MyType"); } } fn main() { let obj = MyType; print_trait(obj); // Printableトレイトを実装したMyTypeのインスタンスを受け取る }
- ここでは、
Printable
というジェネリックなトレイトを定義し、print_trait
関数でPrintable
トレイトを実装した型T
を受け取る方法を示しています。
ジェネリクスは、関数、構造体、列挙型、トレイトなどのRustのさまざまな要素で使用できます。これにより、柔軟性のあるコードを作成し、再利用性を高めることができます。
型制約
[編集]ジェネリックスにおいては、型制約(type constraint)は、ジェネリックな型パラメータに対して特定の条件やトレイトの制約を課すことを指します。これにより、ジェネリックな型が特定の性質を持つことを保証し、安全性を確保することができます。
Rustでは、trait
を使用して型制約を実装します。例えば、std::fmt::Display
トレイトを持つ型に制約を課したい場合、以下のように実装します。
fn print_value<T: std::fmt::Display>(value: T) { println!("Value is: {}", value); }
ここでは<T: std::fmt::Display>
という構文を使用して、T
がstd::fmt::Display
トレイトを実装している必要があることを宣言しています。この制約により、print_value
関数はstd::fmt::Display
トレイトを実装した型に対してのみ呼び出しが可能になります。
また、複数のトレイト制約を持つこともできます。例えば、std::fmt::Debug
とstd::fmt::Display
トレイトの両方を実装した型を要求する場合は、次のように書きます。
fn print_value<T: std::fmt::Debug + std::fmt::Display>(value: T) { println!("Value is: {:?}", value); } #[derive(Debug)] struct Point<T> { x: T, y: T, } // Point<T>型に対するDisplayトレイトの実装 impl<T: std::fmt::Display> std::fmt::Display for Point<T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Point({}, {})", self.x, self.y) } } fn main() { let int_point = Point { x: 5, y: 10 }; print_value(int_point); // `Point`型は`std::fmt::Debug`と`std::fmt::Display`を実装している let float_point = Point { x: 1.5, y: 3.2 }; print_value(float_point); // 同様に、`Point`型は`std::fmt::Debug`と`std::fmt::Display`を実装している }
T
がstd::fmt::Debug
とstd::fmt::Display
の両方を実装している必要があります。
これにより、ジェネリックなコードをより安全に、かつ特定の条件下で使用できるように制約を課すことができます。
例外処理
[編集]Rustでは、Result
型とpanic
による例外処理が一般的です。Result
型は成功または失敗を表す列挙型で、Ok
は成功時の値、Err
はエラー時の情報を持ちます。
まず、Result
型を使用した例を見てみましょう:
use std::fs::File; use std::io::{self, Read}; fn read_file_contents(file_path: &str) -> Result<String, io::Error> { let mut file = File::open(file_path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents) } fn main() { let file_path = "/etc/hosts"; match read_file_contents(file_path) { Ok(contents) => println!("File contents: {}", contents), Err(e) => println!("Error reading file: {:?}", e), } }
この例では、read_file_contents
関数がResult<String, io::Error>
を返します。これは、成功時には文字列をOk
で、エラー時にはio::Error
をErr
で返すことを示しています。ファイルを開いたり、読み込んだりするメソッドの呼び出しには?
演算子を使用し、エラーが発生した場合は早期リターンしてエラーを返します。
match
ブロックでは、read_file_contents
の戻り値に対してOk
とErr
の両方の可能性に対処し、それぞれ成功時の振る舞いとエラー時の振る舞いを定義しています。これにより、関数の呼び出し元で適切にエラーハンドリングを行うことができます。
except()
[編集]また、Rustにはpanic!
マクロを使用してプログラムを異常終了させる方法もあります。これは致命的なエラーが発生した場合に使用されますが、エラーを適切に処理するためにResult
型を使うことが推奨されます。
fn main() { let v = vec![1, 2, 3]; let index = 5; let value = v.get(index).expect("Failed to get the value at index"); println!("Value at index {}: {}", index, value); }
この例では、get
メソッドはOption
型を返し、expect
メソッドはSome
の場合は中の値を返し、None
の場合は指定したメッセージとともにプログラムをpanic
させます。
ただし、expect
を使う際には、panic
が発生した際に表示されるメッセージは注意深く選ぶ必要があります。
panic!()
[編集]panic!()
は通常、予期せぬ状況やプログラムの継続が不可能な状況で使われることがあります。以下はそのような例です。
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("Attempted to divide by zero!"); // 0で割ろうとした場合、致命的なエラーとしてpanic!()を呼ぶ } a / b // 正常な割り算を行う } fn main() { let dividend = 10; let divisor = 0; let result = divide(dividend, divisor); // divide関数を呼び出す println!("Result of division: {}", result); }
この例では、divide()
関数でb
が0の場合にpanic!()
が呼ばれています。0で割ることは数学的に定義されていないため、これは致命的なエラーであり、プログラムの実行を中断させます。
panic!()
は、このような状況に直面した場合に、プログラムを停止させるための手段として使われます。しかし、このようなエラーは通常、if
文やmatch
文などの条件分岐を使用して事前にチェックして、エラーハンドリングを行うことが推奨されます。
Result
を使ってエラーハンドリングを行うことで、panic!()
に頼らずにエラーを適切に処理できます。
fn divide(a: i32, b: i32) -> Result<i32, &'static str> { if b == 0 { Err("Attempted to divide by zero!") // 0で割ろうとした場合、エラーをResultで返す } else { Ok(a / b) // 正常な割り算の結果をOkで返す } } fn main() { let dividend = 10; let divisor = 0; match divide(dividend, divisor) { Ok(result) => println!("Result of division: {}", result), Err(e) => println!("Error: {}", e), // エラーを適切に処理する } }
divide()
関数はResult<i32, &'static str>
を返し、0で割るエラーの場合にはErr
を、正常な計算の場合にはOk
を返します。match
ブロックでは、Ok
とErr
の両方のケースを処理し、エラーが発生した場合にはエラーメッセージを表示します。
このようにResult
型を使用することで、エラーハンドリングを柔軟かつ安全に行うことができます。panic!()
に頼るよりも、エラーを予測し、適切に処理する方が望ましいです。
Option型
[編集]Option
型は、何らかの値が存在するかどうかを表現するRustの列挙型です。Option
型はSome
とNone
の2つのバリアントを持ち、Some
は値が存在することを示し、None
は値が存在しないことを示します。
Option
型は、特定の操作が値を返さない可能性がある場合や、値が存在しない場合にエラーを返す代わりにNone
を返すために使われます。このような状況では、Result
型を使用せずに、Option
型が利用されることがあります。
以下はOption
型の例です:
fn divide(a: i32, b: i32) -> Option<i32> { if b == 0 { None // 0で割ろうとした場合、Noneを返す } else { Some(a / b) // 正常な割り算の結果をSomeで返す } } fn main() { let dividend = 10; let divisor = 0; match divide(dividend, divisor) { Some(result) => println!("Result of division: {}", result), // Someの場合は値を表示 None => println!("Error: Division by zero!"), // Noneの場合はエラーメッセージを表示 } }
この例では、divide()
関数はOption<i32>
を返し、0で割るエラーの場合にはNone
を、正常な計算の場合にはSome
を返します。match
ブロックでは、Some
とNone
の両方のケースを処理し、None
の場合はエラーメッセージを表示します。
Option
型は、特に値が存在しないことが普通に起こり得る場面で、エラー処理や結果の取り扱いを行う際に有用です。例外的な状況ではなく、むしろ普通の操作の一部として考えられる「値の有無」を扱う際に利用されることが多いです。
イテレーター
[編集]RustのIterator
トレイトは、コレクションやデータのシーケンスを反復処理するための非常に強力な機能です。Iterator
は、next()
メソッドを提供し、それを使用して次の要素を返し、シーケンスの終わりに達した場合は None
を返します。
基本的な使い方は次のようになります:
- イテレータの作成: コレクションやデータからイテレータを作成します。
iter()
やinto_iter()
、iter_mut()
などのメソッドを使用して、それぞれイミュータブルな参照、所有権、ミュータブルな参照を使ったイテレータを取得できます。let numbers = vec![1, 2, 3, 4, 5]; let mut iter = numbers.iter(); // イミュータブルなイテレータ
next()
を使用した反復処理:next()
メソッドを使って、イテレータから次の要素を取得します。match iter.next() { Some(number) => println!("Number: {}", number), None => println!("End of sequence"), }
- forループを使用した反復処理: 一般的には、
for
ループを使用してイテレータを処理します。for number in numbers.iter() { println!("Number: {}", number); }
Iteratorトレイト
[編集]Iterator
トレイトは、map()
、filter()
、fold()
などの便利なメソッドも提供しており、これらを組み合わせることでデータを効果的に処理できます。
let numbers = vec![1, 2, 3, 4, 5]; // 各要素を2倍して新しいベクターを作成する let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect(); // 偶数のみをフィルタリングする let evens: Vec<i32> = numbers.into_iter().filter(|&x| x % 2 == 0).collect(); // 全要素を合計する let sum: i32 = numbers.iter().sum();
Iterator
トレイトは、Rustでのデータ処理を非常に柔軟で効率的にします。関数型プログラミングの考え方に基づいた強力な機能を提供しています。
独自イテレータの実装
[編集]Iterator
トレイトを使用して独自のイテレータを実装する例を示します。
例えば、0から始まり、指定されたステップごとに増加するカウンターを作成するイテレータを実装してみましょう。
struct Counter { current: u32, step: u32, max: Option<u32>, } impl Counter { fn new(start: u32, step: u32, max: Option<u32>) -> Counter { Counter { current: start, step, max, } } } impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { match self.max { Some(max) if self.current >= max => None, _ => { let result = self.current; self.current += self.step; Some(result) } } } } fn main() { let counter = Counter::new(0, 2, Some(10)); for num in counter { println!("{}", num); } }
この例では、Counter
構造体はIterator
トレイトを実装しています。next()
メソッドは、現在の値を返し、カウンターを指定されたステップで増分させます。また、max
値が設定されており、その値に達するとイテレータは終了します。
main()
関数内では、Counter
イテレータを使用して0から10まで2ずつ増加する数列を生成しています。
Iterator
トレイトを実装することで、独自のイテレータを作成し、柔軟な反復処理を行うことができます。
impl
[編集]impl
キーワードは、Rustで特定の型に対してメソッドやトレイトを実装するために使用されます。基礎から始めて、応用的な使い方まで見ていきましょう。
基礎的な使用例
[編集]- メソッドの実装
struct MyStruct { value: i32, } impl MyStruct { // MyStruct型に対するメソッドの実装 fn new(value: i32) -> MyStruct { MyStruct { value } } fn get_value(&self) -> i32 { self.value } } fn main() { let instance = MyStruct::new(42); println!("Value: {}", instance.get_value()); // メソッド呼び出し }
impl
ブロック内で、MyStruct
に対するnew
メソッドとget_value
メソッドを実装しています。main
関数でこれらのメソッドを使用しています。
応用的な使用例
[編集]- トレイトの実装
// 'Printable' という名前の機能を定義しています。 // これは、ある型が特定の振る舞いを持つことを宣言します。 trait Printable { // 'print' という名前の機能を提供しています。 // これは、任意の型が実装する必要があるメソッドです。 fn print(&self); } // 'MyType' という名前のデータ構造を定義しています。 // これは整数値を保持する構造体です。 struct MyType { value: i32, // 'MyType' 構造体が保持する整数値のフィールドです。 } // 'Printable' という機能を 'MyType' に実装しています。 // これにより、'MyType' インスタンスは 'Printable' で提供される振る舞いを持ちます。 impl Printable for MyType { // 'Printable' で要求された 'print' 機能を 'MyType' に実装しています。 fn print(&self) { // 'MyType' インスタンスが 'print' を実行すると、値が表示されます。 println!("Value: {}", self.value); } } fn main() { // 'MyType' の新しいインスタンスを作成し、整数値を設定しています。 let obj = MyType { value: 10 }; // 'Printable' で定義された 'print' 機能を呼び出しています。 // これにより、 'MyType' の値が出力されます(この場合は "Value: 10")。 obj.print(); }
- インターフェース
- 共通の振る舞い: インターフェースは、異なるクラスが共通の振る舞いを持つことを保証します。他の言語では、この振る舞いはインターフェースで定義され、クラスはそれを実装します。
- メソッドの宣言: インターフェースでは、クラスが実装しなければならないメソッドの宣言が含まれます。
- 多重継承の代替: インターフェースは多重継承の代替手段としても使われ、クラスは複数のインターフェースを実装することができます。
- プロトコル
- 抽象的な振る舞いの定義: プロトコルは、特定の振る舞いや機能を表す抽象的な規約です。Swiftなどの言語ではプロトコルが使われ、クラスや構造体はそれらを適合させます。
- メソッドの要求: プロトコルは、適合する型によって実装されるメソッドやプロパティの要求を定義します。
- 型の適合性の強化: プロトコルに適合することで、様々な型を同じ抽象的な概念に束縛することができます。
- Rustのトレイト
- 共通の振る舞いの提供: トレイトは、構造体や他の型に共通の振る舞いを提供します。それぞれの型はトレイトを実装することで、その振る舞いを持つことができます。
- メソッドの宣言: トレイトではメソッドの宣言が行われ、それを実装することでトレイトが利用できるようになります。
- 型間の相互運用性と柔軟性: トレイトは型間での相互運用性や柔軟性を提供し、異なる型が同じ振る舞いを共有することができます。
ジェネリックな実装
[編集]- ジェネリックな構造体に対する実装
// ジェネリックなデータ構造体 'MyGeneric' を定義しています。 // '<T>' は型パラメータであり、任意の型を受け入れます。 struct MyGeneric<T> { value: T, // 'T' 型の値を保持する 'value' フィールドです。 } // 'MyGeneric<T>' に対するジェネリックなメソッド実装です。 impl<T> MyGeneric<T> { // 新しい 'MyGeneric<T>' インスタンスを生成する 'new' メソッドです。 // 渡された値で 'value' フィールドを初期化します。 fn new(value: T) -> MyGeneric<T> { MyGeneric { value } } // 'value' フィールドへの参照を返す 'get_value' メソッドです。 fn get_value(&self) -> &T { &self.value } } fn main() { // 'MyGeneric<i32>' 型のインスタンスを作成しています。 // 'value' フィールドには整数値 42 が格納されます。 let instance = MyGeneric::new(42); // 'get_value' メソッドを呼び出して 'value' フィールドの値を取得し、出力しています。 println!("Value: {:?}", instance.get_value()); // ジェネリックなメソッド呼び出し // 'MyGeneric<&str>' 型のインスタンスを作成しています。 // 'value' フィールドには文字列 "hello" が格納されます。 let instance = MyGeneric::new("hello"); // 'get_value' メソッドを呼び出して 'value' フィールドの値を取得し、出力しています。 println!("Value: {:?}", instance.get_value()); // ジェネリックなメソッド呼び出し }
このコードは、MyGeneric
をi32
型と&str
型の両方でインスタンス化しています。ジェネリックなデータ構造体とそのジェネリックなメソッドを利用して、異なる型に対して同じメソッドを使用する様子を示しています。println!
マクロ内の{:?}
は、Debug
トレイトを実装する型の値を表示するためのフォーマット指定子です。これにより、異なる型の値も出力できます。
impl
を使ったさまざまな例を挙げてみましょう。
- 単純なトレイトの実装
// Summaryトレイトを定義します。 trait Summary { fn summarize(&self) -> String; } // Summaryトレイトを実装するBook構造体を定義します。 struct Book { title: String, author: String, } // BookにSummaryトレイトを実装します。 impl Summary for Book { fn summarize(&self) -> String { format!("{} by {}", self.title, self.author) } } // summarize_any関数はSummaryトレイトを実装した任意の型に対して要約を取得します。 fn summarize_any(item: &impl Summary) -> String { item.summarize() } fn main() { let book = Book { title: String::from("Harry Potter"), author: String::from("J.K. Rowling"), }; println!("Summary: {}", summarize_any(&book)); }
- この例では、
Summary
トレイトを実装した様々な型に対して共通のsummarize_any
関数を使用して、異なる型の値に対して要約を取得できます。このようなジェネリックで柔軟なアプローチは、異なる型に対して一貫した操作を提供する場合に非常に便利です。
- 多様なトレイトの実装
// 長方形を表す 'Rectangle' 構造体です。 struct Rectangle { width: u32, // 長方形の幅を表す 'width' フィールド height: u32, // 長方形の高さを表す 'height' フィールド } // 'Rectangle' 構造体に関するメソッドを実装しています。 impl Rectangle { // 新しい 'Rectangle' インスタンスを生成する 'new' メソッドです。 fn new(width: u32, height: u32) -> Self { Rectangle { width, height } } } // 'Area' という名前のトレイトを定義しています。 // このトレイトは 'area' メソッドを持ち、長方形の面積を計算します。 trait Area { // 長方形の面積を計算する 'area' メソッドです。 fn area(&self) -> u32; } // 'Area' トレイトを 'Rectangle' 構造体に実装しています。 // 'Rectangle' には 'Area' トレイトの 'area' メソッドが実装されます。 impl Area for Rectangle { // 'Rectangle' の面積を計算する 'area' メソッドの実装です。 fn area(&self) -> u32 { self.width * self.height } } fn main() { // 幅が5、高さが10の 'Rectangle' インスタンスを作成しています。 let rect = Rectangle::new(5, 10); // 'Area' トレイトの 'area' メソッドを呼び出して、長方形の面積を出力しています。 println!("Area of rectangle: {}", rect.area()); }
- ここでは、
Rectangle
構造体にnew
メソッドを実装し、またArea
トレイトを実装しています。main
関数でRectangle
のインスタンスを作成し、Area
トレイトのメソッドであるarea()
を呼び出しています。
- ジェネリックなメソッド
// 同じ型の2つの要素を持つ 'Pair' 構造体です。 struct Pair<T> { first: T, // ペアの最初の要素を表す 'first' フィールド second: T, // ペアの2番目の要素を表す 'second' フィールド } // 'Pair<T>' 構造体に関するメソッドを実装しています。 impl<T> Pair<T> { // 新しい 'Pair<T>' インスタンスを生成する 'new' メソッドです。 // 渡された2つの値で 'first' と 'second' フィールドを初期化します。 fn new(first: T, second: T) -> Self { Pair { first, second } } // 'first' フィールドへの参照を返す 'get_first' メソッドです。 fn get_first(&self) -> &T { &self.first } // 'second' フィールドへの参照を返す 'get_second' メソッドです。 fn get_second(&self) -> &T { &self.second } } fn main() { // 整数型を持つ 'Pair' インスタンスを作成しています。 let pair = Pair::new(42, 24); // 'get_first' メソッドを使ってペアの最初の要素を出力しています。 println!("First element: {}", pair.get_first()); // 'get_second' メソッドを使ってペアの2番目の要素を出力しています。 println!("Second element: {}", pair.get_second()); }
Pair
構造体に対してT
型のジェネリックなメソッドを実装しています。この例ではget_first
メソッドを定義し、main
関数でそれを呼び出しています。
- トレイトを実装した列挙型
// 'Weather' 列挙型です。3つの異なる天気を表します。 #[derive(Debug)] enum Weather { Sunny, Cloudy, Rainy, } // 'Description' トレイトは、様々な要素に対する説明を提供します。 trait Description { fn describe(&self) -> &'static str; } // 'Description' トレイトを 'Weather' 列挙型に実装しています。 impl Description for Weather { // 'describe' メソッドは、各 'Weather' の状態に対する説明を提供します。 fn describe(&self) -> &'static str { match self { Weather::Sunny => "It's sunny today!", Weather::Cloudy => "It's cloudy today.", Weather::Rainy => "It's raining today.", } } } fn main() { // 'Weather' 列挙型のすべてのヴァリアントを反復処理します。 for weather in &[Weather::Sunny, Weather::Cloudy, Weather::Rainy] { // 各天気状況に対する説明を出力します。 println!("{:?}: {}", weather, weather.describe()); } }
Weather
列挙型に対してDescription
トレイトを実装して、各ヴァリアントに対する説明を返すdescribe
メソッドを定義しています。main
関数でWeather
のインスタンスを作成し、その説明を表示しています。
これらの例は、impl
を使って様々なタイプの型に対してメソッドやトレイトを実装する方法を示しています。この機能を利用することで、Rustの柔軟で型安全なコーディングを行うことができます。
関数
[編集]Rustでの関数は、プログラム内で再利用可能なコードブロックを定義するために使われます。関数は特定のタスクを実行するための手続きを含み、必要に応じて引数を受け取り、結果を返すことができます。基本的な関数の定義と使用方法を以下に示します。
関数の定義
[編集]// 関数の定義 fn add(a: i32, b: i32) -> i32 { a + b // 最後の式の結果が自動的に返り値になる }
この例では、add
という名前の関数が定義されています。a
とb
は整数型(i32
)の引数を受け取ります。-> i32
は関数がi32
型の値を返すことを示しています。
関数の呼び出し
[編集]let result = add(3, 5); println!("Result: {}", result); // "Result: 8"が出力される
add
関数は3
と5
を引数に取り、それらを足して8
を返します。println!
マクロを使ってその結果を出力しています。
引数と戻り値
[編集]- 引数: 関数に渡す値。関数の定義において、引数は型を指定する必要があります。
- 戻り値:
->
演算子を使って関数が返す値の型を指定します。Rustでは最後の式の結果が自動的に返り値となります。
パターンマッチングを使用した複数の戻り値
[編集]Rustの関数は複数の値を返すこともできます。
fn calculate(a: i32, b: i32) -> (i32, i32) { (a + b, a - b) } let (sum, difference) = calculate(10, 5); println!("Sum: {}, Difference: {}", sum, difference); // "Sum: 15, Difference: 5"が出力される
関数の機能
[編集]- 再帰: 自分自身を呼び出すことができます。
- クロージャ: 無名の関数を作成し、変数にキャプチャさせることができます。
- ジェネリクス: 型を指定せずに関数を定義することができ、後から具体的な型を指定できます。
Rustの関数は安全性、速度、パターンマッチング、ジェネリクス、所有権など、言語の多くの特徴を活用しています。これらの特性は、Rustを強力なプログラミング言語にしています。
ライフタイム:
[編集]Rustの関数とライフタイムは、関連性がありますが、関数のシグネチャ内でライフタイムを使用することは必ずしも必要ではありません。しかし、関数が参照を含む場合や、ジェネリクスを使う場合には、ライフタイムの指定が必要になることがあります。
関数内のライフタイム
[編集]fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
この例では、longest
関数は2つの文字列スライスを引数として受け取り、それらのうち長さが長い方の参照を返します。'a
はライフタイムパラメータであり、2つの引数と返り値の参照のライフタイムが同じことを示しています。これにより、返される参照が有効なスコープを保証します。
ライフタイムの省略
[編集]Rustでは、ライフタイムの省略規則があります。特定のパターンにおいては、コンパイラが暗黙的にライフタイムを推論することができます。例えば、次のような関数シグネチャではライフタイムの省略が行われます。
fn longest(x: &str, y: &str) -> &str { // ... }
このような場合、コンパイラは適切なライフタイムを自動的に推論します。ただし、ライフタイムの省略は特定の条件に限定され、全ての場面で使えるわけではありません。
ライフタイムは主に、参照の有効期間を指定するために使用され、特に関数内で参照を扱う際に重要な役割を果たします。関数が複数の参照を扱い、それらの有効期間を整理する必要がある場合には、ライフタイムの指定が必要になることがあります。
クロージャ:
[編集]Rustではクロージャも関数として扱われます。クロージャは自身のスコープ外の変数をキャプチャして利用できます。これは非常に便利で、関数よりも柔軟な振る舞いを提供します。
次のコード例では、クロージャと関数の組み合わせを使って、外部スコープの変数をキャプチャして利用する方法を示します。
fn main() { let base_number = 10; // クロージャの定義 let add_to_base = |x| x + base_number; let new_number = 7; // クロージャの使用 let result = add_to_base(new_number); println!("Result: {}", result); // "Result: 17"が出力される }
この例では、base_number
という変数がadd_to_base
クロージャにキャプチャされ、後でクロージャ内で使用されています。クロージャは外部の変数をキャプチャすることができ、そのコンテキストを保持して使用できる点が関数とは異なる特徴です。
Rustの関数は、パターンマッチングやジェネリクス、ライフタイム、クロージャなどの機能と組み合わせて、安全で効率的なコードを記述するための強力なツールです。
高階関数
[編集]Rustは、関数型プログラミングの要素を備えた汎用プログラミング言語です。Rustでは、関数を変数やデータ構造に格納したり、関数を関数のパラメータとして渡したりすることができます。これらの機能は、高階関数として知られています。
Rustで高階関数を使用すると、コードの再利用性と保守性を向上させることができます。また、コードをより簡潔で読みやすくすることもできます。
Rustで高階関数を使用する方法はいくつかあります。
関数を変数に格納する
[編集]Rustでは、関数を変数に格納することができます。これにより、関数を繰り返し使用したり、関数を別の関数に渡したりすることができます。
fn square(x: i32) -> i32 { x * x } let square_function = square; println!("The square of 5 is {}", square_function(5));
このコードでは、square()関数をsquare_function変数に格納しています。その後、square_function()関数を呼び出すことで、square()関数と同じ結果を得ることができます。
関数を関数のパラメータとして渡す
[編集]Rustでは、関数を関数のパラメータとして渡すことができます。これにより、関数を別の関数に処理させることができます。
fn map_numbers(numbers: &[i32], f: fn(i32) -> i32) -> Vec<i32> { let mut result = Vec::new(); for number in numbers { result.push(f(number)); } result } fn square(x: i32) -> i32 { x * x } let numbers = [1, 2, 3, 4, 5]; let squared_numbers = map_numbers(numbers, square); println!("The squared numbers are: {:?}", squared_numbers);
このコードでは、map_numbers()関数は、numbers配列の各要素をf関数に渡し、その結果を新しい配列に格納します。
クロージャを使用する
[編集]Rustでは、クロージャを使用して高階関数を作成することができます。クロージャは、関数本体の一部として定義された関数です。クロージャは、変数やデータ構造を捕捉して、関数本体内で使用することができます。
fn filter_numbers(numbers: &[i32], f: impl Fn(i32) -> bool) -> Vec<i32> { let mut result = Vec::new(); for number in numbers { if f(number) { result.push(number); } } result } fn is_even(x: i32) -> bool { x % 2 == 0 } let numbers = [1, 2, 3, 4, 5]; let even_numbers = filter_numbers(numbers, is_even); println!("The even numbers are: {:?}", even_numbers);
このコードでは、filter_numbers()関数は、numbers配列の各要素をfクロージャに渡し、その結果がtrueである場合、その要素を新しい配列に格納します。
Rustで高階関数を使用すると、コードの再利用性と保守性を向上させることができます。また、コードをより簡潔で読みやすくすることもできます。
標準ライブラリー
[編集]Rustの標準ライブラリー(Standard Library)は、Rustコンパイラにバンドルされており、基本的なデータ構造、OSとのやり取り、スレッド、ファイルI/O、ネットワーキングなどの多くの機能を提供しています。以下に、標準ライブラリーの主要なカテゴリを紹介します。
コレクション(Collections)
[編集]Vec
,VecDeque
: ベクターや双方向キューなどの動的な配列。HashMap
,BTreeMap
: ハッシュマップやBツリーマップなどのキーと値のペアを保持するマップ。HashSet
,BTreeSet
: ハッシュセットやBツリーセットなどのユニークな値を保持するセット。
スレッドと同期(Concurrency)
[編集]std::thread
: スレッドの生成と操作を提供。std::sync
: Mutex、Atomicなどの同期機能を提供するモジュール。
ファイルI/Oとネットワーキング(I/O and Networking)
[編集]std::fs
: ファイルシステムとの相互作用を可能にするモジュール。std::net
: ネットワーキングのためのモジュール。
プリミティブ型(Primitive Types)
[編集]std::primitive
: Rustのプリミティブ型(整数、浮動小数点数など)の機能を提供。
OS相互作用とその他(OS Interactions and Miscellaneous)
[編集]std::env
: 環境変数の取得などのOS環境に関する操作。std::time
: 時間に関する機能を提供。
入出力(Input/Output)
[編集]std::io
: 標準入出力やバッファリング、ファイルI/OなどのI/O操作を提供。
文字列処理(String Manipulation)
[編集]std::str
: 文字列の操作、変換、検索などの機能を提供。
メモリ管理(Memory Management)
[編集]std::alloc
: メモリの割り当てと解放のための機能。
コンパイラ支援(Compiler Support)
[編集]std::marker
: マーカートレイトを提供し、コンパイラへのヒントを与える。
これらは標準ライブラリーの一部であり、Rustの基本的な機能を提供しています。開発者はこれらのモジュールや機能を組み合わせて、安全で効率的なプログラムを構築できます。
コードギャラリー
[編集]このコードギャラリーは、さまざまなRustの機能やパターン、ベストプラクティスを示すためのサンプルコード集です。
エラトステネスの篩
[編集]fn eratosthenes(n: usize) { let mut sieve = vec![true; n + 1]; sieve[0] = false; sieve[1] = false; for i in 2..=n { if sieve[i] { println!("{}", i); let mut j = i * 2; while j <= n { sieve[j] = false; j += i; } } } } fn main() { eratosthenes(100); }
このRustのコードは、エラトステネスの篩を使用して与えられた範囲内の素数を見つけるものです。
eratosthenes
関数は、与えられたn
までの素数を見つけるためのエラトステネスの篩アルゴリズムを実装しています。このアルゴリズムでは、最初にn + 1
サイズのsieve
というブール型の配列を作成します。この配列は、各インデックスが素数かどうかを示します。sieve[0]
とsieve[1]
は素数ではないので、それらをfalse
に設定します。- 2 から
n
までの各数について、その数が素数である場合は、その数の倍数を素数ではないとマークします。これにより、素数の倍数を持つ数は素数ではないことがわかります。 main
関数では、eratosthenes
関数を呼び出し、100までの素数を見つけます。見つかった素数は画面に出力されます。
このアルゴリズムは素数を見つけるための効率的な方法の一つであり、与えられた範囲内の素数を見つけることができます。
最大公約数と最小公倍数
[編集]fn gcd2(m: i32, n: i32) -> i32 { if n == 0 { m } else { gcd2(n, m % n) } } fn gcd(ints: &[i32]) -> i32 { ints.iter().fold(ints[0], |acc, &value| gcd2(acc, value)) } fn lcm2(m: i32, n: i32) -> i32 { m * n / gcd2(m, n) } fn lcm(ints: &[i32]) -> i32 { ints.iter().fold(ints[0], |acc, &value| lcm2(acc, value)) } fn main() { println!("gcd2(30, 45) => {}", gcd2(30, 45)); println!("gcd(&[30, 72, 12]) => {}", gcd(&[30, 72, 12])); println!("lcm2(30, 72) => {}", lcm2(30, 72)); println!("lcm(&[30, 42, 72]) => {}", lcm(&[30, 42, 72])); }
このコードは高階関数を利用しています。fold
関数は特に重要で、与えられた配列内の要素に対して特定の操作を順番に適用することができます。
まず、gcd
関数では、ints
配列内の要素に対してfold
関数を使って最大公約数(gcd2
関数)を計算しています。fold
は初期値としてints[0]
を受け取り、各要素value
に対してgcd2(acc, value)
を適用し、次の要素に対して再帰的にgcd2
を適用します。これにより、配列内のすべての要素の最大公約数が計算されます。
同様に、lcm
関数もfold
を利用しています。ここではlcm2
関数が利用され、各要素に対して最小公倍数を求めるための計算が行われます。
高階関数の利用により、配列内の要素に対して繰り返し処理を行う必要がある場合でも、シンプルで効率的なコードを書くことができます。
二分法
[編集]fn bisection(low: f64, high: f64, f: impl Fn(f64) -> f64) -> f64 { let x = (low + high) / 2.0; let fx = f(x); match () { _ if (fx.abs() - 1.0e-10) < f64::EPSILON => x, _ if fx < 0.0 => bisection(x, high, f), _ => bisection(low, x, f), } } fn main() { let result1 = bisection(0.0, 3.0, |x| x - 1.0); println!("{}", result1); let result2 = bisection(0.0, 3.0, |x| x * x - 1.0); println!("{}", result2); }
- 旧課程(-2012年度)高等学校数学B/数値計算とコンピューター#2分法の例を Rust に移植しました。
このRustのコードは、二分法(bisection method)を使って与えられた関数の根を見つけるものです。
bisection
関数は、low
からhigh
の範囲で与えられた関数 f
の根を探します。f
は Fn(f64) -> f64
のトレイトを実装しており、実際の関数の定義は呼び出し時に与えられます。
この関数は再帰的に呼び出されます。与えられた区間 [low, high]
の中央値 x
を求め、その点での関数の値 f(x)
を計算します。この値が非常に小さいか(ここでは 1.0e-10
未満)、または非常に近い数になるまで low
または high
を更新して、区間を狭めていきます。
main
関数では、2つの異なる関数 x - 1
と x^2 - 1
に対して bisection
関数を呼び出して、それぞれの関数の根を探し、println!
マクロを使って根を表示しています。
構造体とメソッド
[編集]Rustにクラスはありませんが、構造体がメソッドを持つことが出来ます。
#[derive(Debug)] struct Hello { s: String, } impl Hello { fn new(s: &str) -> Hello { let s = if s.is_empty() { "world" } else { s }; Hello { s: s.to_string() } } fn to_string(&self) -> String { format!("Hello {}!", self.s) } fn print(&self) { println!("{}", self.s); } } fn main() { let hello1 = Hello::new(""); println!("{}", hello1.to_string()); hello1.print(); let hello2 = Hello::new("my friend"); println!("{}", hello2.to_string()); hello2.print(); println!(" Hello.constructor.name => Hello hello1 => {:?} hello2.s => {}", hello1, hello2.s); }
このRustのコードは、Hello
という名前の構造体を定義し、その構造体に関連するメソッドや、main()
関数を含んでいます。
まず、Hello
構造体はString
型のs
フィールドを持っています。#[derive(Debug)]
アトリビュートは、この構造体にDebug
トレイトを自動的に実装するようコンパイラに指示しています。Debug
トレイトを実装することで、デバッグ目的で構造体の内容を出力できるようになります。
impl Hello
ブロックでは、Hello
構造体に対するメソッドが定義されています。
new
メソッドは、引数として文字列を受け取り、それが空文字列の場合はデフォルトの文字列 "world" を持つHello
構造体を生成します。それ以外の場合は、引数で渡された文字列を使用してHello
構造体を作成します。to_string
メソッドは、Hello
構造体のインスタンスに対して、挨拶文を含む文字列を生成します。print
メソッドは、Hello
構造体のインスタンスのs
フィールド(挨拶文)を標準出力に表示します。
main()
関数では、Hello
構造体を使ってインスタンスを生成し、メソッドを呼び出しています。println!
マクロを使用して、構造体やそのフィールドをデバッグ出力しています。最後のprintln!
マクロでは、hello1
のデバッグ表示({:?}
)とhello2
のs
フィールドを出力しています。
構造体とメソッド(2)
[編集]Go/メソッドとインターフェースからの移植です。
use std::f64::consts::PI; #[derive(Debug)] struct GeoCoord { longitude: f64, latitude: f64, } impl GeoCoord { fn distance(&self, other: &GeoCoord) -> f64 { let i = PI / 180.0; let r = 6371.008; let lat_i = self.latitude * i; let other_lat_i = other.latitude * i; let long_diff_i = (self.longitude - other.longitude) * i; let sin_lat = f64::sin(lat_i) * f64::sin(other_lat_i); let cos_lat = f64::cos(lat_i) * f64::cos(other_lat_i); let cos_long_diff = f64::cos(long_diff_i); let distance = f64::acos(sin_lat + cos_lat * cos_long_diff) * r; distance } } impl std::fmt::Display for GeoCoord { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let (mut ew, mut ns) = ("東経", "北緯"); let mut long = self.longitude; let mut lat = self.latitude; if long < 0.0 { ew = "西経"; long = -long; } if lat < 0.0 { ns = "南緯"; lat = -lat; } write!(f, "({}: {}, {}: {})", ew, long, ns, lat) } } fn main() { let sites = vec![ ("東京駅", GeoCoord { longitude: 139.7673068, latitude: 35.6809591 }), ("シドニー・オペラハウス", GeoCoord { longitude: 151.215278, latitude: -33.856778 }), ("グリニッジ天文台", GeoCoord { longitude: -0.0014, latitude: 51.4778 }), ]; for (name, gc) in &sites { println!("{}: {}", name, gc); } let ks: Vec<&str> = sites.iter().map(|(k, _)| *k).collect(); for i in 0..sites.len() { let ksi = ks[i]; let ksx = ks[(i + 1) % sites.len()]; println!( "{} - {}: {:.2} [km]", ksi, ksx, sites[i].1.distance(&sites[(i + 1) % sites.len()].1) ); } }
逆ポーランド記法の解析と評価
[編集]逆ポーランド記法は、数式の演算子を後置記法で表現する方法です。通常の中置記法では演算子がオペランドの間に置かれますが、逆ポーランド記法では演算子がオペランドの後ろに置かれます。これにより、括弧や演算子の優先順位を考える必要がなくなり、計算機で容易に評価できる形式になります。
例えば、中置記法での式 3 + 4 * 5
は、逆ポーランド記法では 3 4 5 * +
と表現されます。この記法では、演算子が対象のオペランドに対して順番に適用されます。
enum Token { Add, Sub, Mul, Div, Operand(i32), } impl Token { fn evaluate(&self, stack: &mut Vec<i32>) -> Result<(), &'static str> { match self { Token::Add | Token::Sub | Token::Mul | Token::Div => { if stack.len() < 2 { return Err("Invalid expression: not enough operands for operator"); } let operand2 = stack.pop().unwrap(); let operand1 = stack.pop().unwrap(); match self { Token::Add => stack.push(operand1 + operand2), Token::Sub => stack.push(operand1 - operand2), Token::Mul => stack.push(operand1 * operand2), Token::Div => { if operand2 == 0 { return Err("Division by zero"); } stack.push(operand1 / operand2); } _ => unreachable!(), } } Token::Operand(num) => { stack.push(*num); } } Ok(()) } } fn evaluate_expression(expression: &str) -> Result<i32, &'static str> { let mut stack: Vec<i32> = Vec::new(); let tokens: Vec<Token> = expression .split_whitespace() .map(|token| { if let Ok(parsed_num) = token.parse::<i32>() { Token::Operand(parsed_num) } else { match token { "+" => Token::Add, "-" => Token::Sub, "*" => Token::Mul, "/" => Token::Div, _ => unreachable!(), } } }) .collect(); for token in tokens { if let Err(err) = token.evaluate(&mut stack) { return Err(err); } } if stack.len() != 1 { return Err("Invalid expression: too many operands or operators"); } Ok(stack[0]) } fn main() { let expression = "5 3 2 * + 8 2 / -"; match evaluate_expression(expression) { Ok(result) => println!("Result: {}", result), Err(err) => println!("Error: {}", err), } }
このコードは、Token
という列挙型を使って逆ポーランド記法の式を評価する関数を実装しています。
まず、Token
はAdd
、Sub
、Mul
、Div
、Operand
の5つのバリアントを持ちます。Operand
は整数値を保持します。
Token
にはevaluate
というメソッドが実装されています。このメソッドでは、Token
の各バリアントに対する処理が行われます。Add
、Sub
、Mul
、Div
の場合は、スタックから2つの値を取り出して、それらを演算し結果をスタックに積みます。Operand
の場合は、その値をスタックに積みます。
evaluate_expression
関数では、与えられた式をトークン化してToken
のベクターに変換し、それぞれのトークンに対してevaluate
メソッドを実行します。各トークンの評価においてエラーが発生した場合、そのエラーメッセージが直ちに返されます。最終的に、スタックに残った値が1つでない場合もエラーが返されます。
main
関数では、evaluate_expression
の結果に応じて結果を出力するか、エラーを表示します。これにより、逆ポーランド記法の式を評価し、正常な結果またはエラーメッセージを表示できます。
式を逆ポーランド記法に変換する(手書き)
[編集]#[derive(Debug, Clone, Copy)] enum Token { Number(i32), Plus, Minus, Multiply, Divide, } fn main() { let input = "12+34*56/78"; let tokens = parse_input(input); let rpn = infix_to_rpn(tokens); println!("{:?}", rpn); } fn parse_input(input: &str) -> Vec<Token> { let mut tokens = Vec::new(); let mut num = String::new(); for c in input.chars() { match c { '0'..='9' => num.push(c), '+' | '-' | '*' | '/' => { if !num.is_empty() { let n = num.parse().unwrap(); tokens.push(Token::Number(n)); num.clear(); } match c { '+' => tokens.push(Token::Plus), '-' => tokens.push(Token::Minus), '*' => tokens.push(Token::Multiply), '/' => tokens.push(Token::Divide), _ => unreachable!(), } } _ => panic!("Invalid character in input!"), } } if !num.is_empty() { let n = num.parse().unwrap(); tokens.push(Token::Number(n)); } tokens } fn infix_to_rpn(tokens: Vec<Token>) -> Vec<Token> { let mut rpn = Vec::new(); let mut stack = Vec::new(); for token in tokens { match token { Token::Number(_) => rpn.push(token), Token::Plus | Token::Minus | Token::Multiply | Token::Divide => { while let Some(top) = stack.last().copied() { if precedence(&token) <= precedence(&top) { rpn.push(stack.pop().unwrap()); } else { break; } } stack.push(token); } } } while let Some(op) = stack.pop() { rpn.push(op); } rpn } fn precedence(token: &Token) -> i32 { match token { Token::Multiply | Token::Divide => 2, Token::Plus | Token::Minus => 1, _ => 0, } }
- 実行結果
[Number(12), Number(34), Number(56), Multiply, Number(78), Divide, Plus]
このコードは、与えられた文字列を逆ポーランド記法(RPN)に変換するプログラムです。以下にその構造を解説します:
Token
列挙型: 数字と演算子を表す列挙型です。Number
は数字を、Plus
,Minus
,Multiply
,Divide
はそれぞれ演算子を表します。derive(Debug, Clone, Copy)
が付与されており、デバッグ表示やクローン、コピーが可能です。parse_input
関数: 与えられた文字列をトークンに分割します。数字の場合は文字列を数値に変換してToken::Number
に、演算子の場合は対応するToken
に変換し、それらをVec<Token>
に収集します。infix_to_rpn
関数: 中置記法のトークンのベクターを逆ポーランド記法に変換します。スタックとRPNベクターを使用して、トークンを処理します。演算子の場合、スタックのトップとの優先順位を比較して、適切な順序でRPNに追加します。precedence
関数: 演算子の優先順位を返します。乗算と除算が優先され、それ以外の演算子は同じ優先順位です。
このコードは、入力された文字列を数値と演算子に分割し、それらを逆ポーランド記法に変換する機能を持っています。特定の演算子の優先順位を考慮しながら適切な順序で演算子を配置し、RPNを生成します。
式を逆ポーランド記法に変換する(手書き:別解:再帰下降パーサー)
[編集]#[derive(Debug, PartialEq)] enum Token { Number(f64), Plus, Minus, Multiply, Divide, LParen, RParen, EOF, } struct Lexer<'a> { input: &'a str, position: usize, } impl<'a> Lexer<'a> { fn new(input: &'a str) -> Lexer<'a> { Lexer { input, position: 0 } } fn skip_whitespace(&mut self) { while self.position < self.input.len() && self.input.chars().nth(self.position).unwrap().is_whitespace() { self.position += 1; } } fn next_token(&mut self) -> Token { self.skip_whitespace(); if self.position >= self.input.len() { return Token::EOF; } let current_char = self.input.chars().nth(self.position).unwrap(); match current_char { '+' => { self.position += 1; Token::Plus } '-' => { self.position += 1; Token::Minus } '*' => { self.position += 1; Token::Multiply } '/' => { self.position += 1; Token::Divide } '(' => { self.position += 1; Token::LParen } ')' => { self.position += 1; Token::RParen } _ => { if current_char.is_digit(10) || current_char == '.' { let start = self.position; while self.position < self.input.len() && (self.input.chars().nth(self.position).unwrap().is_digit(10) || self.input.chars().nth(self.position).unwrap() == '.') { self.position += 1; } let number_str = &self.input[start..self.position]; let number = number_str.parse::<f64>().unwrap(); Token::Number(number) } else { panic!("Invalid character found: {}", current_char); } } } } } struct Parser<'a> { lexer: Lexer<'a>, current_token: Token, } impl<'a> Parser<'a> { fn new(mut lexer: Lexer<'a>) -> Parser<'a> { let current_token = lexer.next_token(); Parser { lexer, current_token } } fn eat(&mut self, token: Token) { if self.current_token == token { self.current_token = self.lexer.next_token(); } else { panic!("Invalid syntax"); } } fn factor(&mut self) -> Vec<Token> { match self.current_token { Token::Number(num) => { self.eat(Token::Number(num)); vec![Token::Number(num)] } Token::LParen => { self.eat(Token::LParen); let result = self.expr(); self.eat(Token::RParen); result } _ => panic!("Invalid syntax"), } } fn term(&mut self) -> Vec<Token> { let mut result = self.factor(); while vec![Token::Multiply, Token::Divide].contains(&self.current_token) { match self.current_token { Token::Multiply => { self.eat(Token::Multiply); let mut next_factor = self.factor(); result.append(&mut next_factor); result.push(Token::Multiply); } Token::Divide => { self.eat(Token::Divide); let mut next_factor = self.factor(); result.append(&mut next_factor); result.push(Token::Divide); } _ => panic!("Invalid syntax"), } } result } fn expr(&mut self) -> Vec<Token> { let mut result = self.term(); while vec![Token::Plus, Token::Minus].contains(&self.current_token) { match self.current_token { Token::Plus => { self.eat(Token::Plus); let mut next_term = self.term(); result.append(&mut next_term); result.push(Token::Plus); } Token::Minus => { self.eat(Token::Minus); let mut next_term = self.term(); result.append(&mut next_term); result.push(Token::Minus); } _ => panic!("Invalid syntax"), } } result } } fn generate_reverse_polish_notation(input: &str) -> Vec<Token> { let lexer = Lexer::new(input); let mut parser = Parser::new(lexer); parser.expr() } fn main() { let result = generate_reverse_polish_notation("12+34*56/78"); println!("{:?}", result); }
コードは、与えられた数式を逆ポーランド記法に変換するためのプログラムです。ここでは、Lexer(字句解析器)とParser(構文解析器)という2つの主要な構成要素があります。
- Token
Token
列挙型は、数式をトークンに分割するために使用されます。数字、演算子、および括弧のトークンを定義します。
- Lexer
Lexer
は、与えられた数式文字列をトークンに分割する役割を担います。next_token
メソッドは、数式の文字列を走査して、各文字がどの種類のトークンに対応するかを判断します。
- Parser
Parser
は、Lexer によって生成されたトークンのストリームを受け取り、逆ポーランド表記に変換します。再帰的に式を解析し、優先順位を考慮しながら、逆ポーランド表記のトークン列を生成します。
factor()
メソッドは、数または括弧で始まる要素(ファクター)を解析します。term()
メソッドは、乗算と除算の演算子を解析します。expr()
メソッドは、加算と減算の演算子を解析します。
- generate_reverse_polish_notation 関数
- この関数は、与えられた数式文字列を逆ポーランド表記に変換します。Lexer を使ってトークンに分割し、Parser を使って逆ポーランド表記のトークン列を生成します。
- main 関数
generate_reverse_polish_notation
を使って、指定された式を逆ポーランド表記で出力します。
逆ポーランド記法は、演算子がオペランドの後ろに置かれるので、式を解析してトークン列に変換することで、演算の優先順位を反映した形で数式を表現することができます。
これらのコードは、与えられた数学式を逆ポーランド記法(Reverse Polish Notation, RPN)に変換する方法を示していますが、アプローチが異なります。
1番目のコードは、文字列を直接解析してトークンに分割し、その後逆ポーランド記法に変換しています。一方、2番目のコードは、字句解析器(lexer)とパーサー(parser)を使用して、トークンに分割し、その後パースして逆ポーランド記法に変換しています。
1番目のコードは、基本的な数値と演算子の処理に集中しています。一方で、2番目のコードは字句解析や構文解析の段階を厳密に分離しています。また、2番目のコードは小数点もサポートしており、より柔軟な数値表現を可能にしています。
どちらのコードも同じ目的を果たしていますが、アプローチの違いが見られます。1番目のコードはシンプルで直感的ですが、拡張性に欠けるかもしれません。
一方で、2番目のコードはより複雑ですが、より柔軟で拡張性があります。それぞれのコードには長所と短所がありますが、どちらも与えられた数式を逆ポーランド記法に変換する点では同等の結果を提供します。実際には2番めのコードは不動小数点数やカッコに対応しています。
式を逆ポーランド記法に変換する(nom版)
[編集]use nom::character::complete::{char, digit1}; use nom::combinator::map; use nom::multi::many0; use nom::sequence::delimited; use nom::IResult; #[derive(Debug, Clone, Copy)] enum Token { Number(i32), Plus, Minus, Multiply, Divide, } fn main() { let input = "12+34*56/78"; let (_, tokens) = parse_input(input).unwrap(); let rpn = infix_to_rpn(tokens); println!("{:?}", rpn); } fn parse_input(input: &str) -> IResult<&str, Vec<Token>> { many0(parse_token)(input) } fn parse_token(input: &str) -> IResult<&str, Token> { let (input, token) = delimited( nom::character::complete::space0, nom::branch::alt(( map(digit1, |s: &str| Token::Number(s.parse().unwrap())), map(char('+'), |_| Token::Plus), map(char('-'), |_| Token::Minus), map(char('*'), |_| Token::Multiply), map(char('/'), |_| Token::Divide), )), nom::character::complete::space0, )(input)?; Ok((input, token)) } fn infix_to_rpn(tokens: Vec<Token>) -> Vec<Token> { let mut rpn = Vec::new(); let mut stack = Vec::new(); for token in tokens { match token { Token::Number(_) => rpn.push(token), Token::Plus | Token::Minus | Token::Multiply | Token::Divide => { while let Some(top) = stack.last().copied() { if precedence(&token) <= precedence(&top) { rpn.push(stack.pop().unwrap()); } else { break; } } stack.push(token); } } } while let Some(op) = stack.pop() { rpn.push(op); } rpn } fn precedence(token: &Token) -> i32 { match token { Token::Multiply | Token::Divide => 2, Token::Plus | Token::Minus => 1, _ => 0, } }
このコードは、nom
というパーサーコンビネータライブラリを使用して、与えられた文字列を解析し、トークンに分割する機能を持っています。前のコードと比較してみましょう。
parse_input
関数:many0
コンビネータを使って、parse_token
を繰り返し適用し、入力文字列をトークンのベクターに変換します。IResult
型を返します。parse_token
関数:delimited
コンビネータを使用してトークンの前後のスペースを処理し、与えられた文字列を様々なルールにマッチングさせます。数字、演算子それぞれのパースを行い、Token
列挙型のトークンを返します。infix_to_rpn
関数: 前のコードと同じですが、与えられたトークンのベクターを逆ポーランド記法に変換する機能を持っています。
このコードは、nom
を使ってトークン分割を行い、より柔軟なパースを可能にしています。nom
を使用することで、トークンのパースやスペースの処理など、より複雑なルールを柔軟に記述できるようになります。
複素数式評価器
[編集]extern crate num_complex; use num_complex::Complex; #[derive(Debug, PartialEq, Clone)] enum Token { Number(Complex<f64>), Plus, Minus, Multiply, Divide, LParen, RParen, EOF, } struct Lexer<'a> { input: &'a str, position: usize, } impl<'a> Lexer<'a> { fn new(input: &'a str) -> Lexer<'a> { Lexer { input, position: 0 } } fn skip_whitespace(&mut self) { while self.position < self.input.len() && self.input.chars().nth(self.position).unwrap().is_whitespace() { self.position += 1; } } fn next_token(&mut self) -> Token { self.skip_whitespace(); if self.position >= self.input.len() { return Token::EOF; } let current_char = self.input.chars().nth(self.position).unwrap(); match current_char { '+' => { self.position += 1; Token::Plus } '-' => { self.position += 1; Token::Minus } '*' => { self.position += 1; Token::Multiply } '/' => { self.position += 1; Token::Divide } '(' => { self.position += 1; Token::LParen } ')' => { self.position += 1; Token::RParen } 'i' => { self.position += 1; Token::Number(Complex::new(0.0, 1.0)) } _ if current_char.is_digit(10) || current_char == '.' => { let start = self.position; while self.position < self.input.len() && (self.input.chars().nth(self.position).unwrap().is_digit(10) || self.input.chars().nth(self.position).unwrap() == '.') { self.position += 1; } let number_str = &self.input[start..self.position]; let number = number_str.parse::<f64>().unwrap(); if let Some('i') = self.input.chars().nth(self.position) { self.position += 1; Token::Number(Complex::new(0.0, number)) } else { Token::Number(Complex::new(number, 0.0)) } } _ => panic!("Invalid character found: {}", current_char), } } } struct Parser<'a> { lexer: Lexer<'a>, current_token: Token, } impl<'a> Parser<'a> { fn new(mut lexer: Lexer<'a>) -> Parser<'a> { let current_token = lexer.next_token(); Parser { lexer, current_token } } fn eat(&mut self, token: Token) { if self.current_token == token { self.current_token = self.lexer.next_token(); } else { panic!("Invalid syntax"); } } fn factor(&mut self) -> Complex<f64> { let token = self.current_token.clone(); match token { Token::Number(num) => { self.eat(Token::Number(num)); num } Token::LParen => { self.eat(Token::LParen); let result = self.expr(); self.eat(Token::RParen); result } _ => panic!("Invalid syntax"), } } fn term(&mut self) -> Complex<f64> { let mut result = self.factor(); while vec![Token::Multiply, Token::Divide].contains(&self.current_token) { let token = self.current_token.clone(); match token { Token::Multiply => { self.eat(Token::Multiply); let next_factor = self.factor(); result = result * next_factor; } Token::Divide => { self.eat(Token::Divide); let next_factor = self.factor(); result = result / next_factor; } _ => panic!("Invalid syntax"), } } result } fn expr(&mut self) -> Complex<f64> { let mut result = self.term(); while vec![Token::Plus, Token::Minus].contains(&self.current_token) { let token = self.current_token.clone(); match token { Token::Plus => { self.eat(Token::Plus); let next_term = self.term(); result = result + next_term; } Token::Minus => { self.eat(Token::Minus); let next_term = self.term(); result = result - next_term; } _ => panic!("Invalid syntax"), } } result } } fn main() { let lexer = Lexer::new("(2+ 3i)*4i"); let mut parser = Parser::new(lexer); let result = parser.expr(); println!("{:?}", result); }
このコードは、数式をパースして複素数を計算する簡単な計算機の基本的な実装です。Rustの機能を活用して、トークン列を生成するLexerと、そのトークン列を解析して計算を行うParserを定義しています。
Token
は、パーサーが認識するトークンの種類を表すenumです。Lexer
は文字列を受け取り、その文字列をトークンに分割する役割を果たします。各トークンは、演算子や数値、括弧などを表現しています。Parser
は、Lexerが生成したトークン列を受け取り、それを解析して数式を計算します。
Lexer
は空白をスキップし、文字列を一文字ずつ見ていき、トークン列を生成します。Parser
はトークン列を再帰的に解析し、四則演算を行って複素数を計算します。演算子の優先順位や括弧の処理も考慮されています。
このコードは、入力文字列 (2+ 3i)*4i
を受け取り、それを計算して結果を表示します。各段階でトークンが正しく識別され、演算子や数値が正しく解釈されることを期待しています。
コード内のパニックは、予期しないトークンや構文エラーがあった場合に発生します。これらのエラーは、コードが期待する形式に文字列が合致しなかった場合に発生します。
このコードを用いると、複雑な数式も計算できますが、入力の検証やエラー処理についてはまだ改善の余地があります。
複素数
[編集]extern crate num_complex; use num_complex::Complex; fn main() { // 複素数の作成 let a = Complex::new(3.0, 4.0); let b = Complex::new(-2.0, 5.0); println!("a: {a}"); println!("b: {b}"); println!("a + b: {}", a + b); println!("a - b: {}", a - b); println!("a * b: {}", a * b); println!("a / b: {}", a / b); }
- 実行結果
a: 3+4i b: -2+5i a + b: 1+9i a - b: 5-1i a * b: -26+7i a / b: 0.4827586206896552-0.7931034482758621i
このコードは、num-complex
クレートを使用して複素数を扱うRustの例です。
extern crate num_complex;
およびuse num_complex::Complex;
は、num-complex
クレートからComplex
型を使えるようにするためのインポートです。Complex::new(3.0, 4.0);
およびComplex::new(-2.0, 5.0);
は、実部と虚部を指定して複素数を作成しています。println!("a: {a}");
およびprintln!("b: {b}");
は、複素数a
とb
を出力しています。
b: {}", a * b);、println!("a / b: {}", a / b);
は、それぞれ複素数 a
と b
の加算、減算、乗算、除算を行っています。結果を文字列として出力しています。
修正されたコードは次のようになります:
ほかの言語からの移植例
[編集]順列・組合わせ
[編集]Goから順列・組合わせを移植
順列
[編集]- 順列
fn permutation<T: Clone>(s: &[T], n: usize) -> Vec<Vec<T>> { if s.is_empty() { panic!("slice is nil"); } if n == 1 { let mut result = Vec::new(); for v in s.iter() { result.push(vec![v.clone()]); } return result; } let mut result = Vec::new(); for (i, v) in s.iter().enumerate() { let mut sf = Vec::new(); for (j, w) in s.iter().enumerate() { if j != i { sf.push(w.clone()); } } for w in permutation(&sf, n - 1) { let mut v_w = vec![v.clone()]; v_w.extend_from_slice(&w); result.push(v_w); } } result } fn main() { println!("{:?}", permutation(&[1, 2, 3], 1)); println!("{:?}", permutation(&[0, 1, 2], 2)); println!( "{:?}", permutation(&["abc".to_string(), "def".to_string(), "xyz".to_string()], 3) ); println!("{:?}", permutation::<i32>(&[], 2)); }
- 実行結果
[[1], [2], [3]] [[0, 1], [0, 2], [1, 0], [1, 2], [2, 0], [2, 1]] [["abc", "def", "xyz"], ["abc", "xyz", "def"], ["def", "abc", "xyz"], ["def", "xyz", "abc"], ["xyz", "abc", "def"], ["xyz", "def", "abc"]]
- 解説
- 上記の移植において、主に以下の点に注意が必要でした。
- ジェネリック型の宣言方法がGoとは異なるため、
func Permutation[T any](s []T, n int)
のような書き方はできません。Rustでは、fn permutation<T: Clone>(s: &[T], n: usize)
のように、<T>
の前に:
を付けてジェネリック境界を宣言します。 - Goの
make
は、新しい配列やスライスを作成するための組み込み関数ですが、RustではVec::with_capacity()
やVec::new()
を使用します。 panic!("slice is nil")
は、Rustのパニック処理において、エラーメッセージを伴うパニックを発生させるために使用されます。Vec
に要素を追加するには、Goのappend
に相当するRustのメソッドであるVec::push()
や、Vec::extend_from_slice()
を使用します。また、Vec
の要素数は、len()
ではなくlen()
とVec::capacity()
の両方を使って取得する必要があります。
- ジェネリック型の宣言方法がGoとは異なるため、
組合わせ
[編集]- 組合わせ
fn combination<T: Clone>(s: &[T], n: usize) -> Vec<Vec<T>> { if s.is_empty() { panic!("slice is empty"); } if n == 1 { let mut result = Vec::new(); for v in s { result.push(vec![v.clone()]); } return result; } let mut result = Vec::new(); for i in 0..=(s.len() - n) { let v = s[i].clone(); for w in combination(&s[i + 1..], n - 1) { let mut res = vec![v.clone()]; res.extend(w); result.push(res); } } return result; } fn main() { println!("{:?}", combination(&[1, 2, 3], 1)); println!("{:?}", combination(&[0, 1, 2], 2)); println!( "{:?}", combination(&["abc", "def", "xyz"], 3) ); // println!("{:?}", combination(&[], 2)); 要素型が確定できない }
- 実行結果
[[1], [2], [3]] [[0, 1], [0, 2], [1, 2]] [["abc", "def", "xyz"]]
- 解説
- 上記の移植において、主に以下の点に注意が必要でした。
- Rustのジェネリック関数の型パラメータには制約が必要なため、
T
がクローン可能であることを示すClone
トレイトを指定する必要があります。 - Goのスライスと異なり、Rustのスライスは要素数が0の場合にも安全であるため、
ErrNilSlice
に相当する処理はslice.is_empty()
で判定することができます。 - Goのスライスと異なり、Rustのスライスは範囲外アクセスがパニックを引き起こすため、再帰呼び出し時にはスライスの範囲を明示的に指定する必要があります。
- Rustのジェネリック関数の型パラメータには制約が必要なため、
脚註
[編集]- ^ Hoare, Graydon (2016年12月28日). “Rust is mostly safety”. Graydon2. Dreamwidth Studios. 2021年12月3日閲覧。
- ^ 文字リテラルであることを強調するなら
let hello : &'static str = "Hello, world!";
とすべきだったかもしれません。 - ^ “Data Types - The Rust Programming Language”. 2021年12月8日閲覧。
- ^ Module std::fmt
- ^ Formatting traits
- ^ C言語系では、式を文にする為いに
};
が必要です。Rustもそう説明されている場合がありますが、Rustでは式の型の一致が目的です。 - ^ コードブロックが値を持つプログラミング言語としては、BCPL, Ruby, ScalaやKotlinがあります。
- ^ Option in std::option - Rust
外部リンク
[編集]参考文献
[編集]Jim Blandy, Jason Orendorff. オライリージャパン,『プログラミングRust』, 第2版, (2022年1月19日).
Jim Blandy, Jason Orendorff. オライリージャパン,『プログラミングRust』, 第1版, (2018年8月10日).