🦍
Rustの#[non_exhaustive]を使ってクレートの後方互換性を壊さないようにする
はじめに
何かしらのクレートを提供する時、提供している構造体フィールドが増えた場合、利用側はビルドできなくなるという問題があります。
具体的に以下の例を考えてみます。
- 複数のクレートを持つプロジェクト内で
Repository
クレートを提供している -
Repository
ではFindOption
という構造体を提供していて、それを他のクレートが使用している
repository/src/lib.rs
pub struct FindOption {
pub id: String,
}
src/lib.rs
use repository::FindOption;
fn find(option: FindOption) {
let FindOption { id } = option;
// using id for something
// ...
}
FindOption
にオプショナルなフィールドが増えた場合、それを使用しているクレートは追加されたフィールドを使用するように修正するか、
または..
を使用して省略するという対処が必要になります。
repository/src/lib.rs
pub struct FindOption {
pub id: String,
+ pub limit: Option<usize>,
}
src/lib.rs
use repository::FindOption;
fn find(option: FindOption) {
- let FindOption { id } = option;
+ let FindOption { id, .. } = option;
// using id for something
// ...
}
このように、必須ではないがフィールドが増えた場合は利用者側はビルドができなくなるという問題があります。
#[non_exhaustive]
を使用して、省略を強制させる
#[non_exhaustive]
を構造体やenumに追加することで、実装時に利用者側にパターン省略を強制させることで、あとから修正しなくて済むようにできます。
ちなみに#[non_exhaustive]
はモジュールレベルでしか動作しないのでご注意ください。
repository/src/lib.rs
#[non_exhaustive]
pub struct FindOption {
pub id: String,
}
src/lib.rs
use repository::FindOption;
fn find(option: FindOption) {
let FindOption { id } = option;
// using id for something
// ...
}
試しにcargo check
してみると、エラーになります。
$ cargo check
warning: unused manifest key: package.members
Checking exhaustive v0.1.0 (/Users/skanehira/dev/github.com/skanehira/sandbox/rust/exhaustive)
error[E0638]: `..` required with struct marked as non-exhaustive
--> src/lib.rs:4:9
|
4 | let FindOption { id } = option;
| ^^^^^^^^^^^^^^^^^
|
help: add `..` at the end of the field list to ignore all other fields
|
4 | let FindOption { id , .. } = option;
| ~~~~~~
#[non_exhaustive]
を使わないほうがよい場合
利用者側にパターン省略を強制することで、フィールドが増えても修正が必要なく互換性を維持できますが、これは却って問題が起きるケースもあります。
具体的に次の例を考えてみます。
- エラーenumを提供している
- エラーのパターンマッチで
panic
を使用している
rpeository/src/lib.rs
#[non_exhaustive]
pub struct FindOption {
pub id: String,
}
#[non_exhaustive]
pub enum FindError {
NotFound(String),
}
pub struct Client;
impl Client {
pub fn find(&self, option: FindOption) -> Result<String, FindError> {
// find data with option
Ok("found".into())
}
}
src/lib.rs
use repository::{Client, FindError, FindOption};
fn find(client: &Client, option: FindOption) {
let result = client.find(option);
match result {
Ok(_) => {
// do something
}
Err(error) => {
match error {
FindError::NotFound(_) => {
// error handling
}
// #[non_exhaustive]があるため、_によるパターンマッチが必要
_ => panic!("unknown error"),
}
}
}
}
上記の例ではエラーのパターンマッチで省略を強制しているため、新しいエラーが追加されても利用者側はビルドが通るので、本当は新しいエラーの対応が必要だけどそれに気づかない可能性があります。
このような場合は#[non_exhaustive]
を使用しないほうがよいでしょう。
#[non_exhaustive]
はフィールドが増えたことを相手に知らせたほうがよいどうかという基準で考えるとよいかなと思います。
まとめ
-
#[non_exhaustive]
は互換性を維持できるように、パターン省略を利用者に強制させることができる - それにより構造体やenumのフィールドが増えてもビルドは通るため、互換性が維持される
- しかし、それだと問題が起きる場合があるので、フィールドが増えたことを相手に知らせたいかどうかという判断基準で使用するとよい
Discussion
「返って問題が起きるケースもあります」といいますが、わざわざライブラリー側で
non_exhaustive
を付けてやっているのに_ => panic!()
みたいなコードを書いちゃうようなユーザーのことなんか気にする必要ありますかね?non_exhaustive
を付けていなくても_ => panic!()
と書いてたらどうせ enum の variant が増えたことに気付けないので同じですよはい、結果的にそうかもしれないですね。
しかし
#[non_exhaustive]
をつけることによって、ユーザに_ => ...
を強制することになります。つまりユーザに
_ => panic!()
を書かせてしまう可能性があるということになります。明言していないですが、そこまで考慮したほうが良いんじゃないかという意味合いで書きました。
「気にする必要があるかどうか」はライブラリ作者が決めることなので、「そんなの必要がないよ」と考えているのであれば、それはそれで良いんじゃないかなと思います。