拡張性のための #[non_exhaustive] と private なフィールド

説明

ライブラリの作者が後方互換性を破壊することなく、publicな構造体へpublicなフィールドを追加したり、enumへ新しいバリアントを追加したりしたい、といったいくつかのシナリオがあります。

Rust はこの問題に対して2つの解決策を提供します:

  • structenumenum バリアントに対して #[non_exhaustive] を使用する。 #[non_exhaustive] が使用できるすべての場所に関する詳細なドキュメントは the docs. を参照してください。

  • 構造体が直接インスタンス化されたり、マッチングされたりしないようにするために、構造体に private なフィールドを追加することができます(Alternative 参照)

#![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"),

        // このバリアントも非網羅的であるため .. が必須
        a::AdmitMoreVariants::VariantC { a, .. } => println!("it's a c"),

        // 将来的にバリアントが追加される可能性があるため、
        // ワイルドカードによるマッチが必須
        _ => println!("it's a new variant"),
    }
}
}

Alternative: 構造体の private なフィールド

#[non_exhaustive] が機能するのは、crate の境界を越えるときのみです。クレート内部では、private なフィールドによる手法を使用できます。

構造体にフィールドを追加することはだいたいの場合、後方互換性のある変更です。ただし、構造体のクライアントが、パターンを使用して構造体のインスタンスをデコンストラクトする際に、構造体内のすべてのフィールドに名前を付けている可能性があります。この状況で構造体に新しいフィールドを追加することは、そのパターンを壊すことになります。クライアントは、パターンの中でいくつかのフィールドに名前を付けるとともに .. を使うことが可能であり、この場合はフィールドの追加に対して後方互換性があります。構造体のフィールドの1つ以上を private なものにすることで、クライアントに後者のパターンを使用することを強制することが可能であり、構造体の将来性を確保できます。

この方法の欠点は、構造体に本来不要なフィールドを追加する必要がある可能性があることです。() 型を使うことでランタイムのオーバーヘッドを回避できます。またフィールド名の頭に _ を付けることで未使用フィールドの警告を回避できます。

#![allow(unused)]
fn main() {
pub struct S {
    pub a: i32,
    // `b` は private です。 `S` に対して `..` を使用せずにパターンマッチすることは不可能であり、 
    // `S` を直接インスタンス化したり、マッチ対象としたりすること不可能です。
    _b: (),
}
}

議論

struct に対しては #[non_exhaustive] は、後方互換性のある形でフィールドを追加可能にします。また、すべてのフィールドがpublicであっても、クライアントは構造体のコンストラクタを使用できなくなります。これは便利ではあります。しかしながら、フィールドの追加がコンパイルエラーを起こすようにすることで、エラーを見たクライアントがフィールドが追加されたことに気づけるようにする形が (フィールドの追加をクライアントに隠す形よりも) 期待 されることもあります。

#[non_exhaustive] は列挙型のバリアントにも適用できます。#[non_exhaustive] なバリアントは、#[non_exhaustive] な構造体と同様に振舞います。

この方法は計画的かつ注意深く使用してください: フィールドやバリアントを追加するときは、 メジャーバージョンをインクリメントした方が良いことが多いのです。#[non_exhaustive]は、あなたのライブラリが、ライブラリに同期させることができない外部のリソースをモデリングする場合に適切かもしれませんが、汎用目的のツールではありません。

短所

#[non_exhaustive]はコードを人間工学的に使いにくくします。特に未知の列挙型のバリアントを扱わなければならない場合は顕著です。この種の開発を、メジャーバージョンをインクリメント せずに 行う必要がある場合のみ使うべきです。

enum#[non_exhaustive] を適用することは、クライアントにワイルドカードの使用を強制します。この場合に取るべき賢明なアクションがなければ、厄介なコードや、極めて稀な状況でのみ実行されるコードパスを生み出しかねません。クライアントがこのシナリオで panic!() を選択するようなら、コンパイル時エラーとして公開された方がよかったかもしれません。実際、#[non_exhaustive]はクライアントに 「その他の何か」 に対するケースを処理するように強制します。そして、このシナリオで取るべき賢明なアクションはまずありません。

See also

Last change: 2024-07-09, commit: 317c88e