
2025/12/06 1:34
Patterns for Defensive Programming in Rust
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
主なメッセージ
一般的な落とし穴(暗黙の不変条件、unsafe インデクシング、隠れたデフォルト値、enum バリアントの無音損失、偶発的変更)を防ぐ具体的な Rust コーディングパターンを採用し、Clippy ラインが自動でそれらを強制できるようにする。根拠 / 推論
のコメントは明示的なチェックに置き換える;そうしないとコンパイラは暗黙の不変条件を検出できない。// this should never happen チェック後にインデックスアクセスする代わりに、スライスパターンマッチング(is_empty())を使い、要素がちょうど一つあることを保証し、空の場合も安全に処理する。match vec.as_slice()- 構造体を構築するときは
を避ける;すべてのフィールドを列挙するか、デフォルトインスタンスを分解して新しいフィールドがコンパイラ警告を発生させるようにする。..Default::default() 実装では構造体を分解し、すべてのフィールドが考慮されるようにする—そうでなければ、新しく追加されたフィールドが静かに等価性ロジックを壊す可能性がある。PartialEq- 失敗を明示的に扱うため
を優先し、失敗時にデフォルト値の無音化を避ける。TryFrom- パターンマッチで
のキャッチ―オールアームは使わない;すべての enum バリアントを列挙するかグループ化して、コンパイラが未処理ケースを警告できるようにする。_ => {}- 使用されない変数プレースホルダー(
)は説明的な名前で置き換え、特にブールフラグの場合には明確さを高める。_- 一時的な可変性はシェーディングまたは内部スコープで表現し、初期化後の偶発的変更を防ぐ。
- コンストラクタ検証を強制するためにプライベートフィールド(
)を追加したり、_private: ()を使用したり、クレート内でプライベートモジュールに型を隠すことで安全性を確保する。#[non_exhaustive]- 重要な型には
を付与し、値が無視されたときに警告を発生させることで静かに誤設定されるのを防ぐ。#[must_use]- ブールパラメータは enum またはパラメータ構造体に置き換えて意図を明示的にし、偶発的なスワップを回避する。
、indexing_slicing、fallible_impl_from、wildcard_enum_match_arm、unneeded_field_pattern、fn_params_excessive_boolsといった Clippy ラインを有効にして、多くのパターンをコンパイル時に検出する。must_use_candidate影響
これらの実践と関連する Clippy ラインを採用すると、ランタイムパニックが減少し、コードの可読性が向上し、将来のリファクタリング時に隠れたバグを早期に検出できるため安全性が高まる。
本文
私は趣味として、コード中に
// this should never happen in code というコメントを見つけたとき、その「決して起こらないはず」の条件を実際に探し出すことに挑戦しています。90 % のケースでその条件を突き止める方法がわかります。多くの場合、コンパイラが強制しない暗黙の不変条件(インバリアント)が根本原因です。コンパイラはメモリ安全性を確保し、標準ライブラリは業界最高水準ですが、それでも欠点があります。私たちが頼りにできるのは、より防御的な Rust コードを書くために「学習済み」である硬直したパターン―文書化されていないものの、全体品質には不可欠な小さなイディオムです。
コードスメル:ベクタへの直接インデックスアクセス
if !matching_users.is_empty() { let existing_user = &matching_users[0]; }
リファクタリング時に
is_empty() チェックを忘れると、matching_users[0] はパニックします。長さ確認とインデックス付けは別々の操作であり、コンパイラは検出できません。
スライスパターンマッチングで修正
match matching_users.as_slice() { [] => todo!("ユーザーが見つからない場合どうする?"), [existing_user] => { /* 安全! 要素はちょうど 1 個 */ } _ => Err(RepositoryError::DuplicateUsers), }
コンパイラにすべての状態を考慮させることで、隠れたエッジケースが明らかになります。
コードスメル:Default
の遅延使用
Default..Default::default() を使うと、本来設定したくないフィールドが暗黙的に初期化されます。
let foo = Foo { field1: value1, field2: value2, ..Default::default() };
すべてのフィールドを明示的に設定
let foo = Foo { field1: value1, field2: value2, field3: value3, field4: value4, };
まだデフォルト値が欲しい場合は:
let Foo { field1, field2, field3, field4 } = Foo::default(); let foo = Foo { field1: value1, field2: value2, field3, field4, };
新しいフィールドが追加されるとコンパイラが警告します。
コードスメル:脆弱なトレイト実装
構造体を分解(デストラクチャリング)すると、忘れたフィールドに気づきやすくなります。
impl PartialEq for PizzaOrder { fn eq(&self, other: &Self) -> bool { let Self { size, toppings, crust_type, ordered_at: _ } = self; let Self { size: o_size, toppings: o_toppings, crust_type: o_crust, ordered_at: _ } = other; size == o_size && toppings == o_toppings && crust_type == o_crust } }
extra_cheese を追加するとコンパイル時に検出できます。
コードスメル:From
と TryFrom
の使い分け
FromTryFrom変換が失敗する可能性があるなら
TryFrom を使用します。
impl TryFrom<&DetectorStartupErrorReport> for DetectorStartupErrorSubject { fn try_from(report: &DetectorStartupErrorReport) -> Result<Self, Error> { let postfix = report.get_identifier() .or_else(get_binary_name) .ok_or(Error::MissingIdentifier)?; Ok(Self(StreamSubject::from( format!("apps.errors.detectors.startup.{postfix}").as_str(), ))) } }
From は失敗しない変換にのみ使用します。
コードスメル:非網羅的マッチ
ワイルドカード
_ を避け、すべてのバリアントを列挙します。
match self { Self::Variant1 => { /* ... */ } Self::Variant2 => { /* ... */ } Self::Variant3 | Self::Variant4 => { /* 共通ロジック */ } }
新しいバリアントが追加されたときにコンパイラが警告します。
コードスメル:未使用変数の _
プレースホルダー
_説明的な名前を付けるだけで意図が明確になります。
match self { Self::Rocket { has_fuel: _, has_crew: _, .. } => { /* ... */ } }
パターン:一時的なミュータビリティ
ミューテーションを明示し、スコープで限定します。
let data = { let mut tmp = get_vec(); tmp.sort(); tmp // 戻り値は不変 };
初期化後に偶発的な変更が起きません。
パターン:コンストラクタの防御的取り扱い
外部コード
#[non_exhaustive] pub struct S { /* フィールド */ } impl S { pub fn new(field1: String, field2: u32) -> Result<Self, Error> { /* 検証 */ } }
_private や Seal を使ってクレート内でのみ生成を許可します。
内部コード
mod inner { struct Seal; pub struct S { field1: String, field2: u32, _seal: Seal } impl S { pub fn new(field1: String, field2: u32) -> Result<Self, Error> { /* 検証 */ } } } pub use inner::S;
new 以外からはインスタンスを作れません。
パターン:重要な型に #[must_use]
を付与
#[must_use]#[must_use = "設定が適用されるには構成を使用する必要があります"] pub struct Config { /* フィールド */ } impl Config { pub fn new() -> Self { /* ... */ } pub fn with_timeout(mut self, timeout: Duration) -> Self { self.timeout = timeout; self } }
結果を無視するとコンパイラが警告します。
コードスメル:ブールパラメータ
ブール値は列挙型やパラメータ構造体に置き換えます。
enum Compression { Strong, Medium, None } enum Encryption { AES, ChaCha20, None } enum Validation { Enabled, Disabled } fn process_data( data: &[u8], compression: Compression, encryption: Encryption, validation: Validation, ) { /* ... */ }
または:
struct ProcessDataParams { compression: Compression, encryption: Encryption, validation: Validation, } impl ProcessDataParams { pub fn production() -> Self { /* デフォルト */ } pub fn development() -> Self { /* デフォルト */ } }
防御的プログラミングのための Clippy ライン
| ライン | 目的 |
|---|---|
| スライス/ベクタへの直接インデックスを防止 |
| 失敗し得る 実装に警告 |
| enum マッチでワイルドカード を禁止 |
| 不要なフィールドパターン()を検出 |
| 余分なブール引数に警告 |
| の追加を提案 |
クレートで有効化するには:
#![deny(clippy::indexing_slicing)] #![deny(clippy::fallible_impl_from)] #![deny(clippy::wildcard_enum_match_arm)] #![deny(clippy::unneeded_field_pattern)] #![deny(clippy::fn_params_excessive_bools)] #![deny(clippy::must_use_candidate)]
または
Cargo.toml で:
[lints.clippy] indexing_slicing = "deny" fallible_impl_from = "deny" wildcard_enum_match_arm = "deny" unneeded_field_pattern = "deny" fn_params_excessive_bools = "deny" must_use_candidate = "deny"
結論
Rust における防御的プログラミングは、暗黙の不変条件を明示化しコンパイラで検証することにあります。パターンマッチング、明示的なデフォルト値、分解、制御された生成、
#[must_use]、名前付きパラメータ、Clippy ラインといったパターンを採用すれば、リファクタリングミスによるバグを事前に防ぎ、実際に起きる前に問題を解決できます。
「// this should never happen」というコメントを書いているときは、一瞬立ち止まり、「コンパイラがその不変条件をどう保証できるか」を考えてみてください。最高のバグとは、コンパイル自体で捕捉されるものです。