Rust の基礎知識
●Hello, Rust!
- Hello, Rust! と表示するプログラム (拡張子は .rs)
リスト : Hello, Rust! (hello.rs)
// コメント
fn main() {
println!("Hello, Rust!");
}
fn main() { ... } は main 関数
C/C++ と同じく、Rust は main 関数からプログラムの実行を開始する
Rust は // から行末までがコメントになる (行コメント)
println!() はC言語の関数 printf() と同様の機能を持つ
println!() は関数ではなく「マクロ」 (マクロはあとで説明する)
マクロは名前の末尾に ! を付ける
プログラムのコンパイルは rustc を使う
$ rustc hello.rs
$ ./hello
Hello, Rust!
rustc hello.rs で実行ファイルが生成される (Linux では hello, Windows では hello.exe)
rustc --help でヘルプを表示する
オプション -O を付けると最適化が行われる (-C opt-level=2 と同じ)
●Hello, Cargo!
- Rust でプロジェクトを作成するときはツール cargo を使う
- cargo はビルドツールで、パッケージの管理マネージャでもある
- cargo は Rust と一緒にインストールされる
$ cargo --version
cargo 1.62.1 (a748cf5a3 2022-06-08)
プロジェクトの作成はコマンド cargo new で行う
cargo new [--bin] path
--bin はバイナリ (実行ファイル)、--lib はライブラリ (Rust ではクレート) を作成する
デフォルトはバイナリの作成になる
path で指定したサブディレクトリにプロジェクトファイルが生成される
デフォルトで path がプロジェクト名になる
$ cargo new hello
Created binary (application) `hello` package
$ tree hello
hello
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
Cargo.toml はプロジェクト hello の設定ファイル
$ cat hello/Cargo.toml
[package]
name = "hello"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
main.rs がプロジェクト hello のルートのソースファイル
$ cat hello/src/main.rs
fn main() {
println!("Hello, world!");
}
コンパイルはコマンド cargo build で行う
デフォルトではデバッグ用のバイナリが生成される
バイナリ名はデフォルトでプロジェクト名と同じ
$ cd hello
$ cargo build
Compiling hello v0.1.0 (/home/mhiroi/rust/hello)
Finished dev [unoptimized + debuginfo] target(s) in 23.27s
$ ls target/debug
build deps examples hello hello.d incremental
$ target/debug/hello
Hello, world!
バイナリは target/debug に格納される
コマンド cargo run を使うと、コンパイルしたあとプログラムを実行する
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.19s
Running `target/debug/hello`
Hello, world!
--release (-r) を指定すると、リリース用のバイナリを生成する
$ cargo build --release
Compiling hello v0.1.0 (/home/mhiroi/rust/hello)
Finished release [optimized] target(s) in 4.49s
バイナリは target/release に格納される
$ ls target/release
build deps examples hello hello.d incremental
$ target/release/hello
Hello, world!
cargo run に--release を指定して実行することもできる
$ cargo run --release
Finished release [optimized] target(s) in 0.02s
Running `target/release/hello`
Hello, world!
●基本的なデータ型
- 整数
- 符号付き整数 (i8, i16, i32, i64, i128, isize)
- 無符号整数 (u8, u16, u32, u64, u128, usize)
- 数値はビットを表す
- isize, usize の大きさは処理系のポインタサイズに依存する
- 接頭辞に 0x、0o、0b を指定すると、16 進数、8 進数、2 進数になる
- 浮動小数点数
- 文字 (char, ユニコード), 'a', 'あ' など
- 真偽値 (bool), true, false
- タプル
- タプルは複数の要素を格納した immutable なデータ構造
- データ型は (T, U, V, ...), T, U, V, ... は要素のデータ型
- タプルは丸カッコ (item1, item2, ...) で生成する
- 要素が一つのタプルを生成するときは (item1,) のようにカンマを付ける
- 要素のアクセスは 変数名.添字 とする (添字は 0 から始まる)
- タプルはパターンマッチや関数で多値を返すときにも使える
- 配列
- Rust の配列は固定長
- 可変長配列は標準ライブラリの Vec<T> を使う
- 要素の型を T とすると、配列の型は [T; size] となる
- size は配列の大きさ (データ型は usize)
- 配列の宣言
- let 変数名: 配列の型 = [初期値; 大きさ];
- let 変数名: 配列の型 = [要素1, 要素2, ...];
- Rust には型推論があるので、左辺の配列の型は省略することができる
- Rust の場合、immutable で宣言された配列は要素の値を書き換えることができない
- 要素の値を書き換えたい場合は let mut で宣言する
- 要素のアクセスには角カッコ [ ] を使う
- 添字は 0 から始まる
- 配列の大きさは 変数名.len() で取得できる
- 二次元配列のデータ型は [[T; row]; column] となる
- 要素のアクセスは mat[x][y] のよう角カッコを 2 つ使う
- 多次元配列も同じ方法で定義できる
- ユニット (unit), () が唯一の値 (値が無いことを表す)
- 基本的なデータ型の場合、他の変数に代入するときや関数の引数に渡すとき値がコピーされる
●変数
- 関数型言語では変数に値を割り当てることを「束縛 (binding)」という
- Rust では「変数束縛」という
- 局所変数は let で宣言する
- 局所変数はスタック (システムスタックとかコールスタックと呼ばれる) に割り当てられる
let 名前: データ型; // 宣言だけする場合
let 名前 = 式;
Rust には型推論があるので、右辺式 (初期値) が指定されていれば、データ型を省略できる
let で宣言された変数は immutable なので、値を書き換えることはできない
let の後ろに mut を付けると mutable になるので、値を書き換えることができる
変数は使用する前に必ず初期化すること
let で宣言された変数の有効範囲 (スコープ) はブロック { ... } の中だけ
ブロックが入れ子の場合、内側のブロックで外側のブロックと同名の変数を宣言することができる
この場合、外側の変数は「隠蔽 (shadowing)」される (関数型言語と同じ)
定数は const で定義する
const 変数名: データ型 = 値;
大域変数 (グローバル変数) の定義は static で行う
static 変数名: データ型 = 値;
static mut 変数名: データ型 = 値;
const, static はデータ型を省略することはできない
右辺の値は定数式であること
static な変数は静的なデータ領域に割り当てられ、プログラムが終了するまで存在する
const, static はブロックの中でも定義できる
その場合、ブロックの外側からその変数を参照することはできない
mutable な大域変数のアクセスは安全ではないので unsafe { ... } の中で行う
●基本的な演算子
- 基本的な演算子はC言語とほぼ同じ
- 算術演算子 (+, -, *, /, %)
- 比較演算子 (==, !=, <, >, <=, >=)
- 論理演算子 (!, &&, ||)
- ビット演算子 (!, &, |, ^, <<, >>)
- ビットの反転は ! を使う (C言語の ~ と同じ)
- 代入演算子 (=, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=)
- インクリメント (++), デクリメント (--) は存在しない
リスト : 基本的なデータ型と演算子の使用例
fn main() {
let a = 10; // 通常の 10 進数は i32
let b = 20; // データ型の指定は 20i32 のように数字の後ろにつけてもよい
let c = 1.234; // 通常の小数点数は f64
let d = 5.678;
println!("{}", a + b); // 文字列中の {} は引数の値を文字列に変換して出力
println!("{}", a - b); // 書式指定は format! と Display を参照
println!("{}", c * d);
println!("{}", c / d);
println!("{}", true && false);
println!("{}", false || true);
println!("{}", !true);
let x = (100, 9.999, 'あ'); // タプル, 型は (u32, f64, char)
println!("{}", x.0);
println!("{}", x.1);
println!("{}", x.2);
}
30
-10
7.006652
0.2173300457907714
false
true
false
100
9.999
あ
●基本的な制御構造
- if test1 { then節; ... } else if test2 { then節2; ... } else { else節; ... }
- if の条件式は括弧で囲む必要はない
- Rust の if は式なので値を返すことができる
- ブロックの最後の処理でセミコロンを付けない場合、その結果を返す
- 最後の処理にセミコロンを付けるとユニットを返す
- 返り値のデータ型は同じでなければならない (関数型言語と同じ)
- while test { 処理; ... }
- for 変数 in expression { ... }
- expression はイテレータに変換可能なアイテムであること
- for はイテレータから要素を順番に取り出して変数にセットする
- 配列や範囲 (range) はイテレータに変換できる
- range の構文
- start .. end, start 以上 end 未満 (start <= x < end)
- start ..= end, start 以上 end 以下 (start <= x <= end)
- loop { ... } // 無限ループ
- 繰り返しの制御に break と continue が使える
- break と continue にはラベル (label) を渡すことができる
- 多重ループの内側から一気に脱出することもできる
リスト : FizzBuzz 問題
fn main() {
for n in 1..101 {
if n % 15 == 0 {
print!("FizzBuzz ");
} else if n % 3 == 0 {
print!("Fizz ");
} else if n % 5 == 0 {
print!("Buzz ");
} else {
print!("{} ", n);
}
}
}
$ ./fizzbuzz
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz Fizz 22 23 Fizz Buzz
26 Fizz 28 29 FizzBuzz 31 32 Fizz 34 Buzz Fizz 37 38 Fizz Buzz 41 Fizz 43 44 FizzBuzz 46 47 Fizz 49 Buzz
Fizz 52 53 Fizz Buzz 56 Fizz 58 59 FizzBuzz 61 62 Fizz 64 Buzz Fizz 67 68 Fizz Buzz 71 Fizz 73 74 FizzBuzz
76 77 Fizz 79 Buzz Fizz 82 83 Fizz Buzz 86 Fizz 88 89 FizzBuzz 91 92 Fizz 94 Buzz Fizz 97 98 Fizz Buzz
リスト : 数値積分 (円周率を求める)
fn main() {
let n = 1000000;
let w = 1.0 / n as f64; // n を f64 にキャスト (Rust は as を使う)
let mut s = 0.0;
for i in 1 .. n + 1 {
let x = (i as f64 - 0.5) * w;
s += 4.0 / (1.0 + x * x);
}
println!("{}", s * w);
}
3.1415926535897643
●所有権
- Rust は変数束縛するとき、変数がデータの「所有権」を持つ
- データの所有権を持つ変数は一つしかない
- 所有権を持つ変数が廃棄されるとき、そのデータもいっしょに廃棄される
- Rust は他の変数にデータを代入するとき、所有権が移動する
- これを「ムーブセマンティクス (Move Semantics)」という
- 基本的なデータ型の場合、所有権は移動せずに値がコピーされる
- 所有権が移動したあと、移動元の変数にアクセスするとコンパイルエラー
リスト : 所有権の移動
fn main() {
let a = vec![1,2,3,4,5]; // vec! はベクタを生成するマクロ
println!("{:?}", a); // {:?} は Debug 表示
let b = a; // a から b に move
println!("{:?}", b);
// println!("{:?}", a); コンパイルエラー
let c: Vec<i32>;
{
let d = b; // b から d に move
// println!("{:?}", b); コンパイルエラー
println!("{:?}", d);
c = d; // d から c に move
// move しないと d が廃棄されるときにベクタも廃棄される
}
println!("{:?}", c);
}
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
●参照と借用
- Rust には C++ と同様の「参照 (reference)」がある
- データ型 T とすると、immutable な参照は & を使う (データ型は &T)
- mutable の参照は &mut を使う (データ型は &mut T)
- データを参照しても「所有権」は移動しない
- Rust では所有権の「借用」という
- immutable な参照はいくつでも作ることができる
- mutable な参照は一つしか作ることができない
- このとき、参照元のデータをアクセスするとコンパイルエラー
- immutable と mutable な参照を同時に使うことはできない
- 参照外し (デリファレンス) は変数名の前に * を付ける
- Rust の場合、演算子は多重定義できるので、基本的な演算子は参照も受け付けるようだ
- マクロ println!() なども参照を受け付ける
- mutable な参照で値を書き換える場合は * を付けること
- 参照元のデータが廃棄されたあと、その参照が残っていてはいけない
- これをチェックするため Rust には「ライフタイム」という機能がある
- ライフタイムは難しいので、あとで 説明 する
リスト : 参照と借用
fn main() {
let a = 123;
let ra = &a;
let a1 = ra * 10;
println!("{}", ra);
println!("{}", a1);
let mut b = 456;
println!("{}", b);
{
let rb = &mut b; // let mut rb とするとワーニング (mut は不要)
// let rb1 = &b; コンパイルエラー
// println!("{}", b); コンパイルエラー
println!("{}", rb);
*rb = 1000;
}
println!("{}", b);
}
1230
123
456
456
1000
●スライス
- スライスは配列への「参照」のこと
- 配列のスライスのデータ型は &[要素のデータ型] となる
- スライスは &配列名[範囲]、または &配列名 で取得する
- 範囲 (range) の構文
- start .. end, start 以上 end 未満 (start <= x < end)
- start ..= end, start 以上 end 以下 (start <= x <= end)
- start .. end は start から end - 1 までの要素がスライスの要素となる
- start ..= end は start から end までの要素がスライスの要素となる
- start を省略すると 0 から、end を省略すると配列の大きさになる
リスト : 配列とスライスの簡単な使用例
fn main() {
let a = [1,2,3,4,5,6,7,8];
println!("{}", a[0]);
println!("{}", a.len());
let a1 = a; // 基本的なデータ型は代入演算子でコピーされる
println!("{:?}", a);
println!("{:?}", a1);
let mut b = [0; 5];
b[0] = 10;
println!("{}", b[0]);
let mut b1 = b; // mutable な配列もコピーされる
b1[0] = 20;
println!("{:?}", b);
println!("{:?}", b1);
let c = &a[2..5]; // immutable のスライス (参照) はいくつでも作れる
// 配列自体はコピーされない
println!("{:?}", c);
println!("{}", c.len());
let d = &a[3..];
println!("{:?}", d);
println!("{}", d.len());
{
let e = &mut b[..]; // mutable な参照はひとつだけ
// let mut e はワーニング (mut は不要)
e[1] = 20;
println!("{}", e[1]);
// b[1] = 30; コンパイルエラー
}
b[1] = 30; // e が破棄されたので b にアクセスできる
println!("{}", b[1]);
}
1
8
[1, 2, 3, 4, 5, 6, 7, 8]
[1, 2, 3, 4, 5, 6, 7, 8]
10
[10, 0, 0, 0, 0]
[20, 0, 0, 0, 0]
[3, 4, 5]
3
[4, 5, 6, 7, 8]
5
20
30
●文字列
- Rust の文字列は utf-8 のバイトシーケンス
- 文字列リテラルは " で囲む
- データ型は &str になる (正確には &'static str)
- これを「文字列スライス」という
- 文字列スライスは immutable で固定長
- mutable で可変長な文字列には String がある
- 文字列スライスはメソッド to_string() で String に変換できる
- String はメソッド push_str() で文字列を追加することができる
- 文字の追加は push() を使う
- 末尾から文字を取り除くには pop() を使う
- 演算子 + で String と文字列スライスを連結することができる
- この場合、新しい String が生成される
リスト : 文字列の簡単な使用例
fn main() {
let mut a = "hello, ".to_string();
println!("{}", a);
a.push_str("world");
println!("{}", a);
let b = a + ", oops!";
println!("{}", b);
}
hello,
hello, world
hello, world, oops!
●関数
fn 関数名(仮引数: データ型, ...) -> データ型 { ... }
Rust の場合、関数の仮引数は immutable
- 仮引数の前に mut を付けると mutable になる (let と同じ)
仮引数のデータ型は省略することができない
返り値のデータ型は -> の後ろに記述する
返り値がユニットの場合はデータ型を省略できる
関数はブロック最後の処理結果を返す
このときセミコロンを付けてはいけない
処理の途中で値を返すときは return を使う
局所関数も定義できる
関数のデータ型は fn(データ型, ...) -> データ型 で表す
クロージャ (ラムダ式) もあるが、ジェネリクスと関連するので、あとで 説明 する
リスト : 関数の簡単な使用例
// 階乗 (再帰)
fn fact(n: i64) -> i64 {
if n == 0 {
1
} else {
n * fact(n - 1)
}
}
// フィボナッチ数
fn fibo(n: i64) -> i64 {
// 末尾再帰 (最適化は保証されていないようだ)
fn fiboiter(n: i64, a: i64, b: i64) -> i64 {
if n == 0 {
a
} else {
fiboiter(n - 1, b, a + b)
}
}
fiboiter(n, 0, 1)
}
// 配列の合計値を求める
fn sum_of(func: fn(i32) -> i32, seq: &[i32]) -> i32 {
let mut acc: i32 = 0;
for i in 0 .. seq.len() {
acc += func(seq[i]);
}
acc
}
fn main() {
for n in 10 .. 20 {
println!("{}", fact(n));
}
for n in 40 .. 50 {
println!("{}", fibo(n));
}
// 局所関数
fn identity(x: i32) -> i32 { x }
fn square(x: i32) -> i32 { x * x }
fn cube(x: i32) -> i32 { x * x * x }
let seq: [i32; 10] = [1,2,3,4,5,6,7,8,9,10];
println!("{}", sum_of(identity, &seq));
println!("{}", sum_of(square, &seq));
println!("{}", sum_of(cube, &seq));
}
3628800
39916800
479001600
6227020800
87178291200
1307674368000
20922789888000
355687428096000
6402373705728000
121645100408832000
102334155
165580141
267914296
433494437
701408733
1134903170
1836311903
2971215073
4807526976
7778742049
55
385
3025
●パターンマッチ
- Rust は match を使ってパターンマッチを行うことができる
- Rust のパターンマッチは関数型言語のそれとよく似ている
match expression {
pattern1 => expression1,
pattern2 => expression2,
...
patternN => expressionN
}
match は式 expression の値とパターン (pattern1, ..., patternN) を順番に比較する
マッチングに成功したらその節を選択し、=> 以降の式を実行する
match は式なので expression1 から expressionN の結果は同じデータ型でなければならない
リスト : 階乗 (パターンマッチ版)
fn fact(n: i64) -> i64 {
match n {
0 => 1,
x => x * fact(x - 1)
}
}
パターンが定数の場合、同じ値とマッチングする
最初のパターンは 0 なので、n が 0 の場合にマッチングする
これは if n = 0 { 1 } と同じ処理になる
パターンが変数の場合はどんな値とでもマッチングする
変数はその値に束縛される
したがって、n が 0 以外の場合は 2 番目のパターンと一致し、変数 x の値は n になる
変数 x は n と同じ値なので、パターンにワイルドカード ( _ ) を使って次のように定義することもできる
_ => n * fact(n - 1)
複数のパターンにマッチさせたい場合は | を使う
リスト : フィボナッチ数 (二重再帰)
fn fibo(n: i64) -> i64 {
match n {
0 | 1 => n, // 0 と 1 にマッチする
_ => fibo(n - 1) + fibo(n - 2)
}
}
定数ではなく範囲で場合分けしたいときは「ガード (guard)」を使うと便利
pattern if 条件式 => expression,
pattern がマッチングしてかつ条件式が真を返すとき、その節が選択される
... は範囲を表すパターン
s ... e は s 以上 e 以下の値とマッチングする
マッチングの結果を変数に束縛するときは @ を使う
変数 @ pattern => expression,
@ はパターンの中でも使うことができる
タプルやあとで説明する構造体や列挙型もパターンとして使用できる
このとき、「分配束縛 (destructuring)」が行われる
Common Lisp の「分配」や JavaScript の「分割代入」と同様な機能
パターンマッチを使ってデータ構造の要素を取り出すことができる
パターンは let や for, 関数の仮引数でも使うことができる
let (a, b) = (1, 2); // a = 1, b = 2
let (c, d) = ((3, 4), 5); // c = (3, 4), d = 5
let (e, (f, g)) = (6, (7, 8)); // e = 6, f = 7, g = 8
図 : タプルの分配束縛の例
- パターンの中で参照を扱うには &, ref, ref mut を使う
- & は参照とマッチングする
- ref は immutable な参照を、ref mut は mutable な参照を生成する
リスト : &, ref , ref mut の使用例
fn main() {
let ref x = 100; // x の型は &i32, let x = &100; と同じ
match x {
&y => println!("{}", y) // 参照と参照のマッチング (y の型は i32)
}
match *x { // デリファレンスしてもよい
y => println!("{}", y)
}
let mut z = 200;
match z {
ref mut y => { // y の型は &mut i32
*y = 300; // 値を書き換える
println!("{}", y)
}
}
}
100
100
300
リスト : 簡単な連想リスト
// 探索
fn assoc(key: &str, data: &[(&str, i32)]) -> i32 {
for &(x, v) in data {
if x == key { return v; }
}
-1
}
fn main() {
let data = [("foo", 10), ("bar", 20), ("baz", 30)];
println!("{}", assoc("foo", &data));
println!("{}", assoc("bar", &data));
println!("{}", assoc("baz", &data));
println!("{}", assoc("oops", &data));
}
10
20
30
-1
- assoc() の for は、&(&str, i32) と &(x, v) をマッチングする
- (x, v) とすると、タプルの参照ではなくなるのでコンパイルエラー
- タプルの参照同士でマッチングし、タプルの要素と x, v がマッチングする
- その結果、x にはキーとなる文字列、v には整数値がセットされる
- 関数の仮引数でも同様にパターンマッチを使用できる
リスト : 関数の仮引数でパターンマッチを使用する
fn foo(&(a, b): &(i32, i32)) {
println!("{},{}", a, b);
}
fn main() {
let x = (1, 2);
foo(&x);
}
1,2
- あとで説明する「クロージャ」では仮引数のデータ型を推論してくれるので、パターンマッチを簡単に利用できる
●構造体
struct 名前 { フィールド名: データ型, ... }
構造体は新しいデータ型を作る (名前がデータ型になる)
値の生成は let 変数名 = 名前 { フィールド名: 初期値, ... };
フィールドのアクセスは 変数名.フィールド名 で行う
Rust の場合、構造体のフィールドは immutable
let mut とすると mutable になる
フィールド単位で mutable を指定する方法はない
構造体の初期化時に .. を使って他の構造体から値を取得することができる
struct Point3D { x: f64, y: f64, z: f64 }
let p1 = Point3D { x: 0.0, y: 1.0, z: 2.0 };
let p2 = Point3D { y: 2.0, .. p1 }; // x, z は p1 の値を使う
タプル構造体はタプルに名前を付けたもの
struct 名前(データ型, ...);
フィールド名はない
Unit-like 構造体はフィールドがない構造体
struct 名前;
構造体もパターンになる
struct Point {x: f64, y: f64}
let Point {x: a, y: b} = Point {x: 1.0, y: 2.0}; // a = 1.0, b = 2.0
リスト : 構造体の簡単な使用例
// 三次元の点
struct Point3D {
x: f64, y: f64, z: f64
}
// 距離を求める
fn distance(p1: &Point3D, p2: &Point3D) -> f64 {
let dx = p1.x - p2.x; // 参照の場合でも 変数名.フィールド名 でアクセスできる
let dy = p1.y - p2.y; // (*変数名).フィールド名 と同じ
let dz = p1.z - p2.z;
(dx * dx + dy * dy + dz * dz).sqrt()
}
fn main() {
let p1 = Point3D {x:0.0, y:0.0, z:0.0};
let p2 = Point3D {x:10.0, y:10.0, z:10.0};
println!("{}", distance(&p1, &p2));
}
17.320508075688775
●列挙型
enum 名前 { 構造体, ... }
enum は新しいデータ型を定義する (名前がデータ型になる)
enum の中で定義できるデータ型は構造体 (タプル構造体, Unit-like 構造体を含む) だけ
enum で定義したデータ型は、中で定義されたデータ型のどれか一つを表す (直和型)
関数型言語の代数的データ構造と似ている
リスト : 列挙型
enum Fruit { Apple, Orange, Grape }
Fruit というデータ型は、Apple, Orange, Grape というデータ型の集合を表す
要素のアクセスは Fruit::Apple のように :: を使う
この場合、C言語の enum のように数値を割り当てることができる
数値を指定しない場合は 0 から順番に割り当てられる
数値に変換するときはキャスト (as 演算子) を使用する
列挙型でデータ型を判別するときはパターンマッチを使うと便利
リスト : 列挙型の簡単な使用例
// 果物を表すデータ型
enum Fruit {
Apple, Banana, Grape, Orange
}
use Fruit::*; // これで Fruit:: を省略できる
// 果物の価格
fn get_price(fruit: &Fruit) -> i32 {
match *fruit {
Apple => 200,
Banana => 150,
Grape => 300,
Orange => 100
}
}
fn main() {
println!("{}", get_price(&Apple));
println!("{}", get_price(&Banana));
println!("{}", get_price(&Grape));
println!("{}", get_price(&Orange));
}
200
150
300
100
●メソッド
- メソッドはオブジェクトに関連付けられた関数
- Rust の場合、オブジェクトは Rust で扱うことができるデータのこと
- メソッドはキーワード impl で定義する
impl データ型名 {
fn メソッド名(self, 仮引数: データ型, ...) -> データ型 { ... }
...
}
メソッドの第 1 引数 self は特別で、self, &self, &mut self の 3 つがある
- self : オブジェクト自身 (所有権の移動がおこる)
- &self : オブジェクトへの immutable な参照
- &mut self : オブジェクトへの mutable な参照
- self は self: Self, &self は self: &Self, &mut self は self: &mut Self の糖衣構文
- Self は呼び出し元オブジェクトのデータ型を表す
メソッドは オブジェクト.メソッド名(...) で呼び出す
このとき、オブジェクトが self に渡される
第 1 引数が self 以外の場合は「静的メソッド」になる (他の言語でいうとクラスメソッド)
静的メソッドは データ型名::メソッド名(...) で呼び出す
リスト : メソッドの簡単な使用例
// 点
struct Point {
x: f64, y: f64
}
// メソッドの定義
impl Point {
fn new(x1: f64, y1: f64) -> Point {
Point {x: x1, y: y1}
}
fn distance(&self, p: &Point) -> f64 {
let dx = self.x - p.x;
let dy = self.y - p.y;
(dx * dx + dy * dy).sqrt()
}
}
fn main() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(10.0, 10.0);
let p3 = &p1;
println!("{}", p1.distance(&p2)); // distance には p1 の参照が渡される
println!("{}", p3.distance(&p2)); // 参照でも呼び出すことができる
}
14.142135623730951
14.142135623730951
●トレイトの基本
- Rust の「トレイト (trait)」はメソッドの仕様を定義したもの
- トレイトは「データ型の振る舞い (機能)」に名前を付けたものと考えることができる
- データ型 A, B がトレイト X を実装していたらならば、A, B を同じデータ型として扱うことができる
- これを「トレイトオブジェクト」という (あとで 説明 する)
- トレイトは「ジェネリクス」で境界条件を設定するときにも使用する (あとで 説明 する)
- トレイトはキーワード trait で定義する
trait 名前 {
fn メソッド名(仮引数: データ型, ...) -> データ型;
...
}
trait の中でメソッドの実装も定義できる (デフォルトメソッド)
メソッドの実装は impl で行う
impl トレイト名 for データ型名 {
fn メソッド名(仮引数: データ型, ...) -> データ型 { ... }
...
}
impl X for A { ... } とすると、データ型 A にトレイト X のメソッドを定義することになる
リスト : トレイトの簡単な使用例
// 距離を求める
trait Distance {
fn distance(&self, p: &Self) -> f64;
}
// 二次元の点
struct Point {
x: f64, y: f64
}
impl Point {
fn new(x1: f64, y1: f64) -> Point {
Point {x: x1, y: y1}
}
}
// Distance の実装
impl Distance for Point {
fn distance(&self, p: &Point) -> f64 {
let dx = self.x - p.x;
let dy = self.y - p.y;
(dx * dx + dy * dy).sqrt()
}
}
// 三次元の点
struct Point3D {
x: f64, y: f64, z: f64
}
impl Point3D {
fn new(x1: f64, y1: f64, z1: f64) -> Point3D {
Point3D { x: x1, y: y1, z: z1 }
}
}
// Distance の実装
impl Distance for Point3D {
fn distance(&self, p: &Point3D) -> f64 {
let dx = self.x - p.x;
let dy = self.y - p.y;
let dz = self.z - p.z;
(dx * dx + dy * dy + dz * dz).sqrt()
}
}
fn main() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(10.0, 10.0);
let p3 = Point3D::new(0.0, 0.0, 0.0);
let p4 = Point3D::new(10.0, 10.0, 10.0);
println!("{}", p1.distance(&p2));
println!("{}", p3.distance(&p4));
}
14.142135623730951
17.320508075688775
●トレイトの継承
trait A : B { ... }
トレイト A を実装するとき、トレイト B も実装しないとコンパイルエラーになる
たとえば、struct C にトレイト A を実装する場合、impl A for C { ... } だけではなく、impl B for C { ... } も必要になる
リスト : トレイトの継承
trait Foo {
fn method_a(&self);
}
trait Bar : Foo {
fn method_b(&self);
}
struct Baz;
impl Foo for Baz {
fn method_a(&self) {
println!("method_a!");
}
}
impl Bar for Baz {
fn method_b(&self) {
println!("method_b!");
}
}
fn main() {
let a = Baz;
a.method_a();
a.method_b();
}
method_a!
method_b!
●Derive
- Rust は #[derive()] というアトリビュートを使って、特定のトレイトの標準的な機能を実装することができる
- カッコの中にトレイトを指定する
- Derive できるトレイト
- Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd
- Debug トレイトを実装すると書式文字列 {:?} で値を表示することができる
- PartialEq トレイトを実装すると演算子 ==, != で等値を判定することができる
- Copy トレイトを実装すると、変数に代入するとき値がコピーされる (所有権の移動は行われない)
- これらのトレイトを自分で実装することもできる
リスト : Derive の簡単な使用例
// 距離を求める
trait Distance {
fn distance(&self, p: &Self) -> f64;
}
#[derive(Debug)]
struct Point {
x: f64, y: f64
}
impl Point {
fn new(x1: f64, y1: f64) -> Point {
Point {x: x1, y: y1}
}
}
// Distance の実装
impl Distance for Point {
fn distance(&self, p: &Point) -> f64 {
let dx = self.x - p.x;
let dy = self.y - p.y;
(dx * dx + dy * dy).sqrt()
}
}
#[derive(Debug, PartialEq)]
struct Point3D {
x: f64, y: f64, z: f64
}
impl Point3D {
fn new(x1: f64, y1: f64, z1: f64) -> Point3D {
Point3D { x: x1, y: y1, z: z1 }
}
}
// Distance の実装
impl Distance for Point3D {
fn distance(&self, p: &Point3D) -> f64 {
let dx = self.x - p.x;
let dy = self.y - p.y;
let dz = self.z - p.z;
(dx * dx + dy * dy + dz * dz).sqrt()
}
}
fn main() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(10.0, 10.0);
let p3 = Point3D::new(0.0, 0.0, 0.0);
let p4 = Point3D::new(10.0, 10.0, 10.0);
println!("{:?}", p1);
println!("{:?}", p2);
println!("{:?}", p3);
println!("{:?}", p4);
println!("{}", p3 == p4);
println!("{}", p3 != p4);
println!("{}", p4 == p4);
println!("{}", p4 != p4);
println!("{}", p1.distance(&p2));
println!("{}", p3.distance(&p4));
}
Point { x: 0.0, y: 0.0 }
Point { x: 10.0, y: 10.0 }
Point3D { x: 0.0, y: 0.0, z: 0.0 }
Point3D { x: 10.0, y: 10.0, z: 10.0 }
false
true
true
false
14.142135623730951
17.320508075688775
●ジェネリクスの基本
- ジェネリクス (generics) はデータ型をパラメータ化する機能のこと
- 型パラメータ (型引数) は <T, U, V, ...> のように < > の中で指定する
- ジェネリクスを使って関数、メソッド、構造体、列挙型、トレイトを定義できる
- fn 名前<T, ...>(仮引数, ...) -> データ型 { ... }
- struct 名前<T, ...> { ... }
- enum 名前<T, ...> { ... }
- trait 名前<T, ...> { ... }
- impl <T, ...> 名前<T, ...> { ... }
- impl <T, ...> トレイト名<T, ...> for データ型名<T, ...> { ... }
- 構造体や列挙型のデータ型は 名前<データ型, ...> になる
- 簡単な例題として、関数型言語でよく用いられる Option というデータ型を取り上げる
- Rust では次のように定義されている
enum Option<T> { None, Some(T) }
Option はデータの有無を表す型で、データが無い場合は None を、データが有る場合は Some を使う
Some のデータ型は T なので、どのデータ型でも Option に格納することができる
データを取り出すときはパターンマッチングを使うと簡単
リスト : ジェネリクスの簡単な使用例
// 配列からデータを探す
fn find_if<T>(pred: fn(&T) -> bool, table: &[T]) -> Option<&T> {
for x in table {
if pred(x) { return Some(x); }
}
None
}
fn main() {
fn evenp(x: &i32) -> bool { x % 2 == 0 }
fn oddp(x: &i32) -> bool { x % 2 != 0 }
let a = [2, 4, 6, 8, 10];
match find_if(evenp, &a) {
Some(v) => println!("{}", v),
None => println!("None")
}
match find_if(oddp, &a) {
Some(v) => println!("{}", v),
None => println!("None")
}
}
2
None
- match 式の代わりに if let を使うことができる
if let パターン = 式 { ... } else { ... }
パターンマッチに成功したら then 節を、失敗したら else 節を実行する
Option と類似のデータ型に Result がある
enum Result<T, E> { Ok(T), Err(E) }
Result は計算結果を返すために使用する
エラーが発生した場合は、Err にエラー情報をセットして返す
リスト : ジェネリクスの簡単な使用例 (2)
// 組
#[derive(Debug, PartialEq)]
struct Pair<T, U> {
fst: T, snd: U
}
impl <T, U> Pair<T, U> {
fn new(a: T, b: U) -> Pair<T, U> {
Pair { fst: a, snd: b }
}
}
fn main() {
let p1 = Pair::new(1, 2.0);
let p2 = Pair::new(1, 3.0);
let p3 = Pair::new("foo", 100);
println!("{:?}", p1);
println!("{:?}", p2);
println!("{:?}", p3);
println!("{}", p1 == p2);
println!("{}", p3 == p3);
}
Pair { fst: 1, snd: 2 }
Pair { fst: 1, snd: 3 }
Pair { fst: "foo", snd: 100 }
false
true
●型パラメータの境界
- ジェネリクスの型パラメータには「境界」を設定することができる
- Rust では「トレイト境界」とか「ジェネリック境界」と呼ばれているようだ
- <T: U> とした場合、T はトレイト U を実装しているデータ型に制限される
- 複数のトレイトを境界に指定したい場合は <T: U + V> のように + でつなげる
- 境界は where 句 (where T: U, ...) を使って設定することもできる
- where 句は本体 {...} の直前に挿入する
リスト : 境界条件の設定
//
// Distance, Point, Point3D の定義は省略
//
// 2点間の距離を表示する
// T は Distance を実装しているデータ型に限定される
fn print_distance<T: Distance>(p1: &T, p2: &T) {
println!("{:.8}", p1.distance(p2));
}
fn main() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(10.0, 10.0);
let p3 = Point3D::new(0.0, 0.0, 0.0);
let p4 = Point3D::new(10.0, 10.0, 10.0);
print_distance(&p1, &p2);
print_distance(&p3, &p4);
}
14.14213562
17.32050808
リスト : 配列と連想リストの探索
// 配列の探索
fn find<T: PartialEq + Copy>(key: T, data: &[T]) -> bool {
for &x in data {
if key == x { return true; }
}
false
}
// 連想リストの探索
fn assoc<T: PartialEq + Copy, U>(key: T, data: &[(T, U)]) -> Option<&U> {
for &(x, ref v) in data {
if key == x { return Some(v); }
}
None
}
fn main() {
let data = [1,2,3,4,5,6,7,8];
println!("{}", find(8, &data));
println!("{}", find(0, &data));
let data1 = ["foo", "bar", "baz"];
println!("{}", find("baz", &data1));
println!("{}", find("oops", &data1));
let data2 = [("foo", 100), ("bar", 200), ("baz", 300)];
match assoc("baz", &data2) {
Some(v) => println!("{}", v),
None => println!("None")
}
match assoc("oops", &data2) {
Some(v) => println!("{}", v),
None => println!("None")
}
}
true
false
true
false
300
None
初版 2017 年 7 月
改訂 2022 年 7 月 30 日