イディオマティックなRustへようこそ
Rustの基礎 では、Rust の構文と中核概念を紹介しました。ここからさらに一歩進みます。プロジェクトで Rust を 効果的に 使うにはどうすればよいのでしょうか。イディオマティックな Rust とはどのようなものでしょうか。
このコースには明確な方針があります。いくつかのパターンを勧め、そうでないものからは距離を置くよう促します。とはいえ、プロジェクトによって必要は異なることも理解しています。皆さん自身のプロジェクトの文脈や制約の中で、十分な情報に基づいた判断ができるよう、必要な情報は常に提供します。
⚠️ このコースは現在も活発に開発中です。
内容は頻繁に変わる可能性があり、まだ見つかっていない誤りが含まれているかもしれません。それでも、ぜひひととおり目を通し、早い段階でフィードバックをお寄せください!
スケジュール
session outline
このコースでは、以下に挙げるトピックを扱います。各トピックは、その複雑さや関連性に応じて、1枚または複数枚のスライドで扱います。
対象読者
C、C++11 以降、Java 7 以降、Python 2 または 3、Go、あるいは同様の命令型プログラミング言語で、少なくとも 2〜3 年のコーディング経験があるエンジニアを対象としています。Swift、Kotlin、C#、TypeScript のような、よりモダンで機能豊富な言語の経験は前提としません。
API設計の基礎
- 黄金律: 呼び出し箇所での明瞭さと読みやすさを最優先する。人は、呼び出される関数の宣言を読むよりも、呼び出し箇所を読むほうにずっと多くの時間を費やす。
- APIを予測可能にする
- 命名規則に従う(大文字・小文字の規約に従い、標準ライブラリですでに先例のある語彙を優先する。たとえば、メソッド名は “push_back” ではなく “push”、“empty” ではなく “is_empty” などにする)
- 標準ライブラリの語彙型やトレイトを理解し、APIでそれらを使う。何かが基本的な型やアルゴリズムのように思えるなら、まず標準ライブラリを確認する。
- この講義の後半で扱う、十分に確立されたAPI設計パターンを使う(例: newtype、所有型/ビュー型のペア、エラーハンドリング)
- 意味があり実用的なドキュメントコメントを書く(例: アンダースコアを空白に置き換えてメソッド名をただ繰り返すだけにしない、すべての markdown タグを埋めるためだけに同じ情報を繰り返さない、使用例を示す)
型システムを活用する
- enum、struct、型エイリアスの簡単なおさらい
- newtype パターンとカプセル化: 検証するのではなく、パースする
- 拡張トレイト: 追加の振る舞いを提供したいだけなら newtype パターンは避ける
- RAII、スコープガード、drop bomb:
Dropを使ってリソースをクリーンアップし、アクションを発火させ、不変条件を強制する - 「トークン」型: 特定の操作を実行したことをユーザーに証明させる
- typestate パターン: 正しい状態遷移をコンパイル時に強制する
- 借用チェッカーを使って、メモリ所有権とは無関係な不変条件を強制する
- 標準ライブラリの OwnedFd/BorrowedFd
- ブランド付き型
借用チェッカーと戦わない
- 「所有」型と「ビュー」型:
&strとString、PathとPathBufなど - 所有権の要件を隠さない: 隠れた
.clone()を避け、Cowを活用する - 所有権の境界に沿って型を分割する
- 所有権の階層を木構造のように設計する
- 循環依存を管理する戦略: 参照カウント、参照の代わりにインデックスを使う
- 内部可変性(Cell、RefCell)
- ユーザー定義データ型のライフタイムパラメータを扱う
Rustにおけるポリモーフィズム
- トレイトとジェネリック関数の簡単なおさらい
- Rustには継承がない: その意味するところは何か?
- ポリモーフィズムに enum を使う
- ポリモーフィズムにトレイトを使う
- コンポジションを使う
- 最も適切なパターンはどう選べばよいか?
- ジェネリクスを扱う
- 引数として使うなら、関数のジェネリック型パラメータにするべきか、それともトレイトオブジェクトにするべきか?
- トレイト境界はジェネリックパラメータを参照している必要はない
- トレイトにおける型パラメータ: ジェネリックパラメータにするべきか、関連型にするべきか?
- マクロ: トレイトだけでは不十分な(あるいは複雑すぎる)ときに、コードをDRYに保つための有用なツール
エラーハンドリング
- エラーの目的は何か? 回復か、報告か。
- Result と Option
- 良いエラーを設計する:
- エラーのスコープを定める。
- エラーが上位へ伝播し、スコープの境界をまたぐ際に、追加のコンテキストを保持する。
Errorトレイトを活用して、完全なエラーチェーンを追跡する。thiserrorを活用して、エラー型定義時のボイラープレートを減らす。anyhow
Result<Result<T, RecoverableError>, FatalError>を使って、致命的なエラーと回復可能なエラーを区別する。