動的なやつら
語弊注意: この章の説明は特に語弊が激しい.ここで説明する内容は,正確には「所有権」「ムーブセマンティクス」「借用」と呼ばれる Rust の中核概念を極限まで簡略化したもの.正確な理解は The Book の第 4 章 を参照.
「動的なやつ」とは
Rust の型には 2 種類ある(語弊あり):
| 種類 | 例 | 特徴 |
|---|---|---|
| サイズが決まってるやつ | i32, f64, bool, char |
コピーできる.何も考えなくていい |
| サイズが動的なやつ | String, Vec<T> |
なんか特殊なルールがある |
i32 や f64 は常に 4 バイトとか 8 バイトとか,サイズが決まっている.こいつらは代入したり関数に渡したりしても自動でコピーされる:
fn main() {
let a = 42;
let b = a; // コピーされる
println!("{}", a); // OK! a はまだ使える
}
String と Vec は特殊
String や Vec は中身の長さが変わる.こいつらには特殊なルールがある:
fn main() {
let a = String::from("hello");
let b = a; // a の中身が b に「移動」する
// println!("{}", a); // コンパイルエラー!a はもう使えない
println!("{}", b); // OK
}
なんで? — 今は深く考えなくていい.「動的なやつは一度渡すと元から消える」とだけ覚えておこう.
関数に渡しても同じ:
fn print_it(s: String) {
println!("{}", s);
}
fn main() {
let name = String::from("Alice");
print_it(name);
// println!("{}", name); // エラー!name はもう使えない
}
解決策: clone
困ったら .clone() すればいい.コピーが作られるので元も使える:
fn main() {
let a = String::from("hello");
let b = a.clone(); // コピーを作る
println!("{}", a); // OK!
println!("{}", b); // OK!
}
fn print_it(s: String) {
println!("{}", s);
}
fn main() {
let name = String::from("Alice");
print_it(name.clone()); // コピーを渡す
println!("{}", name); // OK!
}
「
.clone()ってメモリの無駄遣いでは?」— そう.でも今は気にしない.動けば正義.パフォーマンスが問題になったときに最適化すればいい.
Vec: 動的な配列
Vec は長さが変わる配列:
fn main() {
// 作り方いろいろ
let mut numbers: Vec<i32> = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);
// マクロで一発
let colors = vec!["red", "green", "blue"];
println!("{:?}", numbers); // [1, 2, 3]
println!("{:?}", colors); // ["red", "green", "blue"]
}
よく使う操作
fn main() {
let mut v = vec![10, 20, 30, 40, 50];
// 長さ
println!("len: {}", v.len()); // 5
// アクセス(インデックスは usize)
let i: usize = 0;
println!("first: {}", v[i]); // 10
// 追加
v.push(60);
// 最後を削除
v.pop();
// ループ
for item in v.clone() {
println!("{}", item);
}
// まだ v を使える(clone したから)
println!("{:?}", v);
}
for item in vとするとvが消費されて以降使えなくなる.v.clone()を渡せば OK.
イテレータメソッド
Vec にはループの代わりに使えるメソッドがたくさんある.他の言語で配列の map や filter を使ったことがあるなら馴染みがあるはず.
まず前提として,クロージャ(無名関数)の書き方を知っておく必要がある:
|x| x + 1 // 引数 x を受け取って x + 1 を返す
|x, y| x + y // 引数 2 つ
|x| { x * 2 } // 中括弧で囲んでもいい
|x| x % 2 == 0 // bool を返すのもOK
語弊注意: クロージャには「環境をキャプチャする」という重要な性質があるけど,今は「その場で書ける短い関数」くらいの理解で OK.
map: 各要素を変換する
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers
.iter()
.map(|x| x * 2)
.collect();
println!("{:?}", doubled); // [2, 4, 6, 8, 10]
}
.iter() で各要素を順に取り出し,.map() で変換し,.collect() で Vec に戻す.この 3 点セットが基本パターン.
.iter()は「要素をちょっと覗くだけ」のイテレータを作る.元の Vec は消費されないので clone 不要.
filter: 条件に合う要素だけ残す
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<&i32> = numbers
.iter()
.filter(|x| *x % 2 == 0)
.collect();
println!("{:?}", evens); // [2, 4, 6]
}
filterのクロージャは&&i32(参照の参照)を受け取る.**xと書くか,|&&x|とパターンで受けるか,*xと書くか……正直ややこしい.コンパイラのエラーメッセージに従って*を足し引きすれば大体うまくいく.
map と filter のチェーン
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 偶数だけ取り出して 2 倍にする
let result: Vec<i32> = numbers
.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * 2)
.collect();
println!("{:?}", result); // [4, 8, 12, 16, 20]
}
メソッドチェーンで繋げていく.読みやすい.
enumerate: インデックス付きでループ
fn main() {
let fruits = vec!["apple", "banana", "cherry"];
for (i, fruit) in fruits.iter().enumerate() {
println!("{}: {}", i, fruit);
}
// 0: apple
// 1: banana
// 2: cherry
}
enumerate() は (usize, &要素) のタプルを返す.
find: 条件に合う最初の要素を探す
fn main() {
let numbers = vec![1, 3, 5, 8, 10, 13];
let first_even = numbers.iter().find(|x| *x % 2 == 0);
match first_even {
Some(n) => println!("found: {}", n), // found: 8
None => println!("not found"),
}
}
find は Option を返す.見つからなければ None.
any / all: 条件チェック
fn main() {
let numbers = vec![2, 4, 6, 8];
let has_odd = numbers.iter().any(|x| x % 2 != 0);
println!("has odd: {}", has_odd); // false
let all_positive = numbers.iter().all(|x| *x > 0);
println!("all positive: {}", all_positive); // true
}
sum / count
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let total: i32 = numbers.iter().sum();
println!("sum: {}", total); // 15
let how_many = numbers.iter().filter(|x| *x > 3).count();
println!("count > 3: {}", how_many); // 2
}
sum() は型注釈(: i32)が必要なことが多い.コンパイラが「何の型に合計するか分からない」と言ってきたら型を書こう.
まとめ: よく使うイテレータメソッド
| メソッド | やること | 返り値 |
|---|---|---|
.iter() |
イテレータを作る(元を消費しない) | Iterator |
.map(|x| ...) |
各要素を変換 | Iterator |
.filter(|x| ...) |
条件に合う要素だけ残す | Iterator |
.collect() |
イテレータを Vec 等に変換 | Vec など |
.enumerate() |
インデックスを付ける | Iterator |
.find(|x| ...) |
最初に条件を満たす要素 | Option |
.any(|x| ...) |
条件を満たす要素があるか | bool |
.all(|x| ...) |
全要素が条件を満たすか | bool |
.sum() |
合計 | 数値型 |
.count() |
要素数を数える | usize |
for ループで書いても同じことはできるけど,イテレータメソッドで書くと意図が明確になる.Rust では頻繁に使うので慣れておこう.
Vec と String も「動的なやつ」
fn main() {
let a = vec![1, 2, 3];
let b = a;
// println!("{:?}", a); // エラー!
println!("{:?}", b); // OK
}
String と同じルール.clone で解決.
HashMap: キーと値のペア
おまけ.他の言語の辞書やマップに相当:
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Alice"), 100);
scores.insert(String::from("Bob"), 85);
// 取得
match scores.get("Alice") {
Some(score) => println!("Alice: {}", score),
None => println!("not found"),
}
// ループ
for (name, score) in scores.clone() {
println!("{}: {}", name, score);
}
}
use std::collections::HashMap; を先頭に書くのを忘れずに.get は Option を返す — これは次の章で説明する.
まとめ
i32,f64,bool→ コピーされる.気にしなくていいString,Vec,HashMap→ 渡すと消える.困ったら.clone()- 深い理由は今は考えない
次の章で Option と Result を学べば,Rust の基礎は完了.いよいよ電卓を作れるようになる.