2.10. API设计原则之受约束性(constrained) Pt.1对类型进行修改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中我写了一个结构体名叫Unitpub struct Unit;然后我在main.rs中使用了Unitfn 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 { field: true, }; }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 some_function() { 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, window_height, } 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, window_height, } config {} }这样写就会报错因为这里的代码属于外部crate编译器就会禁止上面所说的两种操作输出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 {} | |_____^我们可以稍微改一下代码使main.rs中的匹配变成带..的非穷尽匹配if let Config { window_width, window_height, .. // 它用于忽略结构体、元组或枚举中的其余字段或变体 } config {}
【Rust中级教程】2.10. API设计原则之受约束性(constrained) Pt.1:对类型进行修改、`#[non_exhaustive]`注解
2.10. API设计原则之受约束性(constrained) Pt.1对类型进行修改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中我写了一个结构体名叫Unitpub struct Unit;然后我在main.rs中使用了Unitfn 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 { field: true, }; }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 some_function() { 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, window_height, } 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, window_height, } config {} }这样写就会报错因为这里的代码属于外部crate编译器就会禁止上面所说的两种操作输出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 {} | |_____^我们可以稍微改一下代码使main.rs中的匹配变成带..的非穷尽匹配if let Config { window_width, window_height, .. // 它用于忽略结构体、元组或枚举中的其余字段或变体 } config {}