2.10.1. Think Carefully Before Changing an Interface
If your interface is going to change in a way that is visible to users, think twice before doing it.
You need to make sure that the changes you make:
- Do not break existing user code
- Should remain in place for a while
Frequently shipping backward-incompatible changes (major version bumps) will make users unhappy.
2.10.2. Backward-Incompatible Changes
Some backward-incompatible changes are obvious, such as changing the name of a public type or removing a public item from it.
Some backward-incompatible changes are more subtle and are closely tied to how Rust works. This article mainly focuses on those changes and how you, as a developer, should plan for them.
In the process, you sometimes need to make trade-offs and compromises in interface flexibility.
2.10.3. Modifying Types
If you remove or rename a public type, it will almost certainly break user code. The solution is to use visibility modifiers as much as possible. For example:
-
pub(crate): visible within the current crate -
pub(in path): visible within the specified path
Example:
pub mod outer_mod {
pub mod inner_mod {
// This function is visible only to `outer_mod`
pub(in crate::outer_mod) fn outer_mod_visible_fn() {}
// This function is visible to the entire crate
pub(crate) fn crate_visible_fn() {}
// This function is visible only to `outer_mod` (using `super` to refer to the outer module)
pub(super) fn super_mod_visible_fn() {
// `inner_mod_visible_fn` is visible within the same module, so it can be called normally
inner_mod_visible_fn();
}
// This function is visible only inside `inner_mod`, equivalent to `private`
pub(self) fn inner_mod_visible_fn() {}
}
pub fn foo() {
inner_mod::outer_mod_visible_fn();
inner_mod::crate_visible_fn();
inner_mod::super_mod_visible_fn();
// This function is no longer visible because we are outside `inner_mod`
// Error! `inner_mod_visible_fn` is private
inner_mod::inner_mod_visible_fn();
}
}
fn bar() {
// This function is still visible because we are in the same crate
outer_mod::inner_mod::crate_visible_fn();
// This function is no longer visible outside `outer_mod`
// Error! `super_mod_visible_fn` is private
outer_mod::inner_mod::super_mod_visible_fn();
// This function is also not visible outside `outer_mod`
// Error! `outer_mod_visible_fn` is private
outer_mod::inner_mod::outer_mod_visible_fn();
outer_mod::foo();
}
Visibility control for the functions in the inner_mod module:
-
outer_mod_visible_fn(): visible only insideouter_mod, not accessible from outside -
crate_visible_fn(): visible to the entire crate, sobar()can still access it -
super_mod_visible_fn(): visible only insideouter_mod, sobar()cannot access it -
inner_mod_visible_fn(): private, visible only insideinner_mod
The fewer public types you expose in your API, the more freedom you have to change it later (freedom here means not breaking existing code).
#[non_exhaustive] Attribute
User code depends on more than just the name of your type. Example:
An Example of a Breaking Change
At the beginning, I wrote a struct called Unit in lib.rs:
pub struct Unit;
Then I used Unit in main.rs:
fn main() {
let u = constrained::Unit;
}
- That works fine.
Later, I modified Unit because users needed it:
pub struct Unit {
pub field: bool,
}
The code in main.rs would also change:
fn is_true(u: constrained::Unit) -> bool {
matches!(u, constrained::Unit { field: true })
}
fn main() {
let u = constrained::Unit {
field: true,
};
}
- The
is_truefunction uses the modifiedUnitfield - But the original code in
mainwould then fail to compile
The same thing happens when Unit has a private field. The compiler knows that Unit has fields, but you did not provide values for them.
Solution
For this situation, Rust provides the #[non_exhaustive] attribute to mitigate these problems. It can be applied to struct, enum, and enum variants. It indicates that the type or enum may gain more fields or variants in the future.
If you use it, then when others use your crate, the compiler will:
- Forbid explicit construction, such as
lib::Unit { field: true } - Forbid non-exhaustive pattern matching, that is, patterns without a trailing
..
If your interface is relatively stable, you should avoid using this attribute.
Example:
lib.rs:
#[non_exhaustive]
pub struct Config {
pub window_width: u16,
pub window_height: u16,
}
fn some_function() {
let config: Config = Config {
window_width: 640,
window_height: 480,
};
// Non-exhaustive structs can be matched exhaustively within the defining crate.
if let Config {
window_width,
window_height,
} = config
{
// ...
}
}
- With
#[non_exhaustive],lib.rscan still use explicit construction and exhaustive matching, because this code belongs to the same crate that defines the struct
What if I write this in main.rs?
use constrained::Config;
fn main() {
let config: Config = Config {
window_width: 640,
window_height: 480,
};
if let Config {
window_width,
window_height,
} = config {}
}
- This will fail to compile, because this code belongs to an external crate, and the compiler will prohibit the two operations mentioned above
Output:
error[E0639]: cannot create non-exhaustive struct using struct expression
--> src/main.rs:4:26
|
4 | let config: Config = Config {
| __________________________^
5 | | window_width: 640,
6 | | window_height: 480,
7 | | };
| |_____^
error[E0638]: `..` required with struct marked as non-exhaustive
--> src/main.rs:9:12
|
9 | if let Config {
| ____________^
10 | | window_width,
11 | | window_height,
12 | | } = config {}
| |_____^
We can slightly change the code so that the match in main.rs becomes a non-exhaustive pattern with ..:
if let Config {
window_width,
window_height,
.. // this ignores the remaining fields or variants in a struct, tuple, or enum
} = config {
}
Top comments (0)