Updated extensibility to discuss non_exhaustive (#135)
parent
fa29385292
commit
567a1f16fb
@ -1,46 +1,122 @@
|
||||
# Privacy for extensibility
|
||||
# `#[non_exhaustive]` and private fields for extensibility
|
||||
|
||||
## Description
|
||||
|
||||
Use a private field to ensure that a struct is extensible without breaking
|
||||
stability guarantees.
|
||||
A small set of scenarios exist where a library author may want to add public
|
||||
fields to a public struct or new variants to an enum without breaking backwards
|
||||
compatibility.
|
||||
|
||||
Rust offers two solutions to this problem:
|
||||
|
||||
- Use `#[non_exhaustive]` on `struct`s, `enum`s, and `enum` variants.
|
||||
For extensive documentation on all the places where `#[non_exhaustive]` can be
|
||||
used, see [the docs](https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute).
|
||||
|
||||
- You may add a private field to a struct to prevent it from being directly
|
||||
instantiated or matched against (see Alternative)
|
||||
|
||||
## Example
|
||||
|
||||
```rust,ignore
|
||||
```rust
|
||||
mod a {
|
||||
// Public struct.
|
||||
#[non_exhaustive]
|
||||
pub struct S {
|
||||
pub foo: i32,
|
||||
// Private field.
|
||||
bar: i32,
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
pub enum AdmitMoreVariants {
|
||||
VariantA,
|
||||
VariantB,
|
||||
#[non_exhaustive]
|
||||
VariantC { a: String }
|
||||
}
|
||||
}
|
||||
|
||||
fn main(s: a::S) {
|
||||
// Because S::bar is private, it cannot be named here and we must use `..`
|
||||
// in the pattern.
|
||||
fn print_matched_variants(s: a::S) {
|
||||
// Because S is `#[non_exhaustive]`, it cannot be named here and
|
||||
// we must use `..` in the pattern.
|
||||
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"),
|
||||
|
||||
// .. required because this variant is non-exhaustive as well
|
||||
a::AdmitMoreVariants::VariantC { a, .. } => println!("it's a c"),
|
||||
|
||||
// The wildcard match is required because more variants may be
|
||||
// added in the future
|
||||
_ => println!("it's a new variant")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Discussion
|
||||
## Alternative: `Private fields` for structs
|
||||
|
||||
`#[non_exhaustive]` only works across crate boundaries.
|
||||
Within a crate, the private field method may be used.
|
||||
|
||||
Adding a field to a struct is a mostly backwards compatible change.
|
||||
However, if a client uses a pattern to deconstruct a struct instance, they
|
||||
might name all the fields in the struct and adding a new one would break that
|
||||
pattern. The client could name some fields and use `..` in the pattern,
|
||||
in which case adding another field is backwards compatible. Making at least one
|
||||
of the struct's fields private forces clients to use the latter form of patterns,
|
||||
ensuring that the struct is future-proof.
|
||||
pattern.
|
||||
The client could name some fields and use `..` in the pattern, in which case adding
|
||||
another field is backwards compatible.
|
||||
Making at least one of the struct's fields private forces clients to use the latter
|
||||
form of patterns, ensuring that the struct is future-proof.
|
||||
|
||||
The downside of this approach is that you might need to add an otherwise unneeded
|
||||
field to the struct. You can use the `()` type so that there is no runtime overhead
|
||||
and prepend `_` to the field name to avoid the unused field warning.
|
||||
|
||||
If Rust allowed private variants of enums, we could use the same trick to make
|
||||
adding a variant to an enum backwards compatible. The problem there is exhaustive
|
||||
match expressions. A private variant would force clients to have a `_` wildcard
|
||||
pattern. A common way to implement this instead is using the [`#[non_exhaustive]`](<https://doc.rust-lang.org/reference/attributes/type_system.html>)
|
||||
attribute.
|
||||
field to the struct.
|
||||
You can use the `()` type so that there is no runtime overhead and prepend `_` to
|
||||
the field name to avoid the unused field warning.
|
||||
|
||||
```rust
|
||||
pub struct S {
|
||||
pub a: i32,
|
||||
// Because `b` is private, you cannot match on `S` without using `..` and `S`
|
||||
// cannot be directly instantiated or matched against
|
||||
_b: ()
|
||||
}
|
||||
```
|
||||
|
||||
## Discussion
|
||||
|
||||
On `struct`s, `#[non_exhaustive]` allows adding additional fields in a backwards
|
||||
compatible way.
|
||||
It will also prevent clients from using the struct constructor, even if all the
|
||||
fields are public.
|
||||
This may be helpful, but it's worth considering if you _want_ an additional field
|
||||
to be found by clients as a compiler error rather than something that may be silently
|
||||
undiscovered.
|
||||
|
||||
`#[non_exhaustive]` can be applied to enum variants as well.
|
||||
A `#[non_exhaustive]` variant behaves in the same way as a `#[non_exhaustive]` struct.
|
||||
|
||||
Use this deliberately and with caution: incrementing the major version when adding
|
||||
fields or variants is often a better option.
|
||||
`#[non_exhaustive]` may be appropriate in scenarios where you're modeling an external
|
||||
resource that may change out-of-sync with your library, but is not a general purpose
|
||||
tool.
|
||||
|
||||
### Disadvantages
|
||||
|
||||
`#[non_exhaustive]` can make your code much less ergonomic to use, especially when
|
||||
forced to handle unknown enum variants.
|
||||
It should only be used when these sorts of evolutions are required **without**
|
||||
incrementing the major version.
|
||||
|
||||
When `#[non_exhaustive]` is applied to `enum`s, it forces clients to handle a
|
||||
wildcard variant.
|
||||
If there is no sensible action to take in this case, this may lead to awkward
|
||||
code and code paths that are only executed in extremely rare circumstances.
|
||||
If a client decides to `panic!()` in this scenario, it may have been better to
|
||||
expose this error at compile time.
|
||||
In fact, `#[non_exhaustive]` forces clients to handle the "Something else" case;
|
||||
there is rarely a sensible action to take in this scenario.
|
||||
|
||||
## See also
|
||||
|
||||
- [RFC introducing #[non_exhaustive] attribute for enums and structs](https://github.com/rust-lang/rfcs/blob/master/text/2008-non-exhaustive.md)
|
||||
|
Loading…
Reference in New Issue