Rust言語入門

Essential
最終更新: タグ: Rust

Rustは、このプロジェクトが最初のチェックポイントからずっと目指してきた言語だ。Zigコースで扱ったあらゆる概念——スタックフレーム(stack frame)、ヒープ割り当て(heap allocation)、ポインタ(pointer)、型のサイズ——は、Rustが基盤とするメンタルモデルへの準備だった。違いは、Rustではコンパイラが、Zigで手動で実践していたことを強制してくれる点だ。

Rustとは何か

Rustは**システムプログラミング言語(systems programming language)**だ。メモリとハードウェアを直接制御でき、効率的なネイティブマシンコードにコンパイルされ、ガベージコレクタ(garbage collector)なしで動作する。その意味ではCやZigと同じカテゴリに位置する。

これらの言語とRustを区別するのが**所有権システム(ownership system)**だ。コンパイル時にコンパイラがチェックする一連のルールであり、メモリ安全性のバグを出荷不可能にする。Zigでは、ヒープメモリの解放を忘れたり、解放済みのポインタにアクセスしたり、複数スレッドで可変データを誤って共有したりしても、プログラムがクラッシュするか実行時に誤った結果を生むまで何も止めてくれなかった。Rustはそれらのバグをコンパイルエラーにする。プログラムはボローチェッカー(borrow checker)を通過するか、ビルドが通らないかのどちらかだ。

この保証にはランタイムコストがない。バックグラウンドでガベージコレクタが動くわけでも、ポインタ参照のたびに参照カウントが走るわけでもない。安全性はすべてコンパイラの静的解析から来ており、RustのプログラムはCやZigの同等のプログラムと同じレベルで動作する。

所有権モデル

Rustの所有権システムは、すべてのRustプログラマがすぐに身につける3つのルールに基づいている。

  1. すべての値にはちょうど1人の**所有者(owner)**がいる——それを保持する変数バインディング(variable binding)だ。
  2. 所有者がスコープ(scope)を抜けると、値は**ドロップ(dropped)**される——メモリは自動的に解放される。
  3. 所有権は新しい所有者へ移譲(transferred)(ムーブ、moved)できる。その時点で元の所有者はその値を使えなくなる。

この3つのルールだけで、ガベージコレクタなしにメモリリークとダブルフリーを排除できる。

fn main() {
    let s = String::from("hello"); // s がヒープ割り当てを所有する
    let t = s;                     // 所有権が t に移動する。s はもう有効ではない
    println!("{}", t);             // ok
    // println!("{}", s);          // コンパイルエラー: 値はムーブ済み
}                                  // t がスコープを抜ける。ヒープメモリが解放される

借用

使うたびに所有権を移譲していたら、関数はほとんど使い物にならない。**借用(borrowing)**は、所有権を手放すことなく値への参照を渡す仕組みだ。

fn length(s: &String) -> usize { // s を借用する。所有権は取らない
    s.len()
}

fn main() {
    let s = String::from("hello");
    let n = length(&s); // この呼び出し後も s は有効
    println!("{} has {} characters", s, n);
}

ボローチェッカーは参照について2つのルールを強制する。

  • 任意の数の共有参照(shared reference)&T)を同時に持てる——ただし、共有参照が生きている間は変更不可。
  • ちょうど1つの排他参照(exclusive reference)&mut T)を持てる——それが生きている間は、読み取りも含め他の参照は一切持てない。

これらのルールはデータ競合(data race)を排除する。プログラムの別の部分が読み取り中にある部分がデータを無音で変更するバグを追いかけたことがあるなら、これがなぜ重要かがわかるはずだ。

Zigコースが何の準備になっていたか

ZigからRustへの対応関係は直接的だ。

ZigRust
終わったら allocator.free を呼ぶ所有者がドロップ → メモリ解放
解放後にアクセスしないボローチェッカーがダングリング参照を拒否
二重解放しない所有権は唯一。ダブルフリーはコンパイルエラー
*T 生ポインタ&T 共有参照、&mut T 排他参照
[*]T ヒープ配列へのポインタVec<T> 所有されたヒープコレクション
手動で境界チェックコンパイラとランタイムが境界チェック。失敗時はパニック

スタック、ヒープ、ポインタについてのメンタルモデルは変わらず使える。変わるのは誰が強制するかだ——Zigでは自分、Rustではコンパイラ。

このコースで学ぶこと

Rustエッセンシャルコースは、おおよそ次の順序でスキルを積み上げる。

  1. 変数と型(Variables and types) — RustのType Systemはzigより表現力が豊かだ。structenummatch でデータを正確にモデル化できる。
  2. 所有権と借用(Ownership and borrowing) — ボローチェッカーの詳細。何が許されるか、そしてなぜかの直感を養う。
  3. エラーハンドリング(Error handling) — Rustは Result<T, E>? 演算子を使う。nullポインタも未チェック例外もない。
  4. トレイト(Traits) — Rustのインタフェースに相当する。トレイトは任意の型が実装できる共有の振る舞いを定義し、ジェネリクスの基盤となる。
  5. イテレータとクロージャ(Iterators and closures) — 慣用的なRustは多くの場面で生のループよりイテレータチェーンを好む。
  6. 並行処理(Concurrency) — 所有権と借用はスレッドにも適用され、データ競合をコンパイルエラーにする。

それぞれが前のものの上に構築される。所有権は内面化するのが最も難しい——最初の数回はコンパイルエラーをよく読むことになるだろう。それらはあらゆる言語の中でも最良のエラーメッセージに属しており、ルールを教えてくれる。