rustを学んで⑤
Rustの学習記録:テスト、イテレータ、ミニgrepアプリで学んだこと
今日はRustのテスト機能とミニgrepアプリの実装を通じて、多くの重要な概念を学びました。躓いた点を中心に、詳しく振り返ります。
論理演算子の罠:OR と AND の使い分け
テストコードを書いている時に、範囲外の値を弾く条件で大きくハマりました。
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess { value }
}
}
最初、この条件を if value < 1 && value > 100 と書いてしまいそうになりました。
なぜ || (OR) が正しいのか?
// ✅ 正しい
if value < 1 || value > 100 {
panic!("範囲外!");
}
これは「valueが1未満 または 100より大きい」という意味です。
- value = 0 →
0 < 1が真 → panic! - value = 200 →
200 > 100が真 → panic! - value = 50 → どちらも偽 → OK(panicしない)
もし && (AND) を使うと...
// ❌ 間違い
if value < 1 && value > 100 {
panic!("範囲外!");
}
これは「valueが1未満 かつ 100より大きい」という意味になります。
数値が1未満かつ100より大きいことは数学的に不可能なので、この条件は常に false です。panicは絶対に起きません。
覚え方
日本語で考えると分かりやすいです:
- 「1以上かつ100以下」→
value >= 1 && value <= 100(範囲内) - 「1未満または100超過」→
value < 1 || value > 100(範囲外)
有効な範囲を考える方が直感的なので、まず value >= 1 && value <= 100 を考えて、それを否定すると value < 1 || value > 100 になります(ド・モルガンの法則)。
まだぱっと見で理解できるほど親しみがないので、何度も書いて体に染み込ませる必要がありますね。
env::args() の仕組み:なぜ0番目から始まらないのか?
ミニgrepアプリを作っている時の疑問:
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1]; // なぜ0じゃなくて1?
let filename = &args[2]; // なぜ1じゃなくて2?
}
答え:0番目にはプログラム名が入っている
$ cargo run searchstring example-filename.txt
実行すると args の中身はこうなります:
["target/debug/プログラム名", "searchstring", "example-filename.txt"]
↑ args[0] ↑ args[1] ↑ args[2]
args[0]= プログラム自体のパス(実行ファイル名)args[1]= 最初のコマンドライン引数(検索文字列)args[2]= 2番目のコマンドライン引数(ファイル名)
これは誰の仕業?
env::args() の仕様です。
この関数がOSからコマンドライン引数を取得する際、0番目にプログラム名を含めるようになっています。Vec は関係なく、ただ env::args() から受け取ったものをそのまま格納しているだけです。
なぜこういう仕様なのか?
これはC言語の伝統から来ています:
// C言語の main 関数
int main(int argc, char *argv[]) {
// argv[0] は常にプログラム名
// argv[1] 以降が実際の引数
}
UNIX系のOSでは、プログラム起動時に「自分自身のパス」を argv[0] として渡すのが標準的な動作です。Rustの env::args() もこの慣習に従っています。
Node.jsとの比較
Node.jsにも process.argv という似たものがあります:
// node script.js search example.txt
console.log(process.argv);
// [
// '/path/to/node', // argv[0]: Node.js実行ファイルのパス
// '/path/to/script.js', // argv[1]: スクリプトファイルのパス
// 'search', // argv[2]: 最初の引数
// 'example.txt' // argv[3]: 2番目の引数
// ]
| 言語 | 0番目 | 1番目 | 実際の引数開始位置 |
|---|---|---|---|
| Rust | プログラム名 | 1番目の引数 | args[1] |
| Node.js | node実行ファイル | スクリプト名 | argv[2] |
Node.jsは「インタプリタ」と「スクリプト」の両方が入るので、実際の引数は [2] からですね。
テスト文字列のインデント問題
テストを書いていて、思わぬところでハマりました:
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
テストが失敗!
assertion `left == right` failed
left: ["safe, fast, productive."]
right: [" safe, fast, productive."]
^^^^^^^^ 余分なスペース8個!
原因:リントツールが自動整形
このインデントは手動で入れたものではなく、rustfmt や cargo fmt が自動的に追加したものでした。
フォーマッタがコードのインデントに合わせて、文字列リテラルの中身まで整形してしまったんです。
解決策1:インデントを削除
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
バックスラッシュ \ を使いつつ、インデントを入れない方法。見た目は悪いですが正確です。
解決策2:バックスラッシュなし(おすすめ)
let contents = "Rust:
safe, fast, productive.
Pick three.";
この書き方ならフォーマッタが触りません。リントツールと戦わず、ツールに合わせた書き方をするのが楽ですね。
解決策3:indoc クレート
大きなプロジェクトでは indoc クレートを使う方法もあります:
use indoc::indoc;
let contents = indoc! {"
Rust:
safe, fast, productive.
Pick three.
"};
// indocが自動で共通のインデントを削除してくれる
クロージャ:Rustのアロー関数
3つの関数定義が出てきて混乱しました:
fn add_one_v1(x: u32) -> u32 {
x + 1
}
fn main() {
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| x + 1;
println!("add one result v1: {}", add_one_v1(1));
println!("add one result v2: {}", add_one_v2(1));
println!("add one result v3: {}", add_one_v3(1));
}
これは何?
これはクロージャ(closure)で、3つとも全く同じことをする関数です。
add_one_v1 - 通常の関数
fn add_one_v1(x: u32) -> u32 {
x + 1
}
普通の関数定義。
add_one_v2 - フルスペックのクロージャ
let add_one_v2 = |x: u32| -> u32 { x + 1 };
// ^^^^^^^^^^^^^^^^^^^^^^^^
// クロージャ(無名関数)
|x: u32|- 引数(パイプ|で囲む)-> u32- 戻り値の型{ x + 1 }- 処理内容
変数に関数を代入している形です。
add_one_v3 - 省略形のクロージャ
let add_one_v3 = |x| x + 1;
Rustが型を推論してくれるので、型注釈を省略できます。
JavaScriptで例えると
// 通常の関数
function addOneV1(x) {
return x + 1;
}
// アロー関数(フル)
const addOneV2 = (x) => { return x + 1; };
// アロー関数(省略)
const addOneV3 = x => x + 1;
うっすらアロー関数かなとは思っていましたが、やはりそうでした。
JavaScriptとの違い:self は使える?
JavaScriptではアロー関数内で this が使えませんが、Rustのクロージャでは self は普通に使えます!
struct Counter {
count: u32,
}
impl Counter {
fn make_incrementer(&mut self) -> impl FnMut() {
|| {
self.count += 1; // self が使える!
}
}
}
なぜ違うのか:
- JavaScript:
thisは呼び出され方で決まる(動的バインディング)。アロー関数はthisを持たず、外側のスコープを参照。 - Rust:
selfは所有権システムで管理される。クロージャは環境をキャプチャする仕組みで、selfも他の変数と同じようにキャプチャできる。
イテレータと所有権:元のデータは残る?
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
疑問:v1 はまだ使えるの?
答え:はい、残ります
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
println!("v1: {:?}", v1); // v1: [1, 2, 3] ← まだ使える!
println!("v2: {:?}", v2); // v2: [2, 3, 4]
なぜ v1 が残るのか?
v1.iter()
// ^^^^^^ 不変借用のイテレータを作る
iter() は参照 &i32 を返すイテレータなので、所有権は移動しません。
イテレータの種類による違い
let v1 = vec![1, 2, 3];
// 1. iter() - 不変借用 (&T)
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
println!("{:?}", v1); // OK!v1 はまだ使える
// 2. iter_mut() - 可変借用 (&mut T)
let mut v1 = vec![1, 2, 3];
v1.iter_mut().for_each(|x| *x += 1);
println!("{:?}", v1); // OK!v1 は [2, 3, 4] に変更された
// 3. into_iter() - 所有権移動 (T)
let v1 = vec![1, 2, 3];
let v2: Vec<_> = v1.into_iter().map(|x| x + 1).collect();
// println!("{:?}", v1); // エラー!v1 はもう使えない
| メソッド | 要素の型 | 元のコレクション |
|---|---|---|
iter() | &T | 残る ✅ |
iter_mut() | &mut T | 残る(変更可能)✅ |
into_iter() | T | 消える ❌ |
重要な理解
基本的に mut をつけることで色々変更できるけど、そうでない場合は参照だったり、変更できなかったりします。参照の場合には clone() や新しいものを作成する(map + collect など)ことが必要です。
何度も何度も間違えて覚えないと馴染まないですね。
ミニgrepアプリの深掘り
ここからは、イテレータでリファクタされたミニgrepアプリを深掘りしていきます。
全体の流れ
1. コマンドライン引数を受け取る (main.rs)
2. 設定を作る (Config)
3. ファイルを読む (run関数)
4. 検索する (search関数)
5. 結果を表示
main.rs の解説
use std::env;
use std::process;
use playground::Config;
fn main() {
let args = env::args();
// env::args() は Iterator<Item = String> を返す
// まだ何も実行されていない(遅延評価)
let config = Config::new(args).unwrap_or_else(|err: &str| {
println!("Problem parsing arguments: {}", err);
process::exit(1);
});
// Config::new に args イテレータを渡す
// 成功したら Config、失敗したらエラーメッセージ表示して終了
if let Err(err) = playground::run(config) {
println!("Application error: {}", err);
process::exit(1);
};
}
Config::new の仕組み
impl Config {
pub fn new(mut args: impl Iterator<Item = String>) -> Result<Self, &'static str> {
args.next();
let query = match args.next() {
Some(query) => query,
None => return Err("Didn't get a query string"),
};
let filename = match args.next() {
Some(filename) => filename,
None => return Err("Didn't get a file name"),
};
Ok(Self { query, filename })
}
}
なぜ mut args が必要?
pub fn new(mut args: impl Iterator<Item = String>) -> ...
// ^^^
イテレータは next() で進めると状態が変わるので、mut が必要です。
ステップバイステップで理解
実行: cargo run frog poem.txt
イテレータの中身:
["target/debug/playground", "frog", "poem.txt"]
1回目の next():
args.next();
// → "target/debug/playground" を取り出して捨てる
// イテレータは ["frog", "poem.txt"] になる
2回目の next():
let query = match args.next() {
Some(query) => query, // → "frog" を取得
None => return Err("Didn't get a query string"),
};
// イテレータは ["poem.txt"] になる
3回目の next():
let filename = match args.next() {
Some(filename) => filename, // → "poem.txt" を取得
None => return Err("Didn't get a file name"),
};
// イテレータは [] (空)になる
イテレータの next() は:
- 要素があれば
Some(値)を返す - 要素がなければ
Noneを返す
Config と config の混乱
pub struct Config { // ← 型名(大文字)
pub query: String,
pub filename: String,
}
pub fn run(config: Config) -> ... {
// ^^^^^^ ^^^^^^
// 変数名 型名
// 小文字 大文字
大文字小文字だけの違いで、どっちがどっちか分からなくなってしまいました。
Rustの命名規則
// 型 = 大文字で始まる(PascalCase)
struct Config { ... }
struct User { ... }
enum Result { ... }
// 変数・関数 = 小文字で始まる(snake_case)
let config = ...
let user_name = ...
fn search(...) { ... }
前提知識(プログラミングの概念)よりも、Rust特有のルールがまだ定着していない感じですね。慣習的には型名と同じ小文字の変数名を使うことが多く、慣れると「あ、これは変数だな」ってすぐ分かるようになるはずです。
標準ライブラリ (std) の役割
ミニgrepアプリで使った std の各モジュールについて:
use std::io::prelude::*;
use std::{error::Error, fs::File};
use std::env;
use std::process;
1. std::io::prelude::*
use std::io::prelude::*;
// ^^ ^^^^^^^
// io よく使うものをまとめたモジュール
prelude は「前奏曲」という意味で、よく使うトレイトをまとめて import するための便利モジュールです。
ここでは特に:
file.read_to_string(&mut contents)?;
// ^^^^^^^^^^^^^^ このメソッドを使うために必要
Read トレイトが提供する read_to_string メソッドを使えるようにしています。
2. std::error::Error
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
// ^^^^^^^^^^^^^^
// どんなエラーでも入る箱
Box<dyn Error> はどんな種類のエラーでも返せる型です。
Box= ヒープに確保された箱(サイズが分からないものを入れる)dyn Error= Error トレイトを実装した何か(動的ディスパッチ)
let mut file = File::open(config.filename)?;
// ^ エラーが起きたら自動で return
file.read_to_string(&mut contents)?;
// ^ これもエラーが起きたら return
? は「エラーなら早期リターン、成功なら続ける」という演算子です。
3. std::fs::File
use std::fs::File;
// ^^ ^^^^
// fs ファイルシステム
ファイルを扱うための型です。
let mut file = File::open(config.filename)?;
// ^^^^^^^^^^^ ファイルを開く(読み取り専用)
// Result<File, Error> を返す
4. std::env
let args = env::args();
// ^^^^^^^^^^^ 環境変数やコマンドライン引数を扱う
env::args() はコマンドライン引数のイテレータを返します。
5. std::process
process::exit(1);
// ^^^^^^^^ プログラムを終了する
// 0 = 成功, 1以上 = エラー
プロセス(実行中のプログラム)を制御するモジュールです。
整理すると
// ファイル入出力
use std::io::prelude::*; // Read トレイトなど
use std::fs::File; // ファイル操作
// エラー処理
use std::error::Error; // エラー型
// プログラム制御
use std::env; // 環境・引数
use std::process; // プロセス制御
すごい量で、これだけでめちゃめちゃお腹いっぱいになりました。
実は全部を今理解する必要はなく、「こういうのがあるんだな〜」くらいで十分です。使いながら覚えていけば、必要になったときに「あ、これ見たことある!」ってなります。
run 関数の解説
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^
// 成功なら()、失敗ならエラー
let mut file = File::open(config.filename)?;
// ^^^ mut が必要(ファイルから読み込むと状態が変わる)
// ^ エラーなら即return
let mut contents = String::new();
// ^^^ これから文字を追加していくので mut
file.read_to_string(&mut contents)?;
// ^^^^^^^^^^^^^ 可変参照で渡す(中身を変更するため)
// ^ これもエラーなら即return
for line in search(&config.query, &contents) {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// search関数がVec<&str>を返す
println!("{}", line);
}
Ok(())
// () = "unit型" = 「何も返さない」という意味
// でもResultなので Ok で包む必要がある
}
File::open の動き
File::open は Result<File, Error> を返します:
- 成功 →
Fileが手に入る - 失敗 →
?が自動でreturn Err(...)してくれる
read_to_string の動き
read_to_string は:
- ファイルの中身を
contentsに追加する - 成功したら読んだバイト数を返す(今回は使ってない)
- 失敗したら
Errを返す
search 関数の解説
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
// ^^^ ^^ ^^
// ライフタイム(戻り値の参照が contents と同じ生存期間)
リファクタ前(コメントアウトされてる方)
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
- 空のVecを作る
- 各行をループ
- queryが含まれてたら追加
- Vecを返す
リファクタ後(イテレータ版)
contents
.lines() // 各行のイテレータ
.filter(|line| line.contains(query)) // 条件に合う行だけ
.collect() // Vecに集める
同じことを1行(チェーン)で表現しています。
詳しく見ると
contents.lines()
// "I'm nobody!\nAre you..." → ["I'm nobody!", "Are you...", ...]
// 各行を返すイテレータ
.filter(|line| line.contains(query))
// ^^^^^^ クロージャ(各lineに対して実行)
// true なら残す、false なら捨てる
.collect()
// イテレータの結果をVecに変換
// Vec<&str> になる
かなり複雑になってきていて、全然分かりませんでした。でも一つずつ分解していけば理解できました。
デリファレンス(*):参照を外す操作
最後にポインタについての質問です:
fn main() {
let a = vec![1, 2, 3];
let borrowed_a = &a;
let b = vec![1, 2, 3];
println!("equality: {}", *borrowed_a == b);
println!("a: {:?}, b: {:?}", a, b);
let mut moved_a = a;
let muttably_borrowed_a = &mut moved_a;
*muttably_borrowed_a = vec![1, 2, 3, 4];
println!("moved_a: {:?}", moved_a);
}
疑問:* で型を揃えているってこと?
答え:参照を外す(デリファレンス)操作
* は**「参照を外す(デリファレンス)」** という操作です。型を揃えるというより、参照の中身を取り出すイメージです。
1. *borrowed_a == b の場合
let a = vec![1, 2, 3];
let borrowed_a = &a;
// ^^ 参照(&Vec<i32>)
let b = vec![1, 2, 3];
// ^^^^^^^^^^^ 実体(Vec<i32>)
println!("equality: {}", *borrowed_a == b);
// ^^^^^^^^^^^
// &Vec を Vec に戻す
borrowed_aの型:&Vec<i32>(参照)bの型:Vec<i32>(実体)*borrowed_aで参照を外してVec<i32>にする- これで同じ型同士を比較できる
2. *muttably_borrowed_a = ... の場合
let mut moved_a = a;
let muttably_borrowed_a = &mut moved_a;
// ^^^^^^^^^^^^ 可変参照(&mut Vec<i32>)
*muttably_borrowed_a = vec![1, 2, 3, 4];
// ^^^^^^^^^^^^^^^^^^^
// 参照を外して、その中身を書き換える
* を付けることで:
&mut Vec<i32>→Vec<i32>に戻す- その中身に新しいVecを代入
* のイメージ
参照 = 「データがある場所を指す矢印」
borrowed_a -----→ [1, 2, 3]
& 実際のデータ
*borrowed_a = 矢印を辿って実際のデータを取り出す
型で見ると
let borrowed_a: &Vec<i32> = &a;
// ^^^^^^^^^^ この型
*borrowed_a // Vec<i32> になる(&が外れる)
// ^^^^^^^^^^ この型
「型を揃える」というより、「参照(ポインタ)の先にある実体にアクセスする」 という操作です。
まとめ
今日は本当に盛りだくさんでした:
重要ポイント
- 論理演算子: 無効範囲の除外は
||(OR)、有効範囲の確認は&&(AND) env::args(): 0番目にプログラム名が入るUNIX由来の仕様- イテレータ:
iter()は参照なので元のデータは残る。into_iter()は所有権移動で元は消える - クロージャ: JavaScriptのアロー関数に似ているが、Rustでは
selfも普通に使える - デリファレンス (
*): 参照を外して中身にアクセスする操作 - 命名規則: 型は
PascalCase、変数・関数はsnake_case ?演算子: エラーハンドリングを簡潔に書ける
実感したこと
- 前提知識よりもRust特有のルールがまだ定着していない
- 何度も何度も間違えて覚えないと馴染まない
- 一度に詰め込みすぎると頭がパンクする
でも、一つずつ分解して理解していけば、ちゃんと分かる。コンパイラが親切に教えてくれるので、エラーと付き合いながら自然に身につけていけそうです。
「あ、なんか自然に書けるようになってる」という瞬間を信じて、引き続きコツコツ進めます!