本ツールで使用しているオープンソースライブラリ

本ツールのコードには 1 個のオープンソースライブラリが含まれています。

Rust チートシート — クイックリファレンス

Rust 2021 の構文・所有権・エラー処理と最もよく使う std API を網羅したチートシート。日常の約80%をカバーします。

Rs

Rust Rust 2021 edition

rustc / cargo · コンパイル型 · システム · 関数型 · 並行 · 静的 · 強い型付け · アフィン (ムーブセマンティクス)

おすすめの学習パス

まず cargo new / run と println! → 変数の束縛、所有権、借用(Rust の核)を習得 → match、関数、クロージャを深く学ぶ → Vec / HashMap / イテレータでデータを整理 → トレイトとジェネリクスを理解 → Result と ? でエラー処理 → スレッドと channel で並行処理 → その後ネットワーク、時間、ビルド/テストを必要に応じて学習。FAQ セクションは落とし穴回避に立ち戻るのに最適です。

1.Hello World とビルド環境

Rust プログラムの実行、cargo プロジェクト、ツールチェーン。

最小プログラム

fn main が入口です。println! マクロで出力します。文はセミコロンで終わります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 新規プロジェクト:
// cargo new hello
// cd hello
// src/main.rs を編集:
fn main() {
println!("Hello, world!");
}
// 実行:
// cargo run
// 出力:Hello, world!
// 要点:
// fn キーワードで関数を定義
// println! はマクロ(! が付く)
// main は () 型を返す
// 式末尾のセミコロンは省略可(関数節を参照)

実行とビルド

cargo run でコンパイルして実行、cargo build でバイナリ生成、cargo check で高速チェック。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// コンパイルして実行:
// cargo run
// ビルド(デバッグ):
// cargo build
// 高速チェック(バイナリを生成しない):
// cargo check
// リリースビルド(最適化):
// cargo build --release
// 成果物の場所:
// target/debug/hello
// target/release/hello
// 単一ファイルの直接コンパイル:
// rustc main.rs
// 実行可能ファイル main を生成
// 大規模プロジェクトは常に cargo

cargo プロジェクト

cargo new でプロジェクトを初期化。Cargo.toml でパッケージと依存を宣言。src/ にソース。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// バイナリプロジェクトを作成:
// cargo new hello --bin
// ライブラリプロジェクトを作成:
// cargo new mylib --lib
// Cargo.toml の核心:
// [package]
// name = "hello"
// version = "0.1.0"
// edition = "2021"
// 構成:
// Cargo.toml マニフェスト
// src/main.rs エントリポイント
// src/lib.rs ライブラリの入口
// target/ ビルド成果物
// ドキュメント生成:
// cargo doc --open
// ビルドキャッシュ target は削除可

edition

edition で言語版を宣言。2021 が現行版。rust-version で最低ツールチェーンを指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Cargo.toml:
// [package]
// name = "app"
// version = "0.1.0"
// edition = "2021" // 言語エディション
// rust-version = "1.70" // 最低 rustc
// 現在のバージョンを確認:
// rustc --version
// edition をアップグレード:
// cargo fix --edition
// 各エディション:
// 2015(旧)/ 2018 / 2021
// edition の違い:
// キーワードの利用可否
// マクロとプレリュードの差異
// 同一プロジェクトでは 1 つだけ使う

出力マクロ

println! / print! で出力、eprintln! でエラーストリーム、format! で文字列生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let name = "Rust";
let ver = 1.82;
// プレースホルダ:
println!("Hello, {}!", name);
println!("版本: {}", ver);
// 番号と名前付き:
println!("{0} {1} {0}", "a", "b");
println!("{name} v{ver}", name = name, ver = ver);
// 書式制御:
println!("{:>10}", "右对齐"); // 幅
println!("{:.2}", 3.14159); // 精度
println!("{:b}", 10); // 2 進数
println!("{:?}", vec![1, 2]); // Debug
// 改行なし:
print!("连续");
// エラーストリーム:
eprintln!("出错了");
}

コマンドライン引数

std::env::args でコマンドライン引数を取得。先頭はプログラムパス。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::env;
fn main() {
// 引数を収集:
let args: Vec<String> = env::args().collect();
println!("共 {} 个参数", args.len());
for a in &args {
println!("{}", a);
}
// 業務引数は 1 番目から:
// args[0] はプログラムパス
// 不正な UTF-8 を無視する場合は args_os:
// env::args_os()
// 一般的な使い方:
// let name = args.get(1).unwrap();
// 複雑な解析は clap ライブラリを使う
}

複数ファイル

mod でモジュールを宣言。サブモジュールはファイルまたはディレクトリ。use で取り込み。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/main.rs:
mod math; // モジュールを宣言
use math::add;
fn main() {
println!("{}", add(1, 2));
}
// src/math.rs:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// またはディレクトリ:
// src/math/mod.rs
// src/math/utils.rs
// 公開:
// pub fn / pub struct
// デフォルトは非公開:
// 同一モジュール内からアクセス可
// 子モジュールは super:: で親を参照

直接コンパイル

rustc で単一ファイルをコンパイル。cargo 依存なし。練習や小スクリプト向け。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// コンパイル:
// rustc hello.rs
// 実行:
// ./hello
// 出力名を指定:
// rustc hello.rs -o app
// 最適化:
// rustc -O hello.rs
// デバッグ情報:
// rustc -g hello.rs
// 警告をすべて有効に:
// rustc -D warnings hello.rs
// バージョン確認:
// rustc --version
// 制限:
// 依存管理がない
// 標準ライブラリ外の crate
// は cargo が必要
// 学習や単一ファイルの実験向き

2.変数と束縛

let 束縛、可変性、シャドウイング、スコープ、型。

let と mut

let の束縛は既定で不変。mut で可変に。Rust の変数は既定で不変です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// 不変の束縛:
let x = 5;
// x = 6; // エラー:不変
// 可変の束縛:
let mut y = 5;
y = 6; // 許される
println!("{}", y);
// 明示的な型:
let n: u32 = 42;
// 宣言してから代入:
let mut z;
z = 10;
// 未使用警告:
// _ プレフィックスで無視
let _unused = 0;
}

シャドウイング

同名の let が以前の束縛を覆います(シャドウイング)。型変更も可能。mut とは別物です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let x = 5;
let x = x + 1; // シャドーイング
let x = x * 2;
println!("{}", x); // 12
// 型を変える:
let spaces = " ";
let spaces = spaces.len(); // usize
// mut との違い:
// シャドーイングは新たな束縛
// mut は元の値を変更
// シャドーイング後は旧値にアクセス不可
// スコープ内のシャドーイング:
{
let x = 99; // 内側でシャドー
println!("{}", x); // 99
}
println!("{}", x); // 12
}

const と static

const はコンパイル時定数。static は静的変数。命名は全大文字スネークケース。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定数:
const MAX_SIZE: u32 = 100;
const PI: f64 = 3.141592;
// 静的変数(固定アドレスを持つ):
static APP_NAME: &str = "demo";
// 静的で可変(unsafe):
static mut COUNTER: u32 = 0;
fn inc() {
unsafe {
COUNTER += 1; // unsafe が必要
}
}
// 違い:
// const はコンパイル時にインライン化、アドレスなし
// static は固定のメモリアドレスを持つ
// static mut のアクセスには unsafe が必要
// 現代のプラクティス:
// 優先的に const を使う
// グローバル状態はミューテックスを使う
// 定数式は計算できる

型推論

コンパイラがコンテキストから型を推論。必要なら明示。整数の既定は i32。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
// 自動推論:
let x = 42; // i32(デフォルト)
let y = 3.14; // f64(デフォルト)
// 文脈による推論:
let mut v = Vec::new();
v.push(1); // Vec<i32> と推論
// 明示的な型注釈:
let n: u64 = 42;
let f: f32 = 3.14;
// メソッドの戻り値で型が決まる:
let s: u32 = "42".parse().unwrap();
// 推論できない場合は注釈が必要
// 整数型のデフォルトは i32
// 浮動小数点のデフォルトは f64

分解束縛

let はパターン分解に対応。タプル・構造体・配列を一度に取り出せます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// タプルの分解:
let (a, b, c) = (1, 2, 3);
println!("{} {} {}", a, b, c);
// 配列の分解:
let [x, y, _] = [10, 20, 30];
// 構造体の分解:
struct Point { x: i32, y: i32 }
let p = Point { x: 1, y: 2 };
let Point { x, y } = p;
// 名前を変える:
let Point { x: px, y: py } = p;
// 残り:
let (first, rest..) = (1, 2, 3, 4);
// ワイルドカード _ で無視:
let (_, b2) = (1, 2);
}

スコープ

ブロックスコープ {}。変数のライフタイム。内側から外側をアクセス可能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let outer = 1;
{
// 内側から外側にアクセス可:
println!("{}", outer);
let inner = 2;
println!("{}", inner);
}
// println!("{}", inner); // エラー:スコープ外
// 同一スコープ内で同名は不可:
// シャドーイングはネストしたスコープが必要
// 所有権はスコープの終わりで解放
// 解放順(逆順):
// Drop はスコープ終了時に呼ばれる
// 早期終了には drop() を使う

命名規則

Rust の命名規則。変数/関数は snake_case、型は CamelCase。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 変数/関数:snake_case
let user_name = "nick";
fn get_user() {}
// 型/enum/trait:CamelCase
struct UserAccount {}
enum ColorChoice {}
trait Displayable {}
// 定数/静的:SCREAMING_SNAKE
const MAX_ITEMS: usize = 100;
// 非公開の識別子には _ プレフィックスを付ける
// ジェネリック引数:大文字 1 文字 T
// ライフタイム:'a 'b
// 組み込みの慣習:
// is_ / has_ プレフィックスの真偽値関数
// into_ / to_ / as_ 変換
// コンパイラの lint が命名をチェック

型変換

as は明示的な数値変換を行う。From/Into trait は安全な変換に用いる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
// as による数値変換:
let i: i32 = 42;
let u: u64 = i as u64;
let f: f64 = i as f64;
// 切り詰めのリスク:
let big: u16 = 300;
let small: u8 = big as u8; // 44
// From/Into:
let s = String::from("hello");
let s2: String = "hi".into();
// 正確な変換:
let x: u8 = 255;
let y: u32 = x.into(); // From<u8> for u32
// 文字列のパース:
let n: i32 = "42".parse().unwrap();
// 表示が必要:
// let n: i32 = "42".parse()?;

3.型システム

基本型、タプル、構造体、列挙型。

基本型

整数、浮動小数点、真偽値、文字。スカラー型とその幅。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// 整数:
let i: i32 = -10; // 符号あり
let u: u32 = 10; // 符号なし
// 浮動小数点:
let f: f32 = 1.5;
let d: f64 = 3.14159;
// 真偽値:
let b: bool = true;
let c: bool = false;
// 文字(Unicode):
let ch: char = '中';
let emoji: char = '🚀';
// 文字列スライス:
let s: &str = "text";
// 整数のデフォルトは i32、浮動小数点は f64
// char は 4 バイト(Unicode スカラー値)

整数型

i8〜u128、usize / isize。符号付き/なし。プラットフォーム依存のサイズ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
// 符号あり:i8 i16 i32 i64 i128
// 符号なし:u8 u16 u32 u64 u128
// プラットフォーム依存:usize isize
let a: i8 = -128; // 範囲 -128..127
let b: u8 = 255; // 範囲 0..255
let c: i64 = 9_223_372_036_854_775_807;
// リテラル接尾辞:
let d = 255u8;
let e = 1_000_000i64; // アンダースコア区切り
// 16 進数/2 進数:
let h = 0xff;
let bin = 0b1010;
// usize の用途:
// インデックス、長さ、ポインタのサイズ
// オーバーフロー検査(デバッグビルドでは panic)

浮動小数点数

f32 / f64 は IEEE 754 に準拠。NaN、無限大、演算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
let a: f64 = 3.14;
let b: f64 = 2.0;
// 演算:
let sum = a + b;
let div = a / b;
let pow = a.powi(2); // べき乗
let sqrt = a.sqrt();
// 特殊値:
let inf = f64::INFINITY;
let neg_inf = f64::NEG_INFINITY;
let nan = f64::NAN;
// 判定:
// nan.is_nan()
// inf.is_infinite()
// 注意:
// NaN != NaN
// 浮動小数点は正確な比較に不向き
// 精度:
// f32 は約 7 桁の 10 進数
// f64 は約 15-16 桁

char 型

char は Unicode スカラー値で 4 バイトを占める。シングルクォートで囲むリテラル。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let c = 'A';
let zh = '中';
let emoji = '😀';
// エスケープ:
let tab = '\t';
let quote = '\'';
let newline = '\n';
// Unicode コードポイント:
let cp = '\u{1F600}'; // 😀
// 操作:
println!("{}", c.is_ascii());
println!("{}", zh.is_alphabetic());
// 数字との関係:
let n = '9' as u8; // 57
// char と String:
// 文字を文字列に変換:
let s = c.to_string();
// char は &str ではない

タプル

固定長の異種コレクション。インデックスアクセスと分解。空タプルは ()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
// 定義:
let t = (1, "two", 3.0);
// インデックスでアクセス:
println!("{}", t.0); // 1
println!("{}", t.1); // "two"
// 型注釈:
let t2: (i32, bool) = (5, true);
// 分解:
let (a, b, c) = t;
// 単一要素のタプル:
let one = (42,); // カンマに注意
// 空タプル/ユニット型:
let u: () = ();
// () は関数のデフォルトの戻り値
// 一般的な用途:
// 複数の値を返す (val, err)
// 交換: (a, b) = (b, a)

構造体

struct は名前付きフィールドを定義。生成・アクセス・更新構文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct User {
name: String,
age: u8,
active: bool,
}
fn main() {
// インスタンス化:
let mut u = User {
name: String::from("nick"),
age: 30,
active: true,
};
// フィールドへのアクセス:
println!("{}", u.name);
u.age += 1; // mut が必要
// 更新構文:
let u2 = User { age: 20, ..u };
// タプル構造体:
struct Point(i32, i32);
let p = Point(1, 2);
println!("{}", p.0);
// ユニット構造体:
struct Unit;
}

列挙型

enum は複数のバリアントを定義。データを持てます。match で網羅的に処理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
enum Message {
Quit,
Move { x: i32, y: i32 }, // 構造体バリアント
Write(String), // タプルバリアント
ChangeColor(u8, u8, u8),
}
fn main() {
let m = Message::Write(String::from("hi"));
// match は網羅的マッチ:
match m {
Message::Quit => println!("退出"),
Message::Move { x, y } => {
println!("移动 {} {}", x, y);
}
Message::Write(s) => println!("{}", s),
Message::ChangeColor(r, g, b) => {
println!("颜色 {}-{}-{}", r, g, b);
}
}
// 列挙型はジェネリクス/メソッドを持てる
// Option/Result も列挙型

ユニット型

() は値なし。関数の既定の戻り値。引数なし戻り値なしの慣習。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// () 型:
let unit: () = ();
// 関数のデフォルトの戻り値は ():
fn say() {}
// 同等:
fn say2() -> () {}
// 式が () を返す:
let r = { let x = 1; }; // 空ブロック
// 用途:
// 型引数として:
// Result<T, ()> はエラーのみを気にする
// HashMap<K, ()> を集合として使う
// ジェネリクスのプレースホルダ:
// 具体的な型を気にしない
// Debug 出力:
println!("{:?}", ());

4.所有権と参照

所有権、借用、参照、スライス — Rust のメモリ安全性の核。

所有権

値は唯一の所有者を持ちます。スコープ終了で自動解放。コピーではなくムーブ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
// 所有権のルール:
// 1. 各値には所有者が 1 人
// 2. 所有者がスコープを出ると解放
// 3. 所有権の移動をムーブと呼ぶ
let s1 = String::from("hello");
let s2 = s1; // ムーブ:s1 は無効
// println!("{}", s1); // エラー:ムーブ済み
println!("{}", s2);
// コピー型(スタック上で Copy):
let a = 5;
let b = a; // コピー、両方有効
println!("{} {}", a, b);
// 関数の引数も所有権が移る:
// fn take(s: String) {}
// 取り戻すには戻り値を使う

参照

&T は不変参照であり、所有権は移動しない。借用後も元の値は引き続き利用できる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let s = String::from("hello");
// 不変の参照:
let len = calculate_length(&s);
println!("{}", len); // 5
println!("{}", s); // まだ使える
}
fn calculate_length(s: &String) -> usize {
s.len() // 借用、所有しない
}
// 参照のルール:
// &T は読み取り専用の借用
// 参照は所有権を移さない
// 参照はデフォルトで不変
// 不変参照は複数共存できる
// 参照がスコープを出ても値は解放されない
// 一般的な使い方:
// 引数は &str / &T にしてコピーを避ける

可変参照

&mut T は可変参照。同一スコープで 1 つの変数に対し可変参照は 1 つだけ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // hello!
}
fn change(s: &mut String) {
s.push_str("!");
}
// ルール:
// 可変参照は &mut T
// 同一スコープでは 1 つだけ
// 不変と可変は共存できない:
// let r1 = &s;
// let r2 = &mut s; // エラー
// データ競合はコンパイル時に禁止:
// 読み取り多く書き込み少ない場面
// 借用が終わってから再度借用できる
// 波括弧で借用範囲を短くできる

逆参照

* 演算子は参照の指す値にアクセスします。逆参照は参照と対になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let x = 5;
let r = &x; // 参照
// デリファレンスで値にアクセス:
println!("{}", *r);
// デリファレンスして代入:
let mut y = 5;
let r2 = &mut y;
*r2 += 1; // y を変更
println!("{}", y); // 6
// メソッド呼び出しは自動デリファレンス:
// s.len() に (*s).len() は不要
// インデックスの内部もデリファレンス
// 参照の比較:
// == は値か参照を比較する?
// 通常はデリファレンスした値を比較
// ぶら下がり参照はコンパイラが拒否

ライフタイム

'a で参照の有効範囲を注釈。借用チェッカーが有効性を保証。省略規則。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ライフタイム注釈:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long");
let s2 = String::from("s");
let r = longest(&s1, &s2);
println!("{}", r);
}
// ルール:
// 'a は参照の有効範囲を表す
// 戻り値のライフタイムは入力の共通部分
// 構造体は参照を保持できる:
// struct X<'a> { part: &'a str }
// 省略ルール:
// 入力が 1 つの場合は出力に省略
// メソッドは self のライフタイムを省略
// 多くの場合は明示的な注釈が不要

スライス

&[T] は連続要素への参照。文字列スライスは &str。所有権を持ちません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
// 配列のスライス:
let arr = [1, 2, 3, 4, 5];
let s: &[i32] = &arr[1..4]; // [2,3,4]
println!("{:?}", s);
// 文字列スライス:
let st = String::from("hello");
let part: &str = &st[0..2]; // "he"
// 境界は文字境界、それ以外は panic
// 全体のスライス:
let all = &arr[..];
// 一般的な使い方:
// 関数の引数は Vec より &[T]
// &str は String の借用
// スライスの長さはコンパイル時に不明
// スライスは参照なのでコピーしない

Deref 自動逆参照

Deref トレイトで &String を &str に自動変換。引数の多相が便利に。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::ops::Deref;
fn main() {
let s = String::from("hello");
// &String が自動で &str になる:
let t: &str = &s;
// 関数の引数も自動変換:
fn takes_str(s: &str) {}
let s2 = String::from("world");
takes_str(&s2); // 自動デリファレンス
// 独自の Deref:
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
// メソッド呼び出しも自動:
// my_box.len() は段階的に deref
// 変換の連鎖:&MyBox<T> -> &T

Copy 型

Copy トレイト型は代入でコピーされ、ムーブしない。整数・浮動小数点・bool・char など。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
// Copy 型:代入後も使える
let a = 5;
let b = a; // コピー
println!("{} {}", a, b); // どちらも可
// Copy 型の一覧:
// すべての整数/浮動小数/bool/char
// タプル(要素がすべて Copy)
// 配列
// &T 参照(Copy)
// 非 Copy:
// String、Vec、Box
// 構造体はデフォルトで非 Copy:
#[derive(Clone, Copy)]
struct Point { x: i32, y: i32 }
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // Copy
println!("{} {}", p1.x, p2.x);
// 判定:
// スコープ終了時に Drop がなければ Copy にできる
// クローンは明示的に .clone()

5.制御フロー

if、match、ループ。Rust の制御フローは式ベースです。

if 式

if / else は式で値を返せます。条件に括弧は不要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let n = 7;
// 分岐:
if n > 0 {
println!("正数");
} else if n == 0 {
println!("零");
} else {
println!("负数");
}
// if は式:
let sign = if n > 0 { "+" } else { "-" };
println!("{}", sign);
// 条件は括弧が不要
// 必ず bool 型
// 分岐の戻り型は一致させる
// else のない if は () を返す

match

match は網羅的にマッチします。各分岐は式。パターン・範囲・ガードが使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let x = 3;
// リテラル値のマッチ:
match x {
1 => println!("一"),
2 | 3 => println!("二或三"), // OR
4..=6 => println!("四到六"), // 範囲
_ => println!("其他"), // ワイルドカード
}
// バインディングとガード:
let num = Some(10);
match num {
Some(n) if n > 5 => println!("大数 {}", n),
Some(n) => println!("小数 {}", n),
None => println!("无"),
}
// 網羅が必要(_ がないとコンパイルエラー)
// match は式として値を返せる

while ループ

while は条件ループ。条件が真の間繰り返します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let mut n = 3;
// カウントループ:
while n > 0 {
println!("{}", n);
n -= 1;
}
// 条件ループ:
let mut line = String::new();
while line.trim() != "quit" {
line.clear();
// 入力の読み取り...
break; // 例は直接抜ける
}
// while ループにインデックスはない
// コレクションの走査は for を優先
// インデックスが必要な場面は:
// カウンタ + while
// while は () を返す

for ループ

for は範囲やイテレータを反復。Rust で優先されるループ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
// 範囲:
for i in 0..5 {
println!("{}", i); // 0..4
}
// 端点を含む:
for i in 0..=5 {
println!("{}", i); // 0..5
}
// 配列の走査:
let arr = [10, 20, 30];
for v in &arr {
println!("{}", v);
}
// インデックスが必要:
for (i, v) in arr.iter().enumerate() {
println!("{}: {}", i, v);
}
// 逆順:
for i in (0..3).rev() {}
// コレクションの走査で所有権は移らない
// 移動して走査するなら into_iter

無限ループ

loop は break までの無限ループ。値を返せます。ラベルも使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
// 無限ループ:
let mut count = 0;
let result = loop {
count += 1;
if count == 3 {
break count * 10; // 30 を返す
}
};
println!("{}", result);
// ラベルで外側を抜ける:
'outer: loop {
loop {
break 'outer; // 外側のループを抜ける
}
}
// 用途:
// リトライ、ポーリング、イベントループ
// while true と同等
// loop の方が慣用的で値を返せる

break と continue

break はループ終了。continue はその反復をスキップ。ラベル併用可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fn main() {
let mut n = 0;
loop {
n += 1;
// 偶数のスキップ:
if n % 2 == 0 {
continue;
}
if n > 7 {
break;
}
println!("{}", n); // 1 3 5 7
}
// continue は現在のイテレーションを終える
// break は値を返せる:
let v = loop { break 42; };
// ネストしたラベル:
'a: for i in 0..3 {
for j in 0..3 {
if i + j == 2 {
continue 'a; // 外側のループを継続
}
}
}
}

if let

if let は単一パターンのマッチを簡潔に書きます。一つのバリアントだけを見たい時に。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
let opt = Some(5);
// 単一パターンのマッチ:
if let Some(v) = opt {
println!("值 {}", v);
}
// else 付き:
if let Some(v) = opt {
println!("有值 {}", v);
} else {
println!("无值");
}
// 他のパターン:
let res: Result<i32, &str> = Ok(10);
if let Ok(v) = res {
println!("成功 {}", v);
}
// 対応する match:
// match opt {
// Some(v) => ...,
// _ => (),
// }
// より簡潔、1 つの場合に注目
// 網羅的でなく、組み合わせ可能

while let

while let はパターンがマッチする間ループ。コレクションの逐次処理に。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let mut stack = vec![1, 2, 3];
// 空になるまでポップ:
while let Some(top) = stack.pop() {
println!("{}", top); // 3 2 1
}
// 文字の列を処理:
let mut chars = "abc".chars();
while let Some(c) = chars.next() {
println!("{}", c);
}
// 対応する loop + match:
// loop {
// match v.next() {
// Some(x) => ...
// None => break,
// }
// }
// イテレータは反復可能オブジェクト
// 条件が複雑な場合は loop を使う

6.関数

関数定義、戻り値、所有権の移動、クロージャ、ジェネリクス。

関数定義

fn で関数を定義する。引数には型注釈が必要で、関数本体は式である。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 戻り値なし:
fn greet(name: &str) {
println!("你好 {}", name);
}
// 戻り値あり:
fn add(a: i32, b: i32) -> i32 {
a + b // 最後の式
}
// 明示的な return:
fn early(x: i32) -> i32 {
if x < 0 {
return 0;
}
x * 2
}
fn main() {
greet("Rust");
println!("{}", add(1, 2));
println!("{}", early(-5));
}
// 戻り値が () なら -> を省略できる

戻り値

最後の式が戻り値。セミコロンで文と式を区別。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let x = five();
println!("{}", x); // 5
let y = with_semi();
println!("{:?}", y); // ()
}
fn five() -> i32 {
5 // 式、値を返す
}
fn with_semi() {
5; // 文、() を返す
}
// 要点:
// 式 = 値を持つコード
// 文 = セミコロンで終わる
// セミコロンなし = その値を返す
// ブロックは式:
let n = { let a = 1; a + 2 }; // 3
// if/match/loop はすべて式

引数

引数は既定で不変。引数でのパターン分解。参照引数でコピーを回避。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 参照の引数:
fn show(name: &str) {
println!("{}", name);
}
// 可変引数は mut が必要:
fn append(v: &mut Vec<i32>) {
v.push(1);
}
// タプル分解の引数:
fn sum((a, b): (i32, i32)) -> i32 {
a + b
}
fn main() {
show("nick");
let mut v = vec![];
append(&mut v);
println!("{}", sum((1, 2)));
}
// 引数は値のコピー/ムーブ
// 大きな型は参照で渡す
// 変更が必要なら &mut を渡す
// 新しい値を返して参照を避ける

所有権の引数

値で渡すと所有権がムーブ。借用すれば再利用可能。必要なら所有権を返す。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn takes(s: String) {
println!("{}", s); // s は使用後に解放される
} // s がスコープを出て解放
fn main() {
let s = String::from("hi");
takes(s);
// println!("{}", s); // エラー:ムーブ済み
// 所有権を取り戻す:
let s2 = String::from("keep");
let s2 = give_back(s2);
println!("{}", s2);
}
fn give_back(s: String) -> String {
s // ムーブして返す
}
// 代替:
// 借用 &str が通常は適切
// 所有権が必要な場合のみ値を渡す
// 複数の値を返すならタプル

借用引数

&T は読み取り借用、&mut T は可変借用。所有権のムーブを回避。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 読み取り専用の借用:
fn len(s: &str) -> usize {
s.len()
}
// 複数の読み取り専用借用は共存可:
fn print_two(a: &str, b: &str) {
println!("{} {}", a, b);
}
// 可変借用:
fn push_item(v: &mut Vec<i32>, x: i32) {
v.push(x);
}
fn main() {
let s = String::from("abc");
println!("{}", len(&s));
println!("{}", s); // 借用後も使える
let mut v = vec![1];
push_item(&mut v, 2);
println!("{:?}", v);
}
// &str 引数は &String より汎用性が高い

クロージャ

クロージャは環境の変数をキャプチャします。|引数| 構文。変数に保持・関数に渡せる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
// 定義:
let add = |a: i32, b: i32| a + b;
println!("{}", add(1, 2));
// 外部変数をキャプチャ:
let factor = 3;
let mul = |x: i32| x * factor;
println!("{}", mul(5)); // 15
// クロージャはキャプチャを変更できる(mut が必要):
let mut count = 0;
let mut inc = || {
count += 1;
count
};
println!("{}", inc()); // 1
// 引数として:
let evens: Vec<i32> =
(1..=6).filter(|x| x % 2 == 0).collect();
println!("{:?}", evens);
// 所有権を移す move クロージャは並行節を参照

関数ポインタ

fn 型は通常の関数を参照する。fn(T) -> U 構文を用いる。クロージャを fn に変換できる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn add_one(x: i32) -> i32 {
x + 1
}
// 関数ポインタ型:
fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
fn main() {
let f: fn(i32) -> i32 = add_one;
println!("{}", f(10)); // 11
println!("{}", apply(add_one, 10));
// キャプチャしないクロージャは fn に変換可:
let c = |x: i32| x * 2;
let g: fn(i32) -> i32 = c;
println!("{}", g(5));
// 関数の配列/ベクタ:
let funcs = [add_one, add_one];
// ジェネリクスな場面では Fn trait を使う

ジェネリック関数

ジェネリックパラメータ <T> で型を抽象化。境界を追加可能。型推論。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ジェネリック関数:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut max = &list[0];
for item in list {
if item > max {
max = item;
}
}
max
}
fn main() {
let nums = vec![34, 50, 25];
println!("{}", largest(&nums));
let chars = vec!['y', 'm', 'a'];
println!("{}", largest(&chars));
}
// T: PartialOrd はトレイト制約
// 複数の引数:fn pair<T, U>
// 呼び出し時に型を自動推論
// 明示も可能:largest::<i32>(&nums)
// ジェネリクスは性能を損なわない(単相化)

7.文字列

&str と String、操作、フォーマット、解析、反復。

&str と String

&str は借用された文字列スライス、String は所有権を持つ可変長文字列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// &str 文字列リテラル:
let s: &str = "hello";
// String は所有権を持つ:
let mut owned = String::from("hello");
owned.push_str(" world");
// &str -> String:
let a: String = s.to_string();
let b: String = String::from(s);
// String -> &str(借用):
let c: &str = &owned;
let d: &str = owned.as_str();
// 違い:
// &str は固定長でデータを所有しない
// String は伸長可能、ヒープに確保
// 引数は &str を優先
// 所有が必要なら String に変換

String の操作

追加・挿入・置換・削除など String を変更するメソッド。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
let mut s = String::from("hello");
// 追加:
s.push_str(", world"); // 文字列
s.push('!'); // 1 文字
// 挿入:
s.insert(5, ' ');
// 置換:
let t = s.replace("hello", "hi");
// 削除:
s.pop(); // 末尾を除去
s.truncate(5); // 切り詰め
// クリア:
s.clear();
// 結合:
let s1 = String::from("a");
let s2 = String::from("b");
let s3 = s1 + &s2; // s1 はムーブされる
let s4 = format!("{} {}", s3, "c");
// 長さ:s.len() はバイト数
}

フォーマット

format! で文字列を連結。プレースホルダと書式指定。安全で高効率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let name = "Nick";
let age = 30;
// 基本的な結合:
let s = format!("{} 今年 {} 岁", name, age);
println!("{}", s);
// 番号/名前付き:
format!("{0} {1} {0}", "a", "b");
format!("{name}-{age}", name = name, age = age);
// 整列と幅:
format!("{:>8}", "right"); // 右寄せ
format!("{:<8}", "left"); // 左寄せ
format!("{:^8}", "center");
// 数値の書式:
format!("{:.2}", 3.14159); // 3.14
format!("{:+}", 42); // +42
format!("{:08b}", 10); // 2 進数のゼロ埋め
format!("{:#x}", 255); // 0xff
// Debug デバッグ書式:
format!("{:?}", vec![1, 2]);

文字列スライス

&s[a..b] はバイト単位のスライス。文字境界でのみ可能、さもなくば panic。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let s = String::from("hello 世界");
// バイト単位のスライス:
let part = &s[0..5]; // "hello"
println!("{}", part);
// 文字境界:
// "世界" は 1 文字 3 バイト
let w = &s[6..12]; // "世界"
// 誤った境界は panic:
// let bad = &s[0..6]; // 文字境界でない
// 安全な走査方法:
for c in s.chars() {
println!("{}", c);
}
// 1 文字を取得:
let first = s.chars().next();
// 行単位:
for line in s.lines() {}
// インデックスはバイト位置、文字位置ではない

文字列の反復

chars() は文字単位、bytes() はバイト単位、char_indices は位置付き。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fn main() {
let s = "你好Rust";
// 文字単位の走査:
for c in s.chars() {
println!("{}", c);
}
// バイト単位の走査:
for b in s.bytes() {
println!("{:02x}", b);
}
// 文字 + バイトオフセット:
for (i, c) in s.char_indices() {
println!("{} at {}", c, i);
}
// 行単位:
for line in "a\nb\n".lines() {
println!("<{}", line);
}
// 分割:
for part in "a,b,c".split(",") {
println!("{}", part);
}
// 文字数を数える:
let n = s.chars().count();
println!("{} 个字符", n);
// バイト数 len() は文字数と異なる

文字列の解析

str::parse は文字列を数値に解析する。parse::<T>() は目標の型を注釈する必要がある。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
// i32 としてパース:
let n: i32 = "42".parse().unwrap();
// 明示的なジェネリクス:
let f: f64 = "3.14".parse().unwrap();
let u: u32 = "255".parse().unwrap();
// エラー処理:
let result = "abc".parse::<i32>();
match result {
Ok(v) => println!("{}", v),
Err(e) => println!("解析失败: {}", e),
}
// 真偽値:
let b: bool = "true".parse().unwrap();
// 数値から文字列へ:
let s = 42.to_string();
let s2 = format!("{}", 3.14);
// ? による簡略化:
// fn f() -> Result<i32, ParseIntError> {
// Ok("42".parse()?)
// }

文字の操作

char のメソッド: 大小文字、数字判定、空白判定。文字列メソッドと組み合わせ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
let c = 'a';
// 判定:
println!("{}", c.is_alphabetic());
println!("{}", c.is_numeric());
println!("{}", c.is_whitespace());
println!("{}", c.is_ascii());
// 大文字/小文字:
let up = 'a'.to_uppercase().to_string();
let low = 'A'.to_lowercase().to_string();
// 文字列メソッド:
let s = "Hello, Rust!";
s.to_lowercase();
s.to_uppercase();
s.trim(); // 空白を除去
s.starts_with("Hello");
s.contains("Rust");
s.replace("Rust", "Go");
// 部分文字列:
let sub = &s[7..]; // "Rust!"
// 空チェック:s.is_empty()
// 繰り返し:"ab".repeat(3)

文字列メソッド

検索・トリム・分割・判定。よく使う str メソッドをチェーン。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
let s = " Hello, Rust 2024 ";
// トリムと大小文字:
let t = s.trim();
let lower = t.to_lowercase();
let upper = t.to_uppercase();
// 検索:
t.starts_with("Hello");
t.ends_with("2024");
t.contains("Rust");
t.find("Rust"); // Option<usize>
// 分割:
let parts: Vec<&str> = t.split(",").collect();
let lines: Vec<&str> = t.lines().collect();
let words: Vec<&str> = t.split_whitespace().collect();
// 置換と削除:
t.replace("Rust", "Go");
t.replacen("o", "0", 1);
// 繰り返しとゼロ埋め:
"ab".repeat(3);
format!("{:0>5}", 42); // 00042
// 空の判定:
s.is_empty();
}

8.コレクション

Vec、HashMap、集合型、イテレータ。

Vec

Vec<T> は可変長配列。長さ可変、要素追加・削除可。参照はスタック、データはヒープ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
// 作成:
let mut v = Vec::new();
v.push(1);
v.push(2);
// マクロで作成:
let mut v2 = vec![10, 20, 30];
let zero = vec![0; 5]; // [0,0,0,0,0]
// アクセス:
let a = &v2[0]; // インデックス、範囲外は panic
let b = v2.get(1); // Option、安全
// 変更:
v2[0] = 99;
v2.push(40);
v2.pop(); // 末尾を除去
v2.remove(1); // インデックスを除去
v2.insert(0, 5); // 先頭に挿入
// 長さ:v2.len()
// クリア:v2.clear()
// 走査:for x in &v2 {}
// ソート:v2.sort()

Vec の操作

反復・フィルタ・マップ・収集。スライスビューと所有権。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
let v = vec![1, 2, 3, 4];
// イテレータ変換:
let doubled: Vec<i32> =
v.iter().map(|x| x * 2).collect();
// フィルタ:
let evens: Vec<&i32> =
v.iter().filter(|x| **x % 2 == 0).collect();
// 合計:
let sum: i32 = v.iter().sum();
// 最大値:
let max = v.iter().max();
// 検索:
let found = v.iter().find(|x| **x == 3);
// 走査しながら変更:
let mut w = vec![1, 2];
for x in w.iter_mut() {
*x += 10;
}
// 移動して走査:
let owned: Vec<i32> = w.into_iter()
.map(|x| x + 1).collect();
// スライスビュー:
let part = &v[1..3];

HashMap

HashMap<K, V> はキー/値ペア。ハッシュ実装、順序なし。ルックアップ O(1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::collections::HashMap;
fn main() {
// 作成:
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Red"), 20);
// マクロで作成:
let mut m: HashMap<&str, i32> =
HashMap::from([("a", 1), ("b", 2)]);
// 読み取り:
let score = scores.get("Blue"); // Option
// 走査:
for (k, v) in &scores {
println!("{}: {}", k, v);
}
// 挿入または更新:
*m.entry("a").or_insert(0) += 10;
m.insert("c", 3);
// 削除:
m.remove("a");
// 確認:
m.contains_key("b");
// 長さ:m.len()
// 順序なし、走査順はランダム

HashSet

HashSet<T> は一意要素の集合。重複除去・交差・和集合・差集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::collections::HashSet;
fn main() {
// 作成:
let mut set = HashSet::new();
set.insert(1);
set.insert(2);
set.insert(2); // 重複は無視
println!("{:?}", set); // {1, 2}
// 操作:
set.contains(&1); // true
set.remove(&1);
set.len();
// 集合演算:
let a: HashSet<i32> = [1, 2, 3].into_iter().collect();
let b: HashSet<i32> = [2, 3, 4].into_iter().collect();
// 和集合:
let u: HashSet<_> = a.union(&b).cloned().collect();
// 積集合:
let i: HashSet<_> = a.intersection(&b).cloned().collect();
// 差集合:
let d: HashSet<_> = a.difference(&b).cloned().collect();
// 部分集合の判定:a.is_subset(&b)

BTree

BTreeMap / BTreeSet はキーが順序付けられて反復可能。性能はハッシュにやや劣る。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::collections::BTreeMap;
fn main() {
// 順序付きのキーと値:
let mut m = BTreeMap::new();
m.insert("b", 2);
m.insert("a", 1);
m.insert("c", 3);
// 順序どおりに走査:
for (k, v) in &m {
println!("{}: {}", k, v);
} // a b c の順
// 範囲クエリ:
for (_k, v) in m.range("a".."c") {
println!("{}", v);
}
// 最大/最小:
let first = m.first_key_value();
let last = m.last_key_value();
// BTreeSet も同じ思想:
use std::collections::BTreeSet;
let s: BTreeSet<i32> = [3, 1, 2].into_iter().collect();
println!("{:?}", s); // {1, 2, 3}
// 適用:
// 順序どおりの走査/範囲クエリが必要な場合
// キーが比較可能なら使える

イテレータ

Iterator trait がチェーン状の操作を駆動する。評価は遅延され、消費時に実行される。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
let v = vec![1, 2, 3, 4, 5];
// チェーン:
let result: Vec<i32> = v.iter()
.filter(|x| **x % 2 == 1) // 奇数
.map(|x| x * 10)
.collect();
println!("{:?}", result); // [10, 30, 50]
// 遅延評価:消費しないと実行されない
// よく使うアダプタ:
// .take(n) 先頭 n 個
// .skip(n) n 個スキップ
// .rev() 反転
// .zip(iter) ペアにする
// .enumerate() インデックスを付ける
// 消費メソッド:
let sum: i32 = v.iter().sum();
let count = v.iter().count();
let any = v.iter().any(|x| x > 3);
let all = v.iter().all(|x| x > 0);
// 配列をイテレータに:
for x in v.iter() { println!("{}", x); }

VecDeque

VecDeque は両端キュー。両端で高効率に追加・削除。リングバッファ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::collections::VecDeque;
fn main() {
// 作成:
let mut d = VecDeque::new();
// 両端の操作:
d.push_back(1);
d.push_back(2);
d.push_front(0);
println!("{:?}", d); // [0, 1, 2]
d.pop_front(); // 0
d.pop_back(); // 2
// アクセス:
d.front(); // Option
d.back();
d.front_mut();
// 範囲:
d.make_contiguous();
// 適した用途:
// キュー/FIFO
// スライディングウィンドウ
// 両端からのスキャン
// Vec との違い:
// Vec の先頭挿入は O(n)
// VecDeque は両端 O(1)
// ランダムアクセスはやや遅い

ネストコレクション

コレクションの組み合わせ。Vec<HashMap>、ネスト HashMap、多次元 Vec。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::collections::HashMap;
fn main() {
// Vec に構造体を格納:
let users = vec![
("a", 1),
("b", 2),
];
// HashMap のネスト:
let mut matrix: HashMap<String, Vec<i32>> =
HashMap::new();
matrix.insert("row1".to_string(), vec![1, 2]);
// 存在しなければ新規作成:
matrix.entry(String::from("row2"))
.or_insert(vec![]).push(3);
// 多次元 Vec:
let mut grid = vec![vec![0; 3]; 3];
grid[0][1] = 5;
// 構造体のコレクション:
#[derive(Debug)]
struct Item { id: u32, tags: Vec<String> }
let items = vec![
Item { id: 1, tags: vec!["x".into()] },
];
println!("{:?}", items);
}

9.メモリと所有権の拡張

ヒープ割り当て、スマートポインタ、共有所有権。

スタックとヒープ

スタックは固定サイズで高速。ヒープは可変サイズで要確保。所有権が寿命を管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
// スタック上のデータ:
let x = 42; // 固定サイズ、スタック上
// ヒープ上のデータ:
let s = String::from("动态大小");
// String の構造はスタック、データはヒープ
// 自動管理:
// スコープを出るとスタックは即座にポップ
// ヒープは所有権システムが解放
// 手動 free なし、リークなし
// 解放のタイミング:
// 所有者がスコープを出る(Drop)
// Box/スマートポインタがヒープオブジェクトを管理
// 再帰構造には Box が必要
// 大きな配列はヒープに置いてスタックオーバーフローを避ける
}

Box

Box<T> は値をヒープへ。サイズ確定・所有権のムーブ。再帰型に必須。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
// ヒープ上に確保:
let b = Box::new(5);
println!("{}", *b); // デリファレンス
// 再帰型には Box が必須:
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1,
Box::new(List::Cons(2, Box::new(List::Nil))));
// Box は単一所有権のみ提供
// 用途:
// 大きな構造体のコピーを減らす
// 動的サイズのトレイトオブジェクト
// 再帰的なデータ構造
// 自動デリファレンス:
// b.len() は自動で deref
// スコープを出ると自動解放

Rc

Rc<T> は参照カウントによる共有所有権。シングルスレッド。複数所有者で読み取りのみ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::rc::Rc;
fn main() {
// 共有所有権:
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // 参照カウント +1
let c = Rc::clone(&a);
println!("{}", a);
println!("{}", b);
// カウント:
println!("{}", Rc::strong_count(&a)); // 3
// 読み取り専用の共有:
// Rc 内のデータは不変
// 可変が必要なら RefCell を使う
// シングルスレッド専用:
// マルチスレッドでは Arc
// 循環参照でリークする:
// Weak で解消する
// スコープを出るとカウントが減る

RefCell

RefCell<T> は実行時に借用規則を検査。内部可変性。借用競合で panic。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::cell::RefCell;
fn main() {
// 内部可変性:
let cell = RefCell::new(42);
*cell.borrow_mut() += 1; // 変更
println!("{}", cell.borrow());
// 実行時の借用ルール:
// 可変借用 1 つ、または
// 不変借用は複数
// 衝突:
// let b1 = cell.borrow();
// let b2 = cell.borrow_mut(); // panic!
// 用途:
// 構造体内の可変フィールド
// Rc と組み合わせて共有可変
// &mut との違い:
// コンパイル時 -> 実行時
// 借用チェッカを回避
// 注意が必要、借用範囲を短く保つ

Arc

Arc<T> はスレッド安全な参照カウント。複数スレッド間の共有所有権。原子カウンタ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::sync::Arc;
use std::thread;
fn main() {
// スレッドセーフな共有:
let data = Arc::new(vec![1, 2, 3]);
let mut handles = vec![];
for i in 0..3 {
let d = Arc::clone(&data);
handles.push(thread::spawn(move || {
println!("线程 {}: {:?}", i, d);
}));
}
for h in handles {
h.join().unwrap();
}
// Rc との違い:
// Arc はアトミックなカウントでスレッドセーフ
// Rc はシングルスレッド
// 読み取り専用の共有:
// 可変には Mutex と組み合わせる:
// Arc<Mutex<T>>
// 循環参照に注意

Mutex

Mutex<T> は内包する値への排他アクセスを提供し、ロックで保護する。Arc と組み合わせれば共有可能かつ可変になる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// 共有可変状態:
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = c.lock().unwrap();
*num += 1; // ロックして変更
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", *counter.lock().unwrap());
// ロックのライフタイム:
// lock() は Guard を返す
// スコープを出ると自動アンロック
// ポイズニング:
// ロック保持中に panic すると毒化
// unwrap で処理 or_else
// 複数ロックはデッドロックの順序に注意

アトミック型

AtomicU32 などのアトミック操作。ロックフリーな並行カウンタ。Ordering でメモリ順序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use std::sync::atomic::{
AtomicU32, Ordering
};
use std::thread;
fn main() {
// アトミックカウンタ:
let counter = AtomicU32::new(0);
let mut handles = vec![];
for _ in 0..10 {
let c = &counter;
handles.push(thread::spawn(move || {
c.fetch_add(1, Ordering::SeqCst);
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", counter.load(Ordering::SeqCst));
// 型:
// AtomicU8..AtomicI64
// AtomicBool AtomicUsize
// 操作:
// fetch_add / fetch_sub
// compare_exchange
// load / store
// メモリオーダリング:
// Relaxed/Release/Acquire
// SeqCst が最も厳格
// ロックフリー、ブロックしない

スマートポインタ

Deref / Drop トレイト。Box / Rc / Arc が実装。ポインタ抽象と自動クリーンアップ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use std::ops::Deref;
// 独自のスマートポインタ:
struct MyBox<T>(T);
// Deref でデリファレンスを可能に:
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
// Drop で解放とクリーンアップ:
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
println!("释放");
}
}
fn main() {
let m = MyBox(5);
println!("{}", *m); // デリファレンス 5
// スコープを出ると自動で drop
// 標準のスマートポインタ:
// Box<T> ヒープ割り当て
// Rc<T> シングルスレッド共有
// Arc<T> マルチスレッド共有
// RefCell/Mutex 内部可変
// String/Vec もポインタ + 容量

10.トレイトとジェネリクス

トレイト抽象、実装、ジェネリック境界、多態。

trait 定義

トレイトは共有振る舞いを定義。メソッドシグネチャのみを宣言。インターフェースに類似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// trait の定義:
#[derive(Debug)]
struct Dog;
struct Cat;
// 振る舞いの抽象化:
trait Speak {
fn sound(&self) -> &'static str;
}
// 型に実装:
impl Speak for Dog {
fn sound(&self) -> &'static str {
"汪汪"
}
}
impl Speak for Cat {
fn sound(&self) -> &'static str {
"喵"
}
}
fn main() {
let dog = Dog;
let cat = Cat;
println!("{}", dog.sound());
println!("{}", cat.sound());
}
// trait はデフォルト実装を持てる
// 実装はメソッドシグネチャを満たす必要がある
// 同じ型に複数の trait を実装できる

trait の実装

impl Trait for Type で振る舞いを実装。オーファンルール: trait か型の少なくとも一方が自前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 組み込み型に独自 trait を実装:
trait Greeting {
fn hello(&self) -> String;
}
// &str に実装:
impl Greeting for &str {
fn hello(&self) -> String {
format!("你好,{}", self)
}
}
// Vec に実装:
impl Greeting for Vec<i32> {
fn hello(&self) -> String {
format!("{:?} 长度 {}", self, self.len())
}
}
fn main() {
println!("{}", "Nick".hello());
println!("{}", vec![1, 2].hello());
}
// 孤児ルール:
// trait または型の少なくとも一方がローカル
// それ以外はコンパイルエラー
// 空実装:
// マーカ型(sealed)
// ジェネリック実装は再利用できる

デフォルトメソッド

トレイトのメソッドは既定実装を持てます。上書きは任意。呼び出しは自動選択。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
trait Animal {
fn name(&self) -> String;
// デフォルト実装:
fn intro(&self) -> String {
format!("我是 {}", self.name())
}
}
struct Dog;
impl Animal for Dog {
fn name(&self) -> String {
"小狗".to_string()
}
// オーバーライド可:
fn intro(&self) -> String {
format!("{} 汪汪", self.name())
}
}
struct Cat;
impl Animal for Cat {
fn name(&self) -> String {
"小猫".to_string()
}
// デフォルトの intro を使う
}
fn main() {
println!("{}", Dog.intro()); // 小狗 汪汪
println!("{}", Cat.intro()); // 我是 小猫
}
// デフォルトメソッドは trait の他のメソッドを呼べる

トレイトオブジェクト

dyn Trait は動的ディスパッチ。異種コレクションを実行時に多相的に扱う。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
trait Draw {
fn draw(&self);
}
struct Circle;
struct Square;
impl Draw for Circle {
fn draw(&self) {
println!("画圆");
}
}
impl Draw for Square {
fn draw(&self) {
println!("画方");
}
}
// 動的ディスパッチ:
fn draw_all(shapes: &[&dyn Draw]) {
for s in shapes {
s.draw();
}
}
// ボックス化 trait オブジェクト:
fn make() -> Box<dyn Draw> {
Box::new(Circle)
}
fn main() {
let shapes: Vec<&dyn Draw> =
vec![&Circle, &Square];
draw_all(&shapes);
// 実行時にどの実装を呼ぶか決定
// dyn には実行時オーバーヘッドあり
// 静的ディスパッチはジェネリクスで高速
// オブジェクト安全性の制約
}

ジェネリクス

ジェネリック型と関数。T で抽象化、コード再利用。単態化でゼロコスト。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ジェネリック関数:
fn identity<T>(x: T) -> T {
x
}
// ジェネリック構造体:
struct Point<T> {
x: T,
y: T,
}
// 複数の型引数:
struct Pair<A, B> {
first: A,
second: B,
}
// ジェネリックメソッド:
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 1, y: 2 }; // i32 と推論
println!("{}", p.x());
let ip = Point { x: 1.5, y: 2.5 };
println!("{}", ip.x());
// 単相化:
// コンパイル時に具体的な版を生成
// 実行時オーバーヘッドなし
// dyn より高効率

ジェネリック境界

T: Trait で境界を指定。where 句、+ で複数境界の合成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 基本の制約:
fn print_it<T: std::fmt::Debug>(x: &T) {
println!("{:?}", x);
}
// where 句:
fn complex<T, U>(a: &T, b: &U)
where
T: std::fmt::Display + Clone,
U: std::fmt::Debug,
{
println!("{}", a);
println!("{:?}", b);
}
// 戻り値のジェネリクス:
fn pair() -> impl std::fmt::Display {
42 // 不透明な戻り型
}
// trait 制約付きジェネリック集合:
fn sortable<T: Ord>(list: &mut [T]) {
list.sort();
}
// 制約によりジェネリックメソッドが使える
// impl Trait 引数の省略記法
// 複数の制約は + で組み合わせる

impl ブロック

impl ブロックで関連関数・関連メソッドを定義。self / Self、コンストラクタ慣習。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// 関連関数(self なし):
fn new(w: u32, h: u32) -> Rectangle {
Rectangle { width: w, height: h }
}
// メソッド(&self):
fn area(&self) -> u32 {
self.width * self.height
}
// 可変メソッド(&mut self):
fn set_width(&mut self, w: u32) {
self.width = w;
}
// self を消費:
fn into_string(self) -> String {
format!("{}x{}", self.width, self.height)
}
}
fn main() {
let mut r = Rectangle::new(2, 3);
println!("{}", r.area());
r.set_width(5);
// Self 型は impl 内で自身を指す
// 複数の impl ブロックに分けられる
}

合成と多態

Rust には継承がありません。trait + 合成で再利用。既定実装が基底クラスの役割を果たす。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 継承の代わりに合成:
struct Engine {
power: u32,
}
impl Engine {
fn start(&self) {
println!("引擎启动 {} 马力", self.power);
}
}
struct Car {
engine: Engine, // 合成
model: String,
}
impl Car {
fn start(&self) {
self.engine.start(); // 委譲
println!("{} 发车", self.model);
}
}
// trait のデフォルトメソッドを基底クラスの代わりに:
trait Fly {
fn fly(&self) {
println!("飞行");
}
}
struct Bird;
impl Fly for Bird {} // デフォルトを使用
// 多態:
fn make_fly(f: &dyn Fly) {
f.fly();
}
fn main() {
let car = Car {
engine: Engine { power: 150 },
model: String::from("Guru"),
};
car.start();
}
// 合成の方が柔軟で菱形継承問題がない

11.エラー処理

Result、Option、? 演算子、panic、カスタムエラー。

Result

Result<T, E> は成功値またはエラーを返す。Ok / Err のバリアントを持つ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::fs::File;
fn main() {
// Result 列挙型:
let result = File::open("nope.txt");
// match で処理:
match result {
Ok(file) => println!("打开成功"),
Err(e) => println!("打开失败: {}", e),
}
// ジェネリックシグネチャ:
// enum Result<T, E> {
// Ok(T),
// Err(E),
// }
// 手動で構築:
let ok: Result<i32, String> = Ok(42);
let err: Result<i32, String> =
Err(String::from("失败"));
// メソッド:
let v = ok.unwrap_or(0);
let v2 = ok.unwrap_or_else(|_| -1);
}

unwrap と expect

unwrap は成功なら値を取り、失敗で panic。expect は panic メッセージを付けます。デバッグ用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let ok: Result<i32, &str> = Ok(10);
// unwrap:
let v = ok.unwrap();
println!("{}", v);
// 失敗時は panic:
// let e: Result<i32, &str> = Err("bad");
// e.unwrap(); // panic!
// expect はメッセージ付き:
// let e = Err("bad");
// e.expect("应该成功"); // panic メッセージ
// 適した場面:
// プロトタイプ/サンプル/テスト
// 論理上失敗し得ない
// 本番での推奨:
// ? で伝播
// または match で処理
// unwrap_or でデフォルト値を提供

? 演算子

? でエラー伝播を簡潔に。Err は自動で戻り、Ok は展開して継続。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::fs::File;
use std::io::Read;
// エラーの伝播:
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut f = File::open(path)?; // Err は直接 return
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
// チェーン:
fn first_line(path: &str) -> Result<String, std::io::Error> {
let content = read_file(path)?;
Ok(content.lines().next().unwrap_or("").to_string())
}
// ? はエラーを自動変換:
// From 実装が必要
// fn f() -> Result<(), Box<dyn Error>> {
// File::open("x")?; // io::Error が自動ボックス化
// Ok(())
// }
// 関数は Result/Option を返す必要がある

Error の実装

標準の Error トレイト。Display + Debug で表示。エラーチェーン。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::error::Error;
use std::fmt;
// 独自のエラー型:
#[derive(Debug)]
struct MyError {
msg: String,
}
// Display:
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "发生错误: {}", self.msg)
}
}
// Error を実装:
impl Error for MyError {}
fn main() {
let e = MyError { msg: "xxx".to_string() };
println!("{}", e); // Display
println!("{:?}", e); // Debug
println!("{}", e.source()); // 発生源なし
}
// Error trait は Display + Debug を要求
// source() は根本エラーを返す
// ? とエラーチェーンに使用

カスタムエラー

独自のエラー型を設計。列挙バリアントにコンテキストを保持。エラーを分類。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#[derive(Debug)]
// 分類エラー:
enum AppError {
NotFound(String),
InvalidInput { field: String, reason: String },
Io(std::io::Error),
}
// Display を手動実装:
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AppError::NotFound(msg) =>
write!(f, "未找到: {}", msg),
AppError::InvalidInput { field, reason } =>
write!(f, "{} 无效: {}", field, reason),
AppError::Io(e) => write!(f, "IO: {}", e),
}
}
}
impl std::error::Error for AppError {}
// 変換:
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> AppError {
AppError::Io(e)
}
}
// 使用:
// fn f() -> Result<(), AppError> {
// std::fs::read_to_string("x")?; // 自動変換
// Ok(())
// }

thiserror

thiserror の derive マクロで Display / Error を自動実装。ボイラープレートを大幅削減。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Cargo.toml に追加:
// thiserror = "2"
use thiserror::Error;
// 派生マクロ:
#[derive(Error, Debug)]
// エラー列挙型:
#[derive(Error, Debug)]
enum DataError {
#[error("记录不存在: {0}")]
NotFound(String),
#[error("校验失败: {field} {reason}")]
Validation {
field: String,
reason: String,
},
#[error(transparent)]
Io(#[from] std::io::Error),
}
// 自動で得られる:
// Display 実装
// Error 実装
// From<std::io::Error> 変換
fn main() {
let e = DataError::NotFound("id".into());
println!("{}", e);
}
// 手書きのボイラープレートを大幅削減
// エコシステムの標準的作法

Option

Option<T> は値が無い可能性を表します。Some / None。安全にアクセス。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
// Option 型:
let some = Some(5);
let none: Option<i32> = None;
// match:
match some {
Some(v) => println!("值 {}", v),
None => println!("无值"),
}
// よく使うメソッド:
let x = some.unwrap_or(0);
let y = some.unwrap_or_else(|| compute());
let z = none.unwrap_or(0); // 0
// チェーン:
let doubled = some.map(|v| v * 2);
let filtered = some.filter(|v| v > 3);
let flat = Some(Some(1)).flatten();
// 組み合わせ:
let a = Some(1);
let b = Some(2);
let sum = a.zip(b).map(|(x, y)| x + y);
// よくある使い方:
// Vec::get / HashMap::get
// ? で None を伝播

panic

panic! は回復不能なエラー。スタックを巻き戻しプログラムを終了。バグ時のみ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
// panic を発生:
// panic!("致命错误");
// インデックス越え:
let v = vec![1, 2, 3];
// let x = v[10]; // panic!
// 空スライスの先頭:
let empty: &[i32] = &[];
// let x = empty[0]; // panic!
// panic の挙動:
// エラーメッセージを出力
// スタックを巻き戻し Drop を実行
// プログラムは終了
// カスタムメッセージ:
// panic!("索引 {} 越界", 10);
// 適した場面:
// 続行不能なバグ
// 契約違反
// 回復可能なら Result
}

12.入出力

標準入出力、ファイルの読み書き、ファイルシステム。

入力の読み込み

stdin から read_line で 1 行を String に読み込み。trim で改行を除去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::io;
fn main() {
// 1 行読み取り:
let mut input = String::new();
io::stdin().read_line(&mut input)
.expect("读取失败");
// 改行を除去:
let trimmed = input.trim();
println!("你输入了: {}", trimmed);
// 数字を読み取り:
let mut line = String::new();
io::stdin().read_line(&mut line).unwrap();
let n: i32 = line.trim().parse().unwrap();
println!("数字: {}", n);
// ループで読み取り:
loop {
let mut s = String::new();
let bytes = io::stdin().read_line(&mut s).unwrap();
if bytes == 0 { break; } // EOF
print!("{}", s);
}
}

ファイルの読み込み

read_to_string で一括読み込み。Read トレイトでストリーム読み込み。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::fs;
fn main() {
// 一度に文字列として読み取り:
let content = fs::read_to_string("data.txt")
.expect("读取失败");
println!("{}", content);
// バイトとして読み取り:
let bytes = fs::read("data.bin").unwrap();
// ストリーム読み取り(大ファイル):
use std::io::{Read, BufReader};
let f = fs::File::open("big.log").unwrap();
let mut reader = BufReader::new(f);
let mut buf = String::new();
reader.read_to_string(&mut buf).unwrap();
// 行ごとに読み取り:
use std::io::BufRead;
let f = fs::File::open("data.txt").unwrap();
for line in std::io::BufReader::new(f).lines() {
println!("{}", line.unwrap());
}
}

ファイルの書き込み

write / append で書き込み。OpenOptions でモード制御。バッファ書き込み。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::fs;
fn main() {
// 上書き書き込み:
fs::write("out.txt", "hello\n")
.expect("写入失败");
// 追記:
use std::fs::OpenOptions;
use std::io::Write;
let mut f = OpenOptions::new()
.append(true)
.create(true)
.open("log.txt")
.unwrap();
f.write_all(b"more data\n").unwrap();
// バッファ書き込み:
let mut writer = std::io::BufWriter::new(
fs::File::create("buf.txt").unwrap());
writeln!(writer, "第 {} 行", 1).unwrap();
writer.flush().unwrap(); // ディスクにフラッシュ
// フォーマット書き込み:
// write!(f, "{}", value)
}

ファイルシステム

ディレクトリ作成、削除、リネーム、存在判定。パス操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::fs;
fn main() {
// ディレクトリ作成:
fs::create_dir("data").unwrap();
fs::create_dir_all("data/a/b").unwrap();
// 削除:
fs::remove_file("tmp.txt").unwrap();
fs::remove_dir("data/a").unwrap();
fs::remove_dir_all("data").unwrap();
// リネーム:
fs::rename("a.txt", "b.txt").unwrap();
// 存在確認:
if fs::metadata("b.txt").is_ok() {
println!("存在");
}
// メタデータ:
let meta = fs::metadata("b.txt").unwrap();
println!("大小: {}", meta.len());
println!("目录: {}", meta.is_dir());
// コピー:
fs::copy("b.txt", "c.txt").unwrap();
}

BufRead 行ごと

BufRead トレイットで行単位読み込み。lines()、read_line、split。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::io::BufRead;
use std::io::BufReader;
use std::fs::File;
fn main() {
let f = File::open("data.txt").unwrap();
let reader = BufReader::new(f);
// 行ごと:
for line in reader.lines() {
let l = line.unwrap();
println!("{}", l);
}
// 行番号付き:
let f = File::open("data.txt").unwrap();
for (i, line) in
BufReader::new(f).lines().enumerate() {
println!("{}: {}", i + 1, line.unwrap());
}
// 1 行を手動読み取り:
// let mut buf = String::new();
// reader.read_line(&mut buf)
// バイト分割:
// for part in reader.split(b",") {}
// stdin にも使える
}

出力と書き込み

print! / println! / eprintln!。write! / writeln! は任意のライターへ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::io::{self, Write};
fn main() {
// 標準出力:
println!("普通输出");
print!("无换行");
// 標準エラー:
eprintln!("错误信息");
// Vec に書き込み:
let mut buf = Vec::new();
writeln!(buf, "写入 Vec {}", 1).unwrap();
println!("{:?}", buf);
// String に書き込み:
let mut s = String::new();
write!(s, "格式化 {}", 42).unwrap();
println!("{}", s);
// ファイルへはファイル節を参照
// io::stdout() も使用可:
// let mut out = io::stdout();
// writeln!(out, "直接写 stdout")
// Flush で手動フラッシュ
}

標準ストリーム

stdin / stdout / stderr。インタラクティブとパイプの両方に対応。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::io::{self, Read, Write};
fn main() {
// パイプで全部読み取り:
// echo data | program
let mut buf = String::new();
io::stdin().read_to_string(&mut buf).unwrap();
// パイプを行ごと処理:
// for line in io::stdin().lines() {
// println!("{} ", line.unwrap());
// }
// stdout に出力:
println!("处理完成");
// stderr にエラー:
eprintln!("警告: 非致命");
// stdout に書き込み:
// io::stdout().write_all(b"raw\n")
// 対話のプロンプト:
// print!("输入名字: ");
// io::stdout().flush().unwrap();
}

バイト I/O

read でバイトをバッファに。バイナリをストリーム処理。write_all で全部書き込み。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::fs::File;
use std::io::{self, Read, Write};
fn main() {
// バッファに読み取り:
let mut f = File::open("data.bin").unwrap();
let mut buf = [0u8; 1024];
let n = f.read(&mut buf).unwrap();
println!("读取 {} 字节", n);
// EOF までループ読み取り:
let mut f = File::open("data.bin").unwrap();
let mut total = 0;
let mut buf = [0u8; 256];
loop {
let n = f.read(&mut buf).unwrap();
if n == 0 { break; } // EOF
total += n;
// buf[..n] を処理
}
// 全部書き込み:
let mut out = File::create("out.bin").unwrap();
out.write_all(&[1, 2, 3, 4]).unwrap();
// 全バイト読み取り:
let all = std::fs::read("data.bin").unwrap();
println!("总 {} 字节", all.len());
}

13.よくある落とし穴

Rust 初学者が最も踏みやすい落とし穴と正しい書き方。

ムーブ後の使用

値がムーブされた後は利用できない。所有権を保ちたい場合は借用するか clone を使う。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// BAD:移動後にアクセス
fn main() {
let s = String::from("hi");
let t = s; // 移動
// println!("{}", s); // エラー:ムーブ済み
println!("{}", t);
}
// GOOD:借用
fn main() {
let s = String::from("hi");
let t = &s; // 借用
println!("{} {}", s, t);
}
// GOOD:明示的クローン
let s = String::from("hi");
let t = s.clone();
// 場面:
// 関数に値を渡すと所有権が移る
// 2 箇所で使う必要がある
// 借用が高効率、clone はより直接

借用競合

不変借用と可変借用は同時に存在できません。スコープで借用範囲を縮める。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// BAD:同時借用
fn main() {
let mut v = vec![1, 2, 3];
// let r1 = &v; // 不変
// v.push(4); // 可変、競合
// println!("{:?}", r1);
}
// GOOD:不変借用を先に終える
fn main() {
let mut v = vec![1, 2, 3];
let r1 = &v[0];
println!("{}", r1); // 借用終了
v.push(4); // これで許可
}
// GOOD:スコープを分ける
{
let r = &v;
println!("{:?}", r);
} // 借用終了
v.push(4);
// ルール:
// 読み読みは可、読み書きは不可
// コンパイラがデータ競合なしを保証

ダングリング参照

ローカル変数の参照を返すとダングリング参照になる。所有権を持つ値で返すかライフタイムを付けて返す。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// BAD:ローカル参照を返す
// fn bad() -> &String {
// let s = String::from("x");
// &s // エラー:s は解放される
// }
// GOOD:所有権を返す
fn good() -> String {
String::from("x")
}
// GOOD:入力を参照する
fn first_char(s: &str) -> &str {
&s[..1]
}
// GOOD:ライフタイム引数
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// ルール:
// 返す参照は有効でなければならない
// ヒープや引数のライフタイムを指す
// コンパイラ(借用チェッカ)が遮断

String の連結

+ は左辺をムーブします。繰り返し + は非効率。format! か push_str を使う。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// BAD:+ の繰り返し結合
let mut s = String::new();
// for _ in 0..10 {
// s = s + "x"; // 毎回再確保
// }
// GOOD:push_str で追記
let mut s = String::new();
for _ in 0..10 {
s.push_str("x");
}
// GOOD:format! で一括
let a = "hello";
let b = "world";
let s = format!("{}, {}", a, b);
// + の構文:
let s1 = String::from("a");
let s2 = String::from("b");
let s3 = s1 + &s2; // s1 は移動
// 理由:
// + は左辺に String が必要
// 大量結合は容量拡大が頻繁
// format!/join が優れている

インデックス型

インデックスと長さは usize。コレクションのインデックス型が合わないケースに注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// BAD:i32 インデックス
fn main() {
let v = vec![1, 2, 3];
let i: i32 = 1;
// let x = v[i]; // エラー:usize が必要
// let x = v[i as usize]; // 変換
}
// GOOD:直接 usize
fn main() {
let v = vec![1, 2, 3];
let i: usize = 1;
let x = v[i]; // 正しい
println!("{}", x);
}
// 場面:
// for (i, x) in v.iter().enumerate()
// 返るのは usize
// len() も usize
// 距離の計算で型が合わない時:
// as usize で変換
// または変数の型を調整

match 分岐の型

match の全分岐は同じ戻り値型を持つ必要があります。欠けた分岐は _ で網羅。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// BAD:分岐の型が不一致
// let x = match 5 {
// 1 => "one",
// _ => 42, // エラー:str vs i32
// };
// GOOD:戻り型を統一
let x = match 5 {
1 => "one",
_ => "other", // どちらも &str
};
// BAD:網羅的でない
// enum Color { R, G, B }
// let c = Color::R;
// match c {
// Color::R => {}
// Color::G => {}
// // B が欠落、コンパイルエラー
// }
// GOOD:ワイルドカードを追加
// match c {
// Color::R => {}
// _ => {}
// }
// ルール:
// 網羅性はコンパイル時に検査
// 型推論は最初の分岐に従う

クロージャのキャプチャ

クロージャは自動で借用またはムーブ。同時借用は競合。move で明示的にムーブ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// BAD:クロージャの借用と変更が衝突
fn main() {
let mut count = 0;
// let print = || println!("{}", count);
// count += 1; // 衝突:借用済み
// print();
}
// GOOD:クロージャを先に終える
fn main() {
let mut count = 0;
{
let print = || println!("{}", count);
print();
} // 借用終了
count += 1;
}
// GOOD:move クロージャ
let data = vec![1, 2];
let handler = move || {
println!("{:?}", data); // 所有権を持つ
};
// ルール:
// Fn / FnMut / FnOnce
// キャプチャ方式で自動選択
// スレッドへ渡すクロージャは move 必要
// 衝突時は借用範囲を狭める

整数オーバーフロー

デバッグビルドはオーバーフローで panic。リリースビルドは折り返し。明示処理か大きい型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// BAD:デバッグビルドでオーバーフロー panic
// fn main() {
// let x: u8 = 255;
// let y = x + 1; // デバッグで panic!
// }
// GOOD:明示的に検査
fn main() {
let x: u8 = 255;
match x.checked_add(1) {
Some(v) => println!("{}", v),
None => println!("溢出"),
}
// 他のメソッド:
// x.checked_add / checked_mul
// x.wrapping_add // 回り込み
// x.saturating_add // 上限で飽和
// x.overflowing_add // 桁あふれを返す
// リリースビルドは既定で回り込み
// 大きなカウンタは u64/usize
// 明確な意味でメソッドを選ぶ

14.並行処理

スレッド、channel、共有状態、Send / Sync。

スレッド

thread::spawn で新スレッド起動。join で完了待機。戻り値も受け取れる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::thread;
use std::time::Duration;
fn main() {
// スレッドを起動:
let handle = thread::spawn(|| {
for i in 1..3 {
println!("子线程 {}", i);
thread::sleep(Duration::from_millis(10));
}
});
// メインスレッドは続行:
for i in 1..3 {
println!("主线程 {}", i);
}
// 子スレッドの終了を待つ:
handle.join().unwrap();
// join で戻り値を取得:
let h = thread::spawn(|| 42);
let result = h.join().unwrap();
println!("{}", result); // 42
}

move クロージャ

move クロージャはキャプチャした値をスレッド内へムーブし、借用によるダングリングを防ぐ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::thread;
fn main() {
let data = String::from("hello");
// スレッドが data を借用:
let h1 = thread::spawn(move || {
println!("{} 世界", data);
});
// data は移動済み、メインでは使えない
// マルチスレッドで共有するには:
// Arc を使う(共有節を参照)
// ループ内の move:
let mut handles = vec![];
for i in 0..3 {
handles.push(thread::spawn(move || {
println!("线程 {}", i);
}));
}
for h in handles {
h.join().unwrap();
}
// move なしクロージャ:
// 参照をキャプチャ、メインを借用するかも
// スレッドが借用より長生きし得る
// move は所有権の移動を明示

channel

mpsc はマルチプロデューサー・シングルコンシューマー。send / recv でデータ転送。型安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::sync::mpsc;
use std::thread;
fn main() {
// チャネルを作成:
let (tx, rx) = mpsc::channel();
// 送信側を子スレッドへ移動:
thread::spawn(move || {
tx.send(String::from("hello")).unwrap();
});
// 受信:
let received = rx.recv().unwrap();
println!("收到: {}", received);
// recv はブロックして待機
// 送信者が全部 Drop されると recv はエラー
// try_recv は非ブロック:
// match rx.try_recv() {
// Ok(v) => ...,
// Err(_) => println!("暂无数据"),
// }
// 反復で受信:
// for v in rx {
// println!("{}", v);
// }

複数プロデューサー

Sender をクローンして複数プロデューサー。レシーバは共有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
// 送信端をクローン:
let tx1 = tx.clone();
let tx2 = tx.clone();
// 3 つの生産者:
thread::spawn(move || {
tx.send(1).unwrap();
});
thread::spawn(move || {
tx1.send(2).unwrap();
});
thread::spawn(move || {
tx2.send(3).unwrap();
});
// メインスレッドが消費:
for _ in 0..3 {
let v = rx.recv().unwrap();
println!("收到 {}", v);
}
// 受信順序は保証されない
// 単一消費者:
// mpsc は rx を 1 つだけ許可
// 複数消費者は分割やミューテックスが必要
// チャネルはメッセージ受渡しの第一候補
// 共有可変状態を避ける

共有可変

Arc<Mutex<T>> で複数スレッド間の共有可変データ。ロックがアクセスを保護。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// 共有カウンタ:
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1; // クリティカルセクション
}));
}
for h in handles {
h.join().unwrap();
}
println!("结果: {}", *counter.lock().unwrap());
// Mutex は相互排他を保証
// Arc は参照カウントをスレッドセーフに
// ロック Guard はスコープを出ると解放
// ロック保持時間はなるべく短く
// ロック内で再ロックする呼び出しを避ける
// デッドロックは順序に注意

scoped スレッド

thread::scope で 'static でないデータを借用。所有権のムーブ不要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::thread;
fn main() {
let mut data = vec![1, 2, 3];
// スコープ内データを借用:
thread::scope(|s| {
// 不変借用:
s.spawn(|| {
println!("{:?}", data);
});
// 可変借用(スレッド別):
s.spawn(|| {
data.push(4); // このスレッドのみ
});
}); // スコープ終了、全スレッド完了
println!("{:?}", data);
// thread::spawn との違い:
// spawn は 'static 所有権が必要
// scope はローカル変数を借用できる
// scope 終了時に自動 join
// 適用:ローカルデータの並列処理
}

rayon 並列

rayon の並列イテレータ。par_iter を iter の代わりに使うだけ。データ並列が簡単に。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Cargo.toml:
// rayon = "1"
use rayon::prelude::*;
fn main() {
let v: Vec<i32> = (0..1000).collect();
// 並列で合計:
let sum: i32 = v.par_iter().sum();
println!("{}", sum);
// 並列マップ:
let doubled: Vec<i32> =
v.par_iter().map(|x| x * 2).collect();
// 並列フィルタ:
let evens: Vec<&i32> =
v.par_iter().filter(|x| **x % 2 == 0).collect();
// 並列ソート:
let mut sorted = vec![3, 1, 2];
sorted.par_sort();
// データは Send である必要
// 自動で分割して並列化
// 大きな集合で効果が明確
// 小さい集合はオーバーヘッド大
// 結果は直列と一致

Send と Sync

Send はスレッド間で移動可能。Sync は参照共有可能。並行の安全性をコンパイル時に保証。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Send:所有権をスレッド間で移せる
// 大多数の型は Send
// 例外:Rc<T> は Send でない
// Sync:&T で共有できる
// 例外:RefCell は Sync でない
use std::rc::Rc;
use std::thread;
fn main() {
// Rc はスレッドに入れられない:
// let rc = Rc::new(5);
// thread::spawn(move || println!("{}", rc));
// エラー:Rc は Send でない
// Arc で代替:
let arc = std::sync::Arc::new(5);
let h = thread::spawn(move || println!("{}", arc));
h.join().unwrap();
// ルール:
// Send 型はスレッドを跨げる
// Sync 型は参照共有が安全
// コンパイラがスレッド間の使用を検査
// 独自型は自動派生
// unsafe impl は慎重に

15.ネットワーク

TCP、UDP、HTTP、サードパーティ製ネットワーク crate。

TCP 接続

TcpStream で接続を確立。read / write でバイト送受信。ストリーム型プロトコル。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::net::TcpStream;
use std::io::{Read, Write};
fn main() {
// 接続を確立:
let mut stream = TcpStream::connect(
"example.com:80").unwrap();
// HTTP リクエストを送信:
stream.write_all(
b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
.unwrap();
// 応答を読み取り:
let mut buf = [0u8; 4096];
let n = stream.read(&mut buf).unwrap();
println!("{}", String::from_utf8_lossy(&buf[..n]));
// 接続エラーの処理:
// TcpStream::connect は Result を返す
// タイムアウト設定:
// stream.set_read_timeout(Some(Duration::from_secs(5)))
// 双方向ストリーム通信
}

TCP リッスン

TcpListener でバインドして accept。複数接続はスレッドで処理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
use std::thread;
fn main() {
// ポートにバインド:
let listener = TcpListener::bind("127.0.0.1:8080")
.unwrap();
println!("监听 8080");
// 接続を受け付け:
for stream in listener.incoming() {
match stream {
Ok(stream) => {
// 接続ごとにスレッド:
thread::spawn(|| handle(stream));
}
Err(e) => eprintln!("连接错误: {}", e),
}
}
}
fn handle(mut stream: TcpStream) {
let mut buf = [0u8; 1024];
let n = stream.read(&mut buf).unwrap();
println!("收到 {} 字节", n);
stream.write_all(&buf[..n]).unwrap();
}

HTTP リクエスト

reqwest で HTTP リクエストを送信。JSON リクエスト/レスポンスに対応。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Cargo.toml:
// reqwest = { version = "0.12", features = ["json"] }
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// GET リクエスト:
let resp = reqwest::Client::new()
.get("https://api.example.com/users")
.timeout(Duration::from_secs(10))
.send()
.await?;
// ステータスコード:
println!("状态: {}", resp.status());
// テキストを読み取り:
let body = resp.text().await?;
println!("{}", &body[..200]);
Ok(())
}
// tokio ランタイムが必要:
// tokio = { version = "1", features = ["full"] }
// JSON:
// .json::<T>() でデシリアライズ
// serde と組み合わせる

HTTP サーバー

axum で HTTP サービスを構築。ルーティングとハンドラ。非同期対応。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Cargo.toml:
// axum = "0.7"
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use axum::{Router, routing::get};
use axum::response::Json;
use serde_json::{json, Value};
#[tokio::main]
async fn main() {
// ルーティング:
let app = Router::new()
.route("/", get(root))
.route("/api/ping", get(ping));
// 起動:
let listener = tokio::net::TcpListener::bind(
"127.0.0.1:3000").await.unwrap();
println!("服务运行在 3000");
axum::serve(listener, app).await.unwrap();
}
async fn root() -> &'static str {
"Hello, GuruToolkit!"
}
async fn ping() -> Json<Value> {
Json(json!({ "ok": true }))
}
// ルートパラメータ:
// .route("/user/{id}", get(user_by_id))

URL 処理

url crate で URL のパース・構築。クエリパラメータ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Cargo.toml:
// url = "2"
use url::Url;
fn main() {
// 解析:
let url = Url::parse(
"https://[email protected]/path?q=rust#sec")
.unwrap();
// フィールドにアクセス:
println!("{}", url.scheme()); // https
println!("{}", url.host_str().unwrap());
println!("{}", url.path());
println!("{}", url.fragment().unwrap());
// クエリパラメータ:
let pairs: Vec<(String, String)> =
url.query_pairs().into_owned().collect();
for (k, v) in pairs {
println!("{}={}", k, v);
}
// URL を構築:
let mut u = Url::parse("https://api.com").unwrap();
u.set_path("/v1/users");
u.query_pairs_mut()
.append_pair("page", "2");
println!("{}", u);
}

IP パース

std::net::IpAddr で IP をパース。IPv4 / IPv6 を判別。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
fn main() {
// IP を解析:
let ip: IpAddr = "192.168.1.1".parse().unwrap();
println!("{}", ip);
// バージョン判定:
match ip {
IpAddr::V4(v) => println!("IPv4: {}", v),
IpAddr::V6(v) => println!("IPv6: {}", v),
}
// 構築:
let v4 = Ipv4Addr::new(127, 0, 0, 1);
let v6: Ipv6Addr = "::1".parse().unwrap();
// プロパティ:
println!("回环: {}", v4.is_loopback());
println!("私有: {}", v4.is_private());
// ポートと組み合わせ:
let addr = "127.0.0.1:8080".parse::<std::net::SocketAddr>().unwrap();
println!("{}", addr.port());
// ドメイン解決:
// use std::net::ToSocketAddrs;
// let mut it = "example.com:80".to_socket_addrs().unwrap();
}

UDP

UdpSocket はコネクションレスのデータグラム。send_to / recv_from。ハンドシェイク不要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::net::UdpSocket;
fn main() {
// ローカルにバインド:
let socket = UdpSocket::bind("127.0.0.1:0")
.unwrap();
// データグラムを送信:
socket.send_to(b"hello", "127.0.0.1:9999")
.unwrap();
// 受信:
let mut buf = [0u8; 1024];
let (n, src) = socket.recv_from(&mut buf).unwrap();
println!("来自 {}: {}", src,
String::from_utf8_lossy(&buf[..n]));
// サーバー側:
let server = UdpSocket::bind("127.0.0.1:9999")
.unwrap();
// 特徴:
// コネクションレス、ハンドシェイクなし
// パケット喪失・順序入れ替えの可能性
// TCP より高効率
// 適している:DNS、動画、ゲーム
// 信頼性のある転送は自前で実装
}

DNS 解決

ToSocketAddrs でホスト名を解決。アドレスのイテレータを返す。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::net::ToSocketAddrs;
fn main() {
// ドメイン解決:
let addrs = "example.com:80"
.to_socket_addrs().unwrap();
for addr in addrs {
println!("{}", addr);
}
// ポート既定:
let addrs = "example.com"
.to_socket_addrs().unwrap();
// 解決は複数アドレスを返すかも
// 順に接続を試す
// エラー処理:
match "bad-domain.invalid:80".to_socket_addrs() {
Ok(it) => println!("解析成功"),
Err(e) => println!("解析失败: {}", e),
}
// 非同期 DNS:
// サードパーティの tokio::net
// 本番では resolv-conf
// テストではローカル hosts

16.時間と日付

SystemTime、Instant、Duration、chrono。

SystemTime

SystemTime はシステム時刻。現在時刻を取得して UNIX エポックと比較。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::time::SystemTime;
fn main() {
// 現在時刻:
let now = SystemTime::now();
// UNIX エポック:
let epoch = SystemTime::UNIX_EPOCH;
// エポックからの経過:
match now.duration_since(epoch) {
Ok(d) => println!("距纪元 {:?}", d),
Err(e) => println!("系统时间早于纪元: {:?}", e),
}
// 比較:
let later = SystemTime::now();
let elapsed = later.duration_since(now).unwrap();
println!("经过 {:?}", elapsed);
// 制限:
// 不正確(システムクロックが変わり得る)
// 計測には Instant を使う
// 日付処理には chrono
// タイムスタンプ:
let secs = now.duration_since(epoch)
.unwrap().as_secs();
println!("{} 秒", secs);
}

Instant 計測

Instant はモノトニッククロック。経過時間の計測に使い、システム時刻変更の影響を受けない。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::time::Instant;
fn main() {
// 計測開始:
let start = Instant::now();
// 時間のかかる処理:
std::thread::sleep(std::time::Duration::from_millis(50));
// 経過時間:
let elapsed = start.elapsed();
println!("耗时: {:?}", elapsed);
// 異なる単位:
println!("{} 微秒", elapsed.as_micros());
println!("{} 毫秒", elapsed.as_millis());
// 複数の起点を比較:
let a = Instant::now();
let b = Instant::now();
let diff = b.duration_since(a);
// 使用場面:
// 性能ベンチ、タイムアウト制御
// 単調増加、クロックの影響を受けない
// プロセス間では比較できない
}

Duration

Duration は時間の長さ。秒 / ナノ秒精度。演算と単位変換。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::time::Duration;
fn main() {
// 作成:
let sec = Duration::from_secs(5);
let ms = Duration::from_millis(1500);
let us = Duration::from_micros(100);
let ns = Duration::from_nanos(1);
// 組み合わせ:
let d = Duration::new(2, 500_000_000); // 2.5s
// 演算:
let sum = sec + ms;
let mul = sec * 2;
// 比較:
let a = Duration::from_secs(1);
let b = Duration::from_millis(500);
println!("{} > {}", a > b, a > b);
// 変換:
println!("{} 秒", sec.as_secs());
println!("{} 毫秒", ms.as_millis());
// 用途:
// sleep、タイムアウト、待機
// ネットワークのタイムアウト設定
}

chrono 現在時刻

chrono crate で日時を扱う。Utc / Local タイムゾーン。人間に可読な書式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Cargo.toml:
// chrono = "0.4"
use chrono::{Utc, Local, NaiveDate};
fn main() {
// UTC 時刻:
let utc = Utc::now();
println!("{}", utc);
// ローカル時刻:
let local = Local::now();
println!("{}", local);
// フィールドにアクセス:
println!("年: {}", utc.year());
println!("月: {}", utc.month());
println!("日: {}", utc.day());
println!("时: {}", utc.hour());
println!("星期: {}", utc.weekday());
// タイムスタンプ:
println!("{}", utc.timestamp());
// 日付を構築:
let d = NaiveDate::from_ymd_opt(2026, 8, 2)
.unwrap();
println!("{}", d);
// タイムゾーン表記:
// +00:00 UTC / +08:00 中国
}

フォーマット出力

format で日時を整形。strftime 風のプレースホルダ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// chrono = "0.4"
use chrono::Utc;
fn main() {
let now = Utc::now();
// フォーマット:
println!("{}", now.format("%Y-%m-%d")); // 2026-08-02
println!("{}", now.format("%H:%M:%S")); // 14:30:22
println!("{}", now.format("%Y-%m-%d %H:%M"));
// よく使うプレースホルダ:
// %Y 年 %m 月 %d 日
// %H 時 %M 分 %S 秒
// %A 曜日名 %a 短縮形
// %B 月名
// %j 年内の通算日
// カスタム:
println!("{}", now.format("%Y/%m/%d %H:%M:%S %z"));
// 既定の Display:
println!("{}", now);
// String を返す:
let s = now.format("%F").to_string();
println!("{}", s); // ISO 日付
}

文字列のパース

DateTime::parse_from_str で文字列をパース。ISO 8601 も対応。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// chrono = "0.4"
use chrono::{DateTime, NaiveDateTime, Utc};
fn main() {
// 固定フォーマットを解析:
let dt = DateTime::parse_from_str(
"2026-08-02 10:30:00 +0800",
"%Y-%m-%d %H:%M:%S %z").unwrap();
println!("{}", dt);
// ISO 8601:
let iso = DateTime::parse_from_rfc3339(
"2026-08-02T10:30:00+08:00").unwrap();
println!("{}", iso);
// タイムゾーンなしで解析:
let naive = NaiveDateTime::parse_from_str(
"2026-08-02 10:30:00",
"%Y-%m-%d %H:%M:%S").unwrap();
println!("{}", naive);
// UTC に変換:
let utc: DateTime<Utc> = iso.with_timezone(&Utc);
println!("{}", utc);
// 解析失敗は Err を返す
}

時刻演算

時刻の加算・減算、差分計算、範囲判定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// chrono = "0.4"
use chrono::{Duration, Utc, Local};
fn main() {
let now = Utc::now();
// 加減算:
let tomorrow = now + Duration::days(1);
let next_hour = now + Duration::hours(1);
let last_week = now - Duration::weeks(1);
println!("{}", tomorrow.format("%F"));
// 間隔:
let later = now + Duration::minutes(90);
let diff = later - now;
println!("间隔 {} 分钟", diff.num_minutes());
// 日付範囲の判定:
let start = now - Duration::hours(1);
let end = now + Duration::hours(1);
let now_local = Local::now();
let n = now_local.with_timezone(&Utc);
if n >= start && n <= end {
println!("在范围内");
}
// タイムゾーン:
// FixedOffset::east_opt(8 * 3600)
// 中国標準時 +08:00
}

タイムゾーン処理

FixedOffset は固定タイムゾーン。ローカルオフセットを扱い、ゾーン間変換も可能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// chrono = "0.4"
use chrono::{Utc, Local, FixedOffset, TimeZone};
fn main() {
// 固定タイムゾーン(中国 +08:00):
let cst = FixedOffset::east_opt(8 * 3600).unwrap();
let now = Utc::now();
let beijing = now.with_timezone(&cst);
println!("北京: {}", beijing);
// ローカルタイムゾーン:
let local = Local::now();
println!("本地: {}", local);
// UTC に戻す:
let utc = beijing.with_timezone(&Utc);
println!("UTC: {}", utc);
// 任意のタイムゾーン:
let tokyo = FixedOffset::east_opt(9 * 3600).unwrap();
let tokyo_now = now.with_timezone(&tokyo);
println!("东京: {}", tokyo_now);
// オフセット:
println!("偏移: {}", cst); // +08:00
// 保存は UTC を推奨
// 表示時にタイムゾーン変換
}

17.プロセスと環境

コマンドライン引数、環境変数、子プロセス、パス。

引数と環境

std::env で引数と環境変数を取得。args と var。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::env;
fn main() {
// コマンドライン引数:
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
// 最初のはプログラムパス
// 環境変数:
match env::var("PATH") {
Ok(v) => println!("PATH: {}", v),
Err(e) => println!("未设置: {:?}", e),
}
// デフォルト値付き:
let port = env::var("PORT")
.unwrap_or_else(|_| "8080".to_string());
// 存在判定:
// env::var_os("KEY").is_some()
// 環境変数の設定:
// env::set_var("KEY", "value")
// 全部を走査:
// for (k, v) in env::vars() {}
}

clap 引数解析

clap でコマンドライン引数を解析。サブコマンド・オプション・自動ヘルプ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Cargo.toml:
// clap = { version = "4", features = ["derive"] }
use clap::Parser;
// 引数を定義:
#[derive(Parser, Debug)]
#[command(name = "app", version, about)]
struct Args {
/// ユーザー名
#[arg(short, long)]
name: String,
/// 回数
#[arg(short, long, default_value_t = 1)]
count: u32,
/// 詳細出力
#[arg(short, long)]
verbose: bool,
}
fn main() {
let args = Args::parse();
println!("{:?}", args);
for _ in 0..args.count {
println!("你好 {}", args.name);
}
}
// 実行:
// app --name Nick --count 3 -v
// ヘルプ自動生成:--help
// 引数検証は自動完了

std::process

終了コード、Command での子プロセス起動、現在プロセスの情報。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::process;
fn main() {
// 終了コード:
// process::exit(0) 成功
// process::exit(1) 失敗
// 現在のプロセス ID:
println!("PID: {}", process::id());
// 子プロセスを実行:
let out = process::Command::new("ls")
.arg("-l")
.output()
.expect("运行失败");
println!("状态: {}", out.status);
println!("输出: {}",
String::from_utf8_lossy(&out.stdout));
// 成功したかを確認:
if out.status.success() {
println!("命令成功");
}
// 完了を待つ:
// process::Command::new("x").status()
}

Command 子プロセス

Command で子プロセスを制御。引数、環境、パイプ、出力キャプチャ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::process::Command;
use std::io::Write;
fn main() {
// 引数付きで実行:
let mut cmd = Command::new("echo");
cmd.arg("hello").arg("world");
let output = cmd.output().unwrap();
println!("{}",
String::from_utf8_lossy(&output.stdout));
// パイプ入力:
let mut child = Command::new("cat")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn().unwrap();
child.stdin.as_mut().unwrap()
.write_all(b"data").unwrap();
// 環境変数を設定:
let out = Command::new("sh")
.arg("-c")
.arg("echo $MYVAR")
.env("MYVAR", "hello")
.output().unwrap();
println!("{}",
String::from_utf8_lossy(&out.stdout));
}

PathBuf

Path / PathBuf でパスを操作。連結・コンポーネント・正規化。クロスプラットフォーム。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::path::{Path, PathBuf};
fn main() {
// 作成:
let mut p = PathBuf::from("/tmp");
p.push("data");
p.push("file.txt");
println!("{}", p.display());
// 構成要素:
println!("父: {:?}", p.parent());
println!("名: {:?}", p.file_name());
println!("扩展名: {:?}", p.extension());
// 判定:
println!("绝对: {}", p.is_absolute());
println!("存在: {}", p.exists());
// 正規化:
let messy = Path::new("./a/../b");
let clean = messy.canonicalize();
// 文字列と結合:
let joined = Path::new("/data")
.join(format!("{}.log", 2026));
println!("{}", joined.display());
// &str と相互変換:
let s = p.to_string_lossy();
}

作業ディレクトリ

カレントディレクトリ、ディレクトリ変更、ホームディレクトリ。パス関連操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::env;
fn main() {
// 現在のディレクトリ:
let cwd = env::current_dir().unwrap();
println!("{}", cwd.display());
// ディレクトリを移動:
// env::set_current_dir("/tmp").unwrap();
// ユーザーのホーム:
let home = env::home_dir();
match home {
Some(h) => println!("主目录: {}", h.display()),
None => println!("无主目录"),
}
// 一時ディレクトリ:
let tmp = env::temp_dir();
println!("临时: {}", tmp.display());
// 実行ファイルのパス:
let exe = env::current_exe().unwrap();
println!("程序: {}", exe.display());
// 相対パスを組み合わせ:
let config = env::current_dir().unwrap()
.join("config")
.join("app.toml");
}

ディレクトリの走査

read_dir でディレクトリを列挙。再帰は自前実装、種類でフィルタ可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::fs;
fn main() {
// ディレクトリを読み取り:
let entries = fs::read_dir(".").unwrap();
for entry in entries {
let e = entry.unwrap();
let path = e.path();
// 型の判定:
if path.is_dir() {
println!("[目录] {}", path.display());
} else {
println!("[文件] {}", path.display());
}
}
// ファイル名を収集:
let files: Vec<String> = fs::read_dir("src")
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
.map(|e| e.file_name().to_string_lossy()
.into_owned())
.collect();
println!("{:?}", files);
// 再帰は自前実装か walkdir を使う
}

シグナル処理

ctrlc crate で Ctrl-C を捕捉。グレースフルシャットダウンとリソース解放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Cargo.toml:
// ctrlc = "3"
use std::sync::atomic::{
AtomicBool, Ordering
};
use std::sync::Arc;
fn main() {
// Ctrl+C を捕捉:
let running = Arc::new(AtomicBool::new(true));
let r = Arc::clone(&running);
ctrlc::set_handler(move || {
println!("\n收到中断,退出中...");
r.store(false, Ordering::SeqCst);
}).expect("设置信号处理失败");
// メインループ:
while running.load(Ordering::SeqCst) {
// タスクを処理...
std::thread::sleep(
std::time::Duration::from_millis(100));
}
println!("清理完成,再见");
}
// 用途:
// 状態の保存、接続のクローズ
// サービスのグレースフルシャットダウン
// 標準ライブラリ:
// シグナルの生の処理には libc/unsafe が必要

18.正規表現

regex crate によるマッチ、キャプチャ、置換、分割。

正規表現のコンパイル

Regex::new でコンパイル。生文字列 r#... を使用。コンパイルは重いので再利用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Cargo.toml:
// regex = "1"
use regex::Regex;
fn main() {
// コンパイル:
let re = Regex::new(r"^[a-z]+$").unwrap();
// 生文字列 r"..." でエスケープ回避
// Result を返し、不正なパターンはエラー
// コンパイルは遅い、使い回す:
// lazy_static/OnceLock でキャッシュ
// またはスレッドローカル
// よく使うパターン:
let email = Regex::new(
r"^[\w.+-]+@[\w-]+\.[\w.]+$").unwrap();
println!("{}", email.is_match("[email protected]"));
// エラー:
match Regex::new(r"[") {
Ok(_) => {}
Err(e) => println!("正则错误: {}", e),
}
}

マッチ判定

is_match でマッチ有無を判定。部分マッチが既定。完全一致はアンカーを付ける。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use regex::Regex;
fn main() {
let re = Regex::new(r"\d+").unwrap();
// マッチするか含むか:
println!("{}", re.is_match("abc123")); // true
// 部分一致が既定:
// 出現するかどうかのみ判定
// 全体の一致:
let full = Regex::new(r"^\d+$").unwrap();
println!("{}", full.is_match("123")); // true
println!("{}", full.is_match("123a")); // false
// find で位置を取得:
let m = re.find("abc123").unwrap();
println!("位置 {}..{}", m.start(), m.end());
// 大文字小文字:
let case = Regex::new(r"(?i)rust").unwrap();
println!("{}", case.is_match("RUST"));
}

キャプチャグループ

キャプチャグループで部分文字列を抽出する。captures はグループを返し、名前付きグループも利用可能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use regex::Regex;
fn main() {
// キャプチャグループ:
let re = Regex::new(r"(\d{4})-(\d{2})-(\d{2})")
.unwrap();
let caps = re.captures("日期 2026-08-02").unwrap();
// caps[0] は全体マッチ:
println!("{}", &caps[0]); // 2026-08-02
// caps[1..] は各グループ:
println!("年: {}", &caps[1]);
println!("月: {}", &caps[2]);
println!("日: {}", &caps[3]);
// 名前付きグループ:
let named = Regex::new(r"(?P<year>\d{4})")
.unwrap();
let caps = named.captures("2026").unwrap();
println!("年: {}", caps.name("year").unwrap().as_str());
// マッチなしは None を返す
// 複数グループの反復:
// captures_iter で全マッチ

全マッチ

find_iter ですべてのマッチを走査。captures_iter はグループ付き。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use regex::Regex;
fn main() {
let re = Regex::new(r"\d+").unwrap();
let text = "a1 b22 c333";
// 全マッチ:
let nums: Vec<&str> =
re.find_iter(text).map(|m| m.as_str()).collect();
println!("{:?}", nums); // ["1", "22", "333"]
// 位置を反復:
for m in re.find_iter(text) {
println!("{} 在 {}..{}", m.as_str(), m.start(), m.end());
}
// グループを全部取得:
let pair = Regex::new(r"(\w+)=(\w+)").unwrap();
for caps in pair.captures_iter("a=1 b=2") {
println!("{} = {}", &caps[1], &caps[2]);
}
// マッチなしは空イテレータ
}

置換

replace でマッチを置換。$1 でグループ参照。replace_all ですべて置換。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use regex::Regex;
fn main() {
let re = Regex::new(r"\d+").unwrap();
// 全て置換:
let out = re.replace_all("a1 b2", "#");
println!("{}", out); // a# b#
// 最初だけ置換:
let first = re.replace("a1 b2", "#");
println!("{}", first); // a# b2
// グループ参照:
let name = Regex::new(r"(\w+),\s*(\w+)").unwrap();
let out = name.replace_all("Doe, John", "$2 $1");
println!("{}", out); // John Doe
// コールバック置換:
let out = re.replace_all("a1 b2", |caps: &regex::Captures| {
let n: i32 = caps[0].parse().unwrap();
format!("{}", n * 2)
});
println!("{}", out); // a2 b4
}

分割

正規表現で文字列を split。splitn で分割数を制限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use regex::Regex;
fn main() {
// 区切り文字で分割:
let re = Regex::new(r"[,;]+").unwrap();
let parts: Vec<&str> =
re.split("a,b;c,,d").collect();
println!("{:?}", parts); // ["a", "b", "c", "d"]
// 空白で分割:
let ws = Regex::new(r"\s+").unwrap();
let words: Vec<&str> =
ws.split("hello world").collect();
println!("{:?}", words);
// 個数を制限:
let parts2: Vec<&str> = re.splitn("a,b,c", 2).collect();
println!("{:?}", parts2); // ["a", "b,c"]
// 区切り文字を保持:
// let parts: Vec<&str> =
// re.split_inclusive("a,b", 0).collect();
// 単純な分割は str::split が高速
// 複雑なパターンのみ regex

よくあるパターン

よくある正規表現レシピ。メール、URL、電話番号、IP、文字クラス。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use regex::Regex;
fn main() {
// メールアドレス:
let email = Regex::new(
r"^[\w.+-]+@[\w-]+\.[\w.]+$").unwrap();
println!("{}", email.is_match("[email protected]"));
// URL:
let url = Regex::new(r"^https?://[\w.-]+(/\S*)?$").unwrap();
println!("{}", url.is_match("https://example.com"));
// 携帯番号(中国):
let phone = Regex::new(r"^1[3-9]\d{9}$").unwrap();
println!("{}", phone.is_match("13800138000"));
// 文字クラス:
// \d 数字 \w 単語 \s 空白
// \D \W \S 否定
// + 1 回以上 * 0 回以上
// {2,4} 回数 {2,} 最低 2 回
// ^ 先頭 $ 末尾
// [a-z] [^0-9] 文字範囲
// 貪欲 ? で非貪欲:\d+?
// グループ ( ) または |
}

フラグと大小文字

(?i) などのインラインフラグ。RegexBuilder での詳細設定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use regex::Regex;
fn main() {
// 大文字小文字を無視:
let re = Regex::new(r"(?i)rust").unwrap();
println!("{}", re.is_match("RUST"));
println!("{}", re.is_match("rust"));
// マルチラインモード:
// (?m) ^ $ は各行にマッチ
let m = Regex::new(r"(?m)^go").unwrap();
println!("{}", m.is_match("x\ngo"));
// ドットが改行にマッチ:
// (?s) . は \n にマッチ
// インラインでない:
// RegexBuilder で設定可能
let re = regex::RegexBuilder::new("rust")
.case_insensitive(true)
.multi_line(true)
.build().unwrap();
println!("{}", re.is_match("RUST"));
// 単一行:
// .size_limit() で複雑度を制限
// 既定は UTF-8、無効化は bytes
}

19.ビルドとテスト

cargo ビルド、ユニットテスト、lint、リリース設定。

cargo build

プロジェクトをビルド。デバッグ/リリースプロファイル。成果物の場所。差分コンパイル。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// デバッグビルド:
// cargo build
// リリースビルド(最適化):
// cargo build --release
// 検査(バイナリを生成しない):
// cargo check
// 成果物:
// target/debug/ target/release/
// パッケージを指定:
// cargo build -p mylib
// インクリメンタルビルド:
// 変更のないファイルは再コンパイルしない
// フィーチャー選択:
// cargo build --features xxx
// クリーン:
// cargo clean
// 実行:
// cargo run
// バイナリを直接実行:
// ./target/debug/app
// ビルド設定は Cargo.toml [profile]

ユニットテスト

#[test] 属性。cargo test で実行。assertion マクロ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ユニットテスト:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(1, 2), 3);
}
#[test]
fn test_sub() {
assert!(sub(2, 1) == 1);
}
#[test]
#[should_panic] // panic を期待
fn test_bad() {
panic!("boom");
}
}
fn add(a: i32, b: i32) -> i32 { a + b }
fn sub(a: i32, b: i32) -> i32 { a - b }
// 実行:
// cargo test
// 個別に実行:
// cargo test test_add
// 出力を表示:
// cargo test -- --nocapture
// アサーション宏:
// assert_eq! / assert_ne!
// assert! はメッセージ付き

ドキュメンテーションテスト

/// ドキュメントコメント内のコードはテストとして実行される。cargo test が検証。統合テスト。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// 二つの数の和。
///
/// # 例
/// ```
/// let result = add(1, 2);
/// assert_eq!(result, 3);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// ドキュメントテスト:
// サンプルコードはコンパイル・実行される
// cargo test で検証
// ドキュメント生成:
// cargo doc --open
// 統合テスト:
// tests/integration.rs
// 公開 API を直接呼ぶ
// 実行:cargo test
// ドキュメントコメント構文:
// /// ドキュメントコメント
// //! モジュールレベルのドキュメント
// ```rust,ignore スキップ
// ```text コンパイルしない

依存関係管理

Cargo.toml で依存を宣言。SemVer。cargo update。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Cargo.toml 依存:
// [dependencies]
// serde = "1" // 任意の 1.x と互換
// serde = "1.0.100" // 正確な下限
// serde = "=1.0.100" // 正確なバージョン
// serde = "^1.2" // 互換
// オプション依存:
// [dependencies]
// serde = { version = "1", optional = true }
// 開発依存:
// [dev-dependencies]
// pretty_assertions = "1"
// 更新:
// cargo update // ロックファイルを更新
// cargo add serde // 依存を追加
// cargo remove serde // 削除
// 陳腐化の検査:
// cargo outdated
// 脆弱性の監査:
// cargo audit

clippy チェック

cargo clippy がコードを検査。潜在バグとスタイルの問題を発見。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 検査を実行:
// cargo clippy
// 厳格モード:
// cargo clippy -- -D warnings
// よくある検査:
// 1. 不要なクローン
// 2. 簡略化できる書き方
// 3. パフォーマンスの罠
// 4. 疑わしいロジック
// 例の問題:
// let v = vec![1, 2];
// if v.len() > 0 {} // clippy が提案
// if !v.is_empty() {} // より良い
// ルールを無視:
// #[allow(clippy::all)]
// 局所的に許可:
// #[allow(clippy::redundant_clone)]
// 設定ファイル:
// clippy.toml
// CI 統合では -D warnings が一般的
// & を .iter() の代わりに使う等の提案

コードフォーマット

cargo fmt が rustfmt で整形。チームで一貫したスタイル。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// フォーマット:
// cargo fmt
// 書式を検査(変更しない):
// cargo fmt --check
// 設定ファイル:
// rustfmt.toml
// よく使う設定:
// max_width = 100
// tab_spaces = 4
// 特徴:
// 自動でインデント・整列
// インポートの並べ替え(nightly が必要):
// cargo +nightly fmt
// CI 検査:
// cargo fmt --check
// clippy と併用:
// 先に fmt してから clippy
// エディタ保存時に自動
// チームでスタイル統一
// diff の衝突を減らす

リリース設定

[profile.release] で最適化設定。バイナリのストリップや LTO など。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Cargo.toml:
// [profile.release]
// opt-level = 3 // 最適化レベル
// lto = true // リンク時最適化
// codegen-units = 1 // 単一コードユニット
// panic = "abort" // 直接終了
// strip = "symbols" // シンボル除去
// debug = false // デバッグ情報なし
// 小さいサイズの組み合わせ:
// [profile.release]
// lto = true
// strip = "symbols"
// opt-level = "z" // サイズ優先
// ビルド:
// cargo build --release
// サイズを確認:
// ls -lh target/release/app
// リリース前に:
// cargo fmt --check
// cargo clippy -- -D warnings
// cargo test
// バージョン番号:
// Cargo.toml version フィールド

ドキュメント生成

cargo doc が API ドキュメントを生成。rustdoc のマークアップ。リリース前にドキュメントテストを実行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ドキュメント生成:
// cargo doc
// ブラウザで開く:
// cargo doc --open
// ドキュメントコメント:
/// 二つの数の和を計算する。
///
/// # パラメータ
/// - `a`:最初の数
/// - `b`:二番目の数
///
/// # 戻り値
/// 和
///
/// # 例
/// ```
/// assert_eq!(add(1, 2), 3);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// ドキュメントのよく使うマーカー:
// # Examples # Panics
// # Errors # Safety
// モジュールドキュメント://!
// 内部リンク:\[`add`\]
// 非公開の項目は生成されない
// リリース前に doc テストを実行

公式リンク

公式ドキュメントとリソースへの直接リンク。

このチートシートについて

このページは Rust 2021 版の自己完結型チートシートです。実プロジェクトで日常の約80%を扱う言語コアと最もよく使う std API をカバーします。内容はモダンな慣用法を優先: ? でのエラー伝播、match での網羅、イテレータで手書きループを置換、String と &str を厳密に区別。权威あるリファレンスは Rust 公式ブックと標準ライブラリドキュメントを参照してください。 19 のセクションはそれぞれ 1 つのテーマに焦点を当てています — 最初のプログラムから、所有権、トレイト、並行処理、よくある落とし穴まで。各セクションは 8 つのサブトピック(各 5〜20 行)に分かれ、合計約 150 トピック。コードスニペットは意図的に短く自己説明的。 すべてブラウザ内で実行されます — アップロードも追跡もありません。このページは GuruToolkit の無料開発者ツールセットの一部です。スニペットは自由に利用できますが、保証はありません。

バージョン 2.1.0