#[non_exhaustive] と拡張性のためのプライベートフィールド
説明
ライブラリ作者が、後方互換性を壊すことなく public な構造体に public な フィールドを追加したり、enum に新しいバリアントを追加したりしたい場合がある、 少数のシナリオが存在します。
Rust はこの問題に対して 2 つの解決策を提供します。
-
struct、enum、およびenumバリアントに#[non_exhaustive]を使用します。#[non_exhaustive]を使用できるすべての場所に関する詳細なドキュメントについては、 ドキュメントを参照してください。 -
構造体にプライベートフィールドを追加して、直接インスタンス化されたり、 マッチされたりしないようにできます(代替案を参照)
例
#![allow(unused)]
fn main() {
mod a {
// Public な構造体。
#[non_exhaustive]
pub struct S {
pub foo: i32,
}
#[non_exhaustive]
pub enum AdmitMoreVariants {
VariantA,
VariantB,
#[non_exhaustive]
VariantC {
a: String,
},
}
}
fn print_matched_variants(s: a::S) {
// S は `#[non_exhaustive]` であるため、ここで名前を指定できず、
// パターン内で `..` を使用する必要があります。
let a::S { foo: _, .. } = s;
let some_enum = a::AdmitMoreVariants::VariantA;
match some_enum {
a::AdmitMoreVariants::VariantA => println!("it's an A"),
a::AdmitMoreVariants::VariantB => println!("it's a b"),
// このバリアントも non-exhaustive であるため、.. が必要です
a::AdmitMoreVariants::VariantC { a, .. } => println!("it's a c"),
// 将来的にさらにバリアントが追加される可能性があるため、
// ワイルドカードマッチが必要です
_ => println!("it's a new variant"),
}
}
}
代替案: 構造体のための Private fields
#[non_exhaustive] は crate の境界を越える場合にのみ機能します。crate 内では、
プライベートフィールド方式を使用できます。
構造体にフィールドを追加することは、ほとんどの場合、後方互換性のある変更です。
しかし、クライアントが構造体インスタンスを分解するためにパターンを使用している場合、
構造体内のすべてのフィールドを名前で指定している可能性があり、新しいフィールドを追加すると
そのパターンが壊れます。クライアントは一部のフィールドに名前を付け、パターン内で .. を使用できます。
その場合、別のフィールドを追加することは後方互換性があります。構造体のフィールドの少なくとも
1 つをプライベートにすると、クライアントは後者の形式のパターンを使用せざるを得なくなり、
構造体が将来に備えたものになります。
このアプローチの欠点は、本来は不要なフィールドを構造体に追加する必要があるかもしれないことです。
実行時オーバーヘッドが発生しないように () 型を使用し、未使用フィールドの警告を避けるために
フィールド名の先頭に _ を付けることができます。
#![allow(unused)]
fn main() {
pub struct S {
pub a: i32,
// `b` はプライベートであるため、`..` と `S` を使用せずに `S` にマッチできず、
// 直接インスタンス化したりマッチしたりすることもできません
_b: (),
}
}
議論
struct において、#[non_exhaustive] は後方互換性のある方法で追加のフィールドを
追加できるようにします。また、すべてのフィールドが public であっても、クライアントが
構造体コンストラクターを使用することを防ぎます。これは役立つ場合がありますが、追加フィールドが
静かに見つからない可能性のあるものではなく、コンパイラーエラーとしてクライアントに発見されることを
望む かどうかを検討する価値があります。
#[non_exhaustive] は enum バリアントにも適用できます。
#[non_exhaustive] バリアントは、#[non_exhaustive] な構造体と同じように振る舞います。
これは意図的かつ慎重に使用してください。フィールドやバリアントを追加するときにメジャーバージョンを
上げることの方が、より良い選択肢であることがよくあります。#[non_exhaustive] は、ライブラリと
同期せずに変更される可能性のある外部リソースをモデル化しているシナリオでは適切な場合がありますが、
汎用的なツールではありません。
欠点
#[non_exhaustive] は、特に未知の enum バリアントの処理を強制される場合、コードの使いやすさを
大きく損なう可能性があります。この種の進化をメジャーバージョンを上げることなしに行う必要がある
場合にのみ使用すべきです。
#[non_exhaustive] が enum に適用されると、クライアントはワイルドカードバリアントを
処理せざるを得なくなります。この場合に取るべき合理的なアクションがない場合、不自然なコードや、
極めてまれな状況でしか実行されないコードパスにつながる可能性があります。クライアントがこのシナリオで
panic!() することを決めるなら、このエラーをコンパイル時に公開する方がよかったかもしれません。
実際、#[non_exhaustive] はクライアントに「その他」のケースを処理させますが、このシナリオで
取るべき合理的なアクションがあることはまれです。