喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
2.10.1. 接口的更改要三思
如果你的接口要做出对用户可见的更改,那么一定要三思而后行。
你需要确保你做出的变化:
- 不会破坏现有用户的代码
- 这次变化应该保留一段时间
频繁推送向后不兼容的更改(主版本增加),会导致用户的不满。
2.10.2. 向后不兼容的更改
有些向后不兼容的更改是显而易见的,比如说你改变公共类型的名称,或事为它添加一个新的公共方法。
有些向后不兼容的更改则很微妙,这与Rust的工作方式息息相关。这篇文章主要讲的就是这种更改,以及你作为开发者应该如何为其制定修改计划。
在这个过程中,有时候你就需要在接口的灵活性上做出权衡与妥协。
2.10.3. 对类型进行修改
如果你移除或重命名一个公共类型几乎肯定会破坏用户的代码,解决办法就是尽可能利用可见性修饰符。比如说:
pub(crate)
:对当前这个crate可见pub(in path)
:对指定的路径可见
看个例子:
pub mod outer_mod {
pub mod inner_mod {
// 该函数仅对 `outer_mod` 可见
pub(in crate::outer_mod) fn outer_mod_visible_fn() {}
// 该函数对整个 crate 可见
pub(crate) fn crate_visible_fn() {}
// 该函数仅对 `outer_mod` 可见(使用 `super` 指向外部模块)
pub(super) fn super_mod_visible_fn() {
// 由于 `inner_mod_visible_fn` 在相同模块内可见,可以正常调用
inner_mod_visible_fn();
}
// 该函数仅对 `inner_mod` 内部可见,相当于 `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();
// 该函数不再可见,因为我们已经在 `inner_mod` 之外
// Error! `inner_mod_visible_fn` 是私有的
inner_mod::inner_mod_visible_fn();
}
}
fn bar() {
// 这个函数仍然可见,因为我们在同一个 crate 内
outer_mod::inner_mod::crate_visible_fn();
// 这个函数在 `outer_mod` 之外不再可见
// Error! `super_mod_visible_fn` 是私有的
outer_mod::inner_mod::super_mod_visible_fn();
// 这个函数在 `outer_mod` 之外也不可见
// Error! `outer_mod_visible_fn` 是私有的
outer_mod::inner_mod::outer_mod_visible_fn();
outer_mod::foo();
}
inner_mod
模块中函数的可见性控制:
outer_mod_visible_fn()
: 仅在outer_mod
内部可见,外部无法访问。crate_visible_fn()
: 整个crate
可见,即bar()
仍然可以访问它。super_mod_visible_fn()
: 仅outer_mod
内部可见,bar()
无法访问。inner_mod_visible_fn()
: 私有,仅inner_mod
内部可见。
你写的API中公共类型越少,更改时就越自由(自由指保证不会破坏现有代码)。
#[non_exhaustive]
注解
用户的代码不仅仅通过名称依赖于你的类型。看个例子:
一个破坏性变更的例子
最开始在lib.rs中我写了一个结构体名叫Unit
:
pub struct Unit;
然后我在main.rs中使用了Unit
:
fn main() {
let u = constrained::Unit;
}
- 这没有任何问题。
后来呢,我对Unit
进行了一些修改,因为用户要用:
pub struct Unit {
pub field: bool;
}
在main.rs中代码也会变:
fn is_true(u: constrained::Unit) -> bool {
matches!(u, constrained::Unit { field: true })
}
fn main() {
let u = constrained::Unit;
}
is_true
这个函数用到了修改后Unit
的字段- 但是
main
函数中本来的代码就会报错
这种情况也会在Unit
的field
是私有字段时发生。因为编译器知道Unit
有字段,而你没有填写这个字段的值。
解决方案
针对这种情况,Rust提供了#[non_exhaustive]
注解来缓解这些问题。它可以引用于struct
、enum
和enum
的变体。这个注解表示类型或枚举在将来可能会添加更多字段或变体。
如果你使用了它,那么别人在使用你的crate时,编译器会:
- 禁止显式的构造,比如:
lib::Unit { field: true }
- 禁止非穷尽模式的匹配(即没有尾随
..
的模式)
如果你的接口比较稳定,就应该避免使用这个注解。
看例子:
lib.rs:
#[non_exhaustive]
pub struct Config {
pub window_width: u16,
pub window_height: u16,
}
fn SomeFunction() {
let config: Config = Config {
window_width: 640,
window_height: 480,
};
// Non-exhaustive structs can be matched on exhaustively within the defining crate.
if let Config {
window_width: u16,
window_height: u16,
} = config
{
// ...
}
}
- 标注了
#[non_exhaustive]
,lib.rs里仍然可以使用显式的构造,仍然可以使用非穷尽模式的匹配,因为这些代码与定义这个结构体的代码属于同一crate之内
那么我在main.rs这么写呢:
use constrained::Config;
fn main() {
let config: Config = Config {
window_width: 640,
window_height: 480,
};
if let Config {
window_width: u16,
window_height: u16,
} = config
}
- 这样写就会报错,因为这里的代码属于外部crate,编译器就会静止上面所说的两种操作
我们可以稍微改一下代码使main.rs中的非穷尽模式的匹配变成穷尽模式的匹配:
if let Config {
window_width: u16,
window_height: u16,
.. // 它用于忽略结构体、元组或枚举中的其余字段或变体
} = config