Rust
Rustは、安全性とパフォーマンスを兼ね備えたモダンなシステムプログラミング言語です。本書では、Rustの基礎から応用までを網羅し、実践的なプログラミングスキルの習得を目指します。Rustは初めてのプログラミング言語としても、既存のスキルをさらに強化するためにも最適です。その特徴である所有権システムや並行性モデルについても詳しく解説し、堅牢で効率的なプログラムを構築するための知識を提供します。一緒にRustの世界を探求し、その魅力を存分に体験していきましょう。
はじめに
[編集]Rust(ラスト)は、高性能かつ安全な並行処理を実現するために設計されたマルチパラダイム汎用プログラミング言語です[1]。 Rustの構文はC++に似ており、ボローチェッカーを利用して参照の検証を行い、メモリ安全性を保証しています。ボローチェッカーは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 86"), 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.84.0-nightly (1e4f10ba6 2024-10-29)
このコードは、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の所有概念は、変数がリソースの所有権を持ち、スコープを抜ける際にそのリソースを自動的に解放する仕組みです。この仕組みは、メモリ安全性を高めるための中心的な考え方です。
コピーとムーブ
[編集]Rustでは、変数が参照しているデータがスタック上かヒープ上かによって、コピーやムーブの挙動が異なります。スタック上のプリミティブ型はコピーされますが、ヒープ上のデータはムーブされ、所有権が移動します。
- コピーされるデータ
fn main() { let x = 5; let y = x; // xの値がコピーされる println!("x: {}, y: {}", x, y); // 両方の変数が利用可能 }
この例では、整数型x
の値がコピーされるため、x
とy
の両方が使用できます。
- ムーブされるデータ
fn main() { let s1 = String::from("Hello"); let s2 = s1; // s1の所有権がs2に移動 // println!("{}", s1); // コンパイルエラー:s1はもう使用できない println!("{}", s2); // 正常に出力される }
ここでは、s1
の所有権がs2
に移動するため、s1
は利用できなくなります。
次に、コピーとムーブの違いを簡単に表にまとめます。
コピーとムーブの違い データ種別 コピー ムーブ プリミティブ型(整数型、bool型など) コピーされる コピーされる ヒープ上のデータ(String型、Vec型など) されない 所有権が移動する
借用と参照
[編集]所有権を移動せずにデータを利用する方法として、借用(参照)があります。
- 借用と参照の例
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() }
この例では、calculate_length
関数が&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では、関数の引数として変数を渡すと、所有権が関数にムーブされる場合があります。例で見てみましょう。
fn main() { let original = String::from("Hello, Rust!"); // String型の変数originalを作成 let moved = move_ownership(original); // originalがmove_ownership関数にムーブ // println!("original: {}", original); // コンパイルエラー println!("moved: {}", moved); // 正常に動作 } fn move_ownership(input: String) -> String { input }
この例では、original
が関数move_ownership
にムーブされ、original
は使用できなくなります。
クローン
[編集]所有権を共有したい場合、clone
メソッドでデータの複製が可能です。
fn main() { let data1 = vec![1, 2, 3]; let data2 = data1.clone(); // データの複製 println!("{:?}", data1); // data1は所有権を保持している println!("{:?}", data2); // data2はクローンされたデータを持つ }
この例では、data1
の clone
メソッドを呼び出すことで、ヒープ上にあるデータの複製を作成し、data2
に格納します。このとき、data1
と data2
はそれぞれ独立した所有権を持つため、どちらかを変更してももう一方には影響しません。
クローンを作成することで、所有権が必要な場合でも、元のデータをそのまま保持しながらコピーを生成できるため、データの安全な扱いが可能です。
ライフタイム
[編集]ライフタイムは、参照が有効である期間を示すRustの注釈で、所有権と借用のルールと密接に関係しています。ライフタイムを指定することで、コンパイラがメモリの安全性を保証し、不正なメモリアクセスや参照の無効化を防ぎます。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
この例では、'a
というライフタイムパラメータを使って、x
と y
の参照が同じライフタイムを共有することをコンパイラに伝えています。これにより、返り値のライフタイムも入力引数と同じ期間で有効であることが保証されます。
ライフタイムは複雑な場面では特に重要となり、Rustの所有権システムがさらに強力にメモリ安全を保証する仕組みとなっています。
Rc, Box と Drop
[編集]Rc (Reference Counting)
[編集]Rc
は参照カウントを用いたスマートポインタで、複数の所有者が同じデータを共有できるようにします。主にマルチスレッドを必要としない環境で利用され、clone
メソッドでカウントを増加させることで、同じデータを複数の変数が所有できます。
use std::rc::Rc; let a = Rc::new(5); let b = Rc::clone(&a); // aとbが同じデータを共有
Rc
はスレッドセーフではありませんが、シングルスレッドの環境でデータ共有を簡単に行えるため便利です。
Box (Heap Allocation)
[編集]Box
はデータをヒープに格納し、そのポインタをスタック上で保持するスマートポインタです。スタックではなくヒープにデータを配置したい場合に使用します。Box
は最も単純なスマートポインタで、シングルオーナーのデータの所有権を提供します。
let b = Box::new(5); println!("b = {}", b); // ヒープ上に格納されたデータにアクセス
ボックスは特に再帰的なデータ構造の作成やヒープメモリ管理に適しており、ポインタの解放はスコープを外れたときに自動で行われます。
Drop (Resource Cleanup)
[編集]Drop
トレイトは、オブジェクトがスコープを外れるときにリソースをクリーンアップするための機構を提供します。カスタムデストラクタを実装する際に使用され、例えばファイルハンドルやネットワークリソースなどのリソース解放に役立ちます。
struct CustomResource; impl Drop for CustomResource { fn drop(&mut self) { println!("CustomResourceが解放されました"); } } let resource = CustomResource; // スコープを抜けるときにdropメソッドが呼ばれる
Drop
トレイトにより、Rustは自動的にリソース管理を行い、メモリリークやリソースの取りこぼしを防ぎます。手動でdrop
を呼び出すことも可能ですが、通常はスコープ終了時に自動で解放されます。
- まとめ
Rustの所有権システムは、メモリ管理とデータ競合の予防において非常に強力な機能です。所有、借用、ムーブ、コピー、クローンといった各機能を活用することで、安全かつ効率的にメモリを管理できるようになります。特に、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)は、コンパイラに対してコードに関する追加情報や指示を提供するための注釈です。これらはコードの振る舞いや最適化、データレイアウト、コード生成に影響を与えます。属性は#[...]
の形式で記述され、コンパイラやコードの挙動を制御します。Rustでは、属性を使用することでコードの効率性や可読性を高め、特定の機能を拡張できます。
よく使用される属性
[編集]#[derive(...)]
- 自動導出(Derive Attribute)は、特定のトレイトを自動的に実装するために使用されます。
- 例:
#[derive(Debug)]
は、構造体や列挙型にDebug
トレイトを自動的に実装させ、println!
でその内容を表示可能にします。
#[cfg(...)]
- コンパイル時の条件を指定するために使います。条件に応じてコードの一部をコンパイルするかどうかを制御します。
- 例:
#[cfg(target_os = "linux")]
は、ターゲットOSがLinuxの場合のみ有効です。
#[allow(...)]
/#[deny(...)]
/#[warn(...)]
- コンパイラ警告のレベルを制御します。これにより、警告を許可、エラー化、または警告として表示させることができます。
- 例:
#[deny(warnings)]
は、すべての警告をエラーとして扱います。
#[repr(...)]
- データのレイアウト方法を指定します。構造体や列挙型のメモリ配置を変更するために使用されます。
- 例:
#[repr(C)]
は、C言語と互換性のあるレイアウトを指定します。
#[inline]
/#[noinline]
- インライン展開の制御を行います。関数呼び出しのオーバーヘッドを削減するために使われ、最適化のためにインライン展開を促進します。
- 例:
#[inline(always)]
は、関数が常にインライン展開されるように指定します。
#[test]
/#[bench]
- ユニットテストやベンチマークテストをマークするために使います。
- 例:
#[test]
は関数をテスト対象として指定し、テストランナーによって自動的に実行されます。
#[macro_use]
- マクロを利用する際に、外部クレートからマクロをインポートするために使います。
- 例:
#[macro_use] extern crate serde_json;
で、serde_json
クレートのマクロを利用できます。
#[feature(...)]
- 実験的な機能を有効にするために使われます。安定版Rustで使用するには、
nightly
ビルドが必要です。 - 例:
#[feature(proc_macro)]
はプロシージャマクロ機能を有効にします。
- 実験的な機能を有効にするために使われます。安定版Rustで使用するには、
Debugトレイト
[編集]Debug
トレイトは、構造体や列挙型などのデータ構造をフォーマットして表示するために使われます。このトレイトを実装することで、デバッグ情報を簡単に出力することができます。
使用例
[編集]#[derive(Debug)] struct MyStruct { name: String, age: u32, } fn main() { let my_data = MyStruct { name: String::from("Alice"), age: 30, }; // Debugトレイトを使用して構造体を表示 println!("My data: {:?}", my_data); }
- 出力例
My data: MyStruct { name: "Alice", age: 30 }
このコード例では、#[derive(Debug)]
を使ってMyStruct
構造体にDebug
トレイトを実装し、println!
マクロで{:?}
フォーマット指定子を使って内容を表示しています。Debug
トレイトを手動で実装することなく、簡単にデバッグ情報を出力できます。
その他の属性
[編集]#[allow(dead_code)]
- 使用されていないコードに対する警告を無視します。特にデバッグコードや一時的な実装で便利です。
#[cfg_attr(...)]
- 条件付きで属性を適用するために使います。
#[cfg]
と組み合わせることで、特定の条件下で属性を追加できます。
- 条件付きで属性を適用するために使います。
#[must_use]
- 戻り値が使われなければコンパイラから警告を出します。戻り値を意図的に無視することを防ぎます。
Rustの属性は、コードの最適化やコンパイラの挙動、エラーチェックを制御するために強力なツールです。それぞれの属性は特定の目的を持っており、正しく使用することでコードの品質を向上させることができます。
ジェネリックス
[編集]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
関数でこれらのメソッドを使用しています。
応用的な使用例
[編集]- トレイトの実装
trait Printable { fn print(&self); } struct MyType { value: i32, } impl Printable for MyType { fn print(&self) { println!("Value: {}", self.value); } } fn main() { let obj = MyType { value: 10 }; obj.print(); }
- インターフェース
- 共通の振る舞い: インターフェースは、異なるクラスが共通の振る舞いを持つことを保証します。他の言語では、この振る舞いはインターフェースで定義され、クラスはそれを実装します。
- メソッドの宣言: インターフェースでは、クラスが実装しなければならないメソッドの宣言が含まれます。
- 多重継承の代替: インターフェースは多重継承の代替手段としても使われ、クラスは複数のインターフェースを実装することができます。
- プロトコル
- 抽象的な振る舞いの定義: プロトコルは、特定の振る舞いや機能を表す抽象的な規約です。Swiftなどの言語ではプロトコルが使われ、クラスや構造体はそれらを適合させます。
- メソッドの要求: プロトコルは、適合する型によって実装されるメソッドやプロパティの要求を定義します。
- 型の適合性の強化: プロトコルに適合することで、様々な型を同じ抽象的な概念に束縛することができます。
- Rustのトレイト
- 共通の振る舞いの提供: トレイトは、構造体や他の型に共通の振る舞いを提供します。それぞれの型はトレイトを実装することで、その振る舞いを持つことができます。
- メソッドの宣言: トレイトではメソッドの宣言が行われ、それを実装することでトレイトが利用できるようになります。
- 型間の相互運用性と柔軟性: トレイトは型間での相互運用性や柔軟性を提供し、異なる型が同じ振る舞いを共有することができます。
ジェネリックな実装
[編集]- ジェネリックな構造体に対する実装
struct MyGeneric<T> { value: T, } impl<T> MyGeneric<T> { fn new(value: T) -> MyGeneric<T> { MyGeneric { value } } fn get_value(&self) -> &T { &self.value } } fn main() { let instance = MyGeneric::new(42); println!("Value: {:?}", instance.get_value()); let instance_str = MyGeneric::new("hello"); println!("Value: {:?}", instance_str.get_value()); }
このコードは、MyGeneric
をi32
型と&str
型の両方でインスタンス化しています。ジェネリックなデータ構造体とそのジェネリックなメソッドを利用して、異なる型に対して同じメソッドを使用する様子を示しています。println!
マクロ内の{:?}
は、Debug
トレイトを実装する型の値を表示するためのフォーマット指定子です。
他の例
[編集]- 単純なトレイトの実装
trait Summary { fn summarize(&self) -> String; } struct Book { title: String, author: String, } impl Summary for Book { fn summarize(&self) -> String { format!("{} by {}", self.title, self.author) } } 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
トレイトを実装した様々な型に対して共通のsummary_any
関数を使用して、異なる型の値に対して要約を取得できます。
関数
[編集]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] { // jをi*iから始める for j in (i * i..=n).step_by(i) { sieve[j] = false; } } if i * i >= n { break; } } for i in 2..=n { if sieve[i] { println!("{}", 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().cloned().fold(ints[0], gcd2) } fn lcm2(m: i32, n: i32) -> i32 { m * n / gcd2(m, n) } fn lcm(ints: &[i32]) -> i32 { ints.iter().cloned().fold(ints[0], lcm2) } 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
を適用し、次の要素に対して再帰的に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 { // 空文字列の場合は "world" を使用 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.to_string()); // 直接to_stringを呼び出す } } fn main() { let hello1 = Hello::new(""); hello1.print(); // to_string()を呼ぶ必要がなくなる let hello2 = Hello::new("my friend"); hello2.print(); // 同上 println!( "Hello.constructor.name => Hello\nhello1 => {:?}\nhello2.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 { /// 地球の半径(キロメートル) const EARTH_RADIUS: f64 = 6371.008; /// 緯度・経度をラジアンに変換するための係数 const RADIAN_CONVERSION: f64 = PI / 180.0; /// 2つの地理座標間の距離を計算する fn distance(&self, other: &GeoCoord) -> f64 { let lat_i = self.latitude * Self::RADIAN_CONVERSION; let other_lat_i = other.latitude * Self::RADIAN_CONVERSION; let long_diff_i = (self.longitude - other.longitude) * Self::RADIAN_CONVERSION; 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) * Self::EARTH_RADIUS; distance } } impl std::fmt::Display for GeoCoord { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let (ew, ns, long, lat) = format_coordinates(self.longitude, self.latitude); write!(f, "({}: {:.6}, {}: {:.6})", ew, long, ns, lat) } } /// 緯度・経度の値を適切にフォーマットする関数 fn format_coordinates(longitude: f64, latitude: f64) -> (&'static str, &'static str, f64, f64) { let (ew, long) = if longitude < 0.0 { ("西経", -longitude) } else { ("東経", longitude) }; let (ns, lat) = if latitude < 0.0 { ("南緯", -latitude) } else { ("北緯", latitude) }; (ew, ns, long, 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); } for i in 0..sites.len() { let current_site = &sites[i]; let next_site = &sites[(i + 1) % sites.len()]; println!( "{} - {}: {:.2} [km]", current_site.0, next_site.0, current_site.1.distance(&next_site.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), // 数字はそのままRPNに追加 Token::Plus | Token::Minus | Token::Multiply | Token::Divide => { while let Some(&top) = stack.last() { if precedence(&token) <= precedence(&top) { rpn.push(stack.pop().unwrap()); // 優先順位が高い演算子を出力 } else { break; // 自分より優先順位が低い演算子が来たら中断 } } stack.push(token); // 演算子をスタックに追加 } } } // スタックに残った演算子を全てRPNに追加 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, Clone, PartialEq)] // Cloneトレイトを追加 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(); self.position += 1; // 次の位置に進める match current_char { '+' => Token::Plus, '-' => Token::Minus, '*' => Token::Multiply, '/' => Token::Divide, '(' => Token::LParen, ')' => Token::RParen, _ if current_char.is_digit(10) || current_char == '.' => { let start = self.position - 1; // トークンの開始位置 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]; Token::Number(number_str.parse::<f64>().unwrap()) } _ => 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 matches!(self.current_token, Token::Multiply | Token::Divide) { let op = self.current_token.clone(); self.eat(op.clone()); let mut next_factor = self.factor(); result.append(&mut next_factor); result.push(op); } result } // 式を解析する fn expr(&mut self) -> Vec<Token> { let mut result = self.term(); while matches!(self.current_token, Token::Plus | Token::Minus) { let op = self.current_token.clone(); self.eat(op.clone()); let mut next_term = self.term(); result.append(&mut next_term); result.push(op); } 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
の加算、減算、乗算、除算を行っています。結果を文字列として出力しています。
修正されたコードは次のようになります:
改廃された技術
[編集]Rustの改廃された技術や利用が推奨されない技術は、言語標準の進化、安全性の向上、エコシステムの成熟などによって置き換えられます。以下に、代表的な技術を示します。
try! マクロ
[編集]- サポート開始年: 2015年
- サポート終了年: 2018年(非推奨化)
- 廃止または衰退の理由
- より簡潔で表現力のある
?
演算子が導入され、エラーハンドリングがより簡単になりました。 - 代替技術
?
演算子の使用が推奨されます。
std::sync::Arc::get_mut_unchecked
[編集]- サポート開始年: 2015年
- サポート終了年: 2018年(非推奨化)
- 利用推奨されない理由
- 安全性の問題があり、データ競合を引き起こす可能性があります。
- 代替技術
get_mut()
またはmake_mut()
の使用が推奨されます。
std::env::home_dir
[編集]- 対象: ホームディレクトリのパス取得
- サポート終了年: 2019年(非推奨化)
- 廃止または衰退の理由
- プラットフォーム間での一貫性の問題と、エッジケースでの不正確な結果。
- 代替技術
dirs
クレートやhome
クレートの使用が推奨されます。
catch_unwind_safe
[編集]- サポート開始年: 2016年
- サポート終了年: 2018年(非推奨化)
- 廃止または衰退の理由
- 安全性の保証に関する問題と、より良い代替手段の存在。
- 代替技術
std::panic::catch_unwind
と適切なパニック安全性の設計が推奨されます。
std::error::Error::description
[編集]- サポート開始年: 2015年
- サポート終了年: 2019年(非推奨化)
- 廃止または衰退の理由
- エラーメッセージの国際化や詳細な文脈提供が困難でした。
- 代替技術
- Display実装とError::sourceメソッドの使用が推奨されます。
古いRaw Pointersの特定の使用法
[編集]- 対象: 特定の生ポインタ操作
- サポート終了年: 継続的に改善
- 廃止または衰退の理由
- メモリ安全性とプログラムの正確性に関する問題。
- 代替技術
- 参照、Box、Rc、Arcなどの安全な所有権型の使用が推奨されます。
std::thread::scoped
[編集]- サポート開始年: 2015年
- サポート終了年: 2015年(早期に削除)
- 廃止または衰退の理由
- メモリ安全性の問題が発見され、より安全な代替手段が必要とされました。
- 代替技術
crossbeam::scope
やrayon
のスコープ付き並列処理が推奨されます。
古いマクロ定義構文
[編集]- サポート開始年: 2015年
- サポート終了年: なし(ただし新構文推奨)
- 廃止または衰退の理由
- 可読性とデバッグの困難さ、より表現力のある新しい構文の登場。
- 代替技術
macro_rules!
の新しい構文やproc-macroの使用が推奨されます。
std::ascii::AsciiExt
[編集]- サポート開始年: 2015年
- サポート終了年: 2019年(非推奨化)
- 廃止または衰退の理由
- トレイトの設計が最適でなく、より良い代替手段が標準ライブラリに追加されました。
- 代替技術
- str、charの組み込みメソッドの使用が推奨されます。
古いFuture実装
[編集]- 対象: futures 0.1系
- サポート終了年: なし(ただし非推奨)
- 廃止または衰退の理由
- async/await構文の導入により、より簡潔で理解しやすい非同期プログラミングが可能になりました。
- 代替技術
- futures 0.3系とasync/await構文の使用が推奨されます。
ほかの言語からの移植例
[編集]順列・組合わせ
[編集]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日).