SemVer 兼容性

本章详细介绍了在发布新包版本时,哪些变更通常被认为是兼容的或破坏性的 SemVer 变更。有关 SemVer 是什么以及 Cargo 如何使用它来确保库兼容性的详细信息,请参阅 SemVer 兼容性 部分。

这些只是指导原则,并非所有项目都必须遵守的硬性规定。变更类别 部分详细介绍了本指南如何对变更的级别和严重性进行分类。本指南的大部分内容着眼于会导致 cargorustc 无法构建以前可以构建的东西的变更。几乎所有变更都带有一些可能会对运行时行为产生负面影响的风险,对于这些情况,项目维护者通常需要自行判断这是否是一个与 SemVer 不兼容的变更。

变更类别

以下列出的所有策略都根据变更级别进行了分类

  • 主要变更:需要进行 SemVer 主要版本号提升的变更。
  • 次要变更:只需要进行 SemVer 次要版本号提升的变更。
  • 可能破坏性变更:有些项目可能认为是主要变更,而另一些项目可能认为是次要变更的变更。

“可能破坏性变更”类别涵盖了那些在更新过程中有可能导致破坏,但不一定一定会导致破坏的变更。应仔细考虑这些变更的影响。具体性质将取决于变更本身以及项目维护者的原则。

有些项目可能会选择在次要变更时仅提升补丁版本号。鼓励遵循 SemVer 规范,并且只在补丁版本中应用错误修复。然而,错误修复可能需要进行被标记为“次要变更”的 API 变更,但这不应影响兼容性。本指南不就如何处理每一个“次要变更”发表立场,因为次要变更和补丁变更之间的区别是取决于变更性质的约定。

有些变更被标记为“次要变更”,即使它们带有潜在的破坏构建的风险。这适用于潜在风险极低的情况,并且潜在的破坏性代码不太可能以惯用的 Rust 风格编写,或者明确不鼓励使用。

本指南使用“主要”和“次要”术语时,假定这与“1.0.0”或更高版本有关。以“0.y.z”开头的初始开发版本可以将“y”中的变更视为主要版本,将“z”视为次要版本。“0.0.z”版本总是主要变更。这是因为 Cargo 使用的约定是只有最左边的非零组件中的变更才被视为不兼容。

API 兼容性

下面的所有示例都包含三个部分:原始代码、修改后的代码以及可能出现在其他项目中的代码使用示例。在次要变更中,示例使用应该在变更前和变更后版本中都能成功构建。

主要:重命名/移动/移除任何公共项

缺少公共暴露的 将导致使用该项的代码编译失败。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub fn foo() {}

///////////////////////////////////////////////////////////
// After
// ... item has been removed

///////////////////////////////////////////////////////////
// Example usage that will break.
fn main() {
    updated_crate::foo(); // Error: cannot find function `foo`
}

这包括添加任何类型的 cfg 属性,这可以根据 条件编译 改变哪些项或行为可用。

缓解策略

  • 将要移除的项标记为 已废弃,然后在后续的 SemVer 破坏性版本中移除它们。
  • 将重命名的项标记为 已废弃,并使用 pub use 项将旧名称重新导出。

次要:添加新的公共项

添加新的公共 是次要变更。

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
// ... absence of item

///////////////////////////////////////////////////////////
// After
pub fn foo() {}

///////////////////////////////////////////////////////////
// Example use of the library that will safely work.
// `foo` is not used since it didn't previously exist.

请注意,在一些罕见情况下,这可能会由于 glob imports(通配符导入)而成为破坏性变更。例如,如果你添加了一个新的 trait,并且一个项目使用了 glob import 将该 trait 引入范围,而新的 trait 引入了一个与其实现所在的任何类型冲突的关联项,这可能会导致由于歧义引起的编译时错误。示例

// Breaking change example

///////////////////////////////////////////////////////////
// Before
// ... absence of trait

///////////////////////////////////////////////////////////
// After
pub trait NewTrait {
    fn foo(&self) {}
}

impl NewTrait for i32 {}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::*;

pub trait LocalTrait {
    fn foo(&self) {}
}

impl LocalTrait for i32 {}

fn main() {
    123i32.foo(); // Error:  multiple applicable items in scope
}

这不被视为主要变更,因为根据惯例,glob imports 是已知的前向兼容性风险。应避免对外部 crate 中的项进行 glob imports。

主要:更改定义明确的类型的对齐方式、布局或大小

更改先前定义明确的类型的对齐方式、布局或大小是破坏性变更。

通常,使用 默认表示 的类型没有明确定义的对齐方式、布局或大小。编译器可以自由更改其对齐方式、布局或大小,因此代码不应对此做任何假设。

注意:即使外部 crate 假设了未明确定义的类型的对齐方式、布局或大小,也可能导致外部 crate 崩溃。这不被视为 SemVer 破坏性变更,因为不应该做出这些假设。

一些非破坏性变更的示例(假设没有违反本指南中的其他规则)

使用 repr 属性 的类型可以说在某种程度上定义了其对齐方式和布局,代码可能对此做出一些假设,而更改该类型可能会导致这些假设失效。

在某些情况下,带有 repr 属性的类型可能没有明确定义的对齐方式、布局或大小。在这些情况下,更改类型可能是安全的,但应谨慎行事。例如,具有私有字段且未以其他方式记录其对齐方式、布局或大小保证的类型,外部 crate 不能依赖它,因为公共 API 并未完全定义该类型的对齐方式、布局或大小。

一个具有私有字段但定义明确的常见示例是:一个具有单个私有字段(该字段是泛型类型),并使用 repr(transparent),且文档中说明它对该泛型类型是透明的类型。例如,参见 UnsafeCell

一些破坏性变更的示例:

次要:repr(C) 添加、移除或更改私有字段

通常可以安全地添加、移除或更改 repr(C) 结构体、联合体或枚举的私有字段,前提是遵循本指南中的其他准则(参见 struct-add-private-field-when-publicstruct-add-public-field-when-no-privatestruct-private-fields-with-privateenum-fields-new)。

例如,只有在已经存在其他私有字段或该类型是 non_exhaustive 时才能添加私有字段。只有在存在私有字段或该类型是 non_exhaustive,并且添加不会改变其他字段的布局时,才能添加公共字段。

然而,这可能会改变类型的大小和对齐方式。如果大小或对齐方式发生变化,应谨慎处理。除非有文档说明大小或对齐方式,否则代码不应假定具有私有字段或 non_exhaustive 的类型的大小或对齐方式。

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[derive(Default)]
#[repr(C)]
pub struct Example {
    pub f1: i32,
    f2: i32, // a private field
}

///////////////////////////////////////////////////////////
// After
#[derive(Default)]
#[repr(C)]
pub struct Example {
    pub f1: i32,
    f2: i32,
    f3: i32, // a new field
}

///////////////////////////////////////////////////////////
// Example use of the library that will safely work.
fn main() {
    // NOTE: Users should not make assumptions about the size or alignment
    // since they are not documented.
    let f = updated_crate::Example::default();
}

次要:repr(C) 添加枚举变体

通常可以安全地向 repr(C) 枚举添加变体,前提是该枚举使用了 non_exhaustive。更多讨论请参见 enum-variant-new

请注意,这可能是一个破坏性变更,因为它会改变类型的大小和对齐方式。类似的问题请参见 repr-c-private-change

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[repr(C)]
#[non_exhaustive]
pub enum Example {
    Variant1 { f1: i16 },
    Variant2 { f1: i32 },
}

///////////////////////////////////////////////////////////
// After
#[repr(C)]
#[non_exhaustive]
pub enum Example {
    Variant1 { f1: i16 },
    Variant2 { f1: i32 },
    Variant3 { f1: i64 }, // added
}

///////////////////////////////////////////////////////////
// Example use of the library that will safely work.
fn main() {
    // NOTE: Users should not make assumptions about the size or alignment
    // since they are not specified. For example, this raised the size from 8
    // to 16 bytes.
    let f = updated_crate::Example::Variant2 { f1: 123 };
}

次要:向默认表示的类型添加 repr(C)

可以安全地向使用 默认表示 的结构体、联合体或枚举添加 repr(C)。这是安全的,因为用户不应假定使用默认表示的类型的对齐方式、布局或大小。

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub struct Example {
    pub f1: i32,
    pub f2: i16,
}

///////////////////////////////////////////////////////////
// After
#[repr(C)] // added
pub struct Example {
    pub f1: i32,
    pub f2: i16,
}

///////////////////////////////////////////////////////////
// Example use of the library that will safely work.
fn main() {
    let f = updated_crate::Example { f1: 123, f2: 456 };
}

次要:向枚举添加 repr(<int>)

可以安全地向使用 默认表示 的枚举添加 repr(<int>) 原语表示。这是安全的,因为用户不应假定使用默认表示的枚举的对齐方式、布局或大小。

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub enum E {
    Variant1,
    Variant2(i32),
    Variant3 { f1: f64 },
}

///////////////////////////////////////////////////////////
// After
#[repr(i32)] // added
pub enum E {
    Variant1,
    Variant2(i32),
    Variant3 { f1: f64 },
}

///////////////////////////////////////////////////////////
// Example use of the library that will safely work.
fn main() {
    let x = updated_crate::E::Variant3 { f1: 1.23 };
}

次要:向默认表示的结构体或枚举添加 repr(transparent)

可以安全地向使用 默认表示 的结构体或枚举添加 repr(transparent)。这是安全的,因为用户不应假定使用默认表示的结构体或枚举的对齐方式、布局或大小。

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[derive(Default)]
pub struct Example<T>(T);

///////////////////////////////////////////////////////////
// After
#[derive(Default)]
#[repr(transparent)] // added
pub struct Example<T>(T);

///////////////////////////////////////////////////////////
// Example use of the library that will safely work.
fn main() {
    let x = updated_crate::Example::<i32>::default();
}

主要:向结构体或联合体添加 repr(packed)

向结构体或联合体添加 repr(packed) 是破坏性变更。将类型设置为 repr(packed) 会导致代码无法运行,例如不允许获取字段的引用,或者导致不相交闭包捕获被截断。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub struct Example {
    pub f1: u8,
    pub f2: u16,
}

///////////////////////////////////////////////////////////
// After
#[repr(packed)] // added
pub struct Example {
    pub f1: u8,
    pub f2: u16,
}

///////////////////////////////////////////////////////////
// Example usage that will break.
fn main() {
    let f = updated_crate::Example { f1: 1, f2: 2 };
    let x = &f.f2; // Error: reference to packed field is unaligned
}
// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub struct Example(pub i32, pub i32);

///////////////////////////////////////////////////////////
// After
#[repr(packed)]
pub struct Example(pub i32, pub i32);

///////////////////////////////////////////////////////////
// Example usage that will break.
fn main() {
    let mut f = updated_crate::Example(123, 456);
    let c = || {
        // Without repr(packed), the closure precisely captures `&f.0`.
        // With repr(packed), the closure captures `&f` to avoid undefined behavior.
        let a = f.0;
    };
    f.1 = 789; // Error: cannot assign to `f.1` because it is borrowed
    c();
}

主要:向结构体、联合体或枚举添加 repr(align)

向结构体、联合体或枚举添加 repr(align) 是破坏性变更。将类型设置为 repr(align) 会导致在 repr(packed) 类型中使用该类型时出错,因为这种组合是不允许的。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub struct Aligned {
    pub a: i32,
}

///////////////////////////////////////////////////////////
// After
#[repr(align(8))] // added
pub struct Aligned {
    pub a: i32,
}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::Aligned;

#[repr(packed)]
pub struct Packed { // Error: packed type cannot transitively contain a `#[repr(align)]` type
    f1: Aligned,
}

fn main() {
    let p = Packed {
        f1: Aligned { a: 123 },
    };
}

主要:从结构体或联合体移除 repr(packed)

从结构体或联合体移除 repr(packed) 是破坏性变更。这可能会改变外部 crate 所依赖的对齐方式或布局。

如果任何字段是公共的,那么移除 repr(packed) 可能会改变不相交闭包捕获的工作方式。在某些情况下,这可能导致代码出错,类似于 edition guide 中概述的情况。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[repr(C, packed)]
pub struct Packed {
    pub a: u8,
    pub b: u16,
}

///////////////////////////////////////////////////////////
// After
#[repr(C)] // removed packed
pub struct Packed {
    pub a: u8,
    pub b: u16,
}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::Packed;

fn main() {
    let p = Packed { a: 1, b: 2 };
    // Some assumption about the size of the type.
    // Without `packed`, this fails since the size is 4.
    const _: () = assert!(std::mem::size_of::<Packed>() == 3); // Error: evaluation of constant value failed
}
// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[repr(C, packed)]
pub struct Packed {
    pub a: *mut i32,
    pub b: i32,
}
unsafe impl Send for Packed {}

///////////////////////////////////////////////////////////
// After
#[repr(C)] // removed packed
pub struct Packed {
    pub a: *mut i32,
    pub b: i32,
}
unsafe impl Send for Packed {}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::Packed;

fn main() {
    let mut x = 123;

    let p = Packed {
        a: &mut x as *mut i32,
        b: 456,
    };

    // When the structure was packed, the closure captures `p` which is Send.
    // When `packed` is removed, this ends up capturing `p.a` which is not Send.
    std::thread::spawn(move || unsafe {
        *(p.a) += 1; // Error: cannot be sent between threads safely
    });
}

主要:如果更改 repr(packed(N)) 中的值 N 会改变对齐方式或布局

更改 repr(packed(N)) 中的值 N,如果这会改变对齐方式或布局,则是破坏性变更。这可能会改变外部 crate 所依赖的对齐方式或布局。

如果将值 N 降低到低于公共字段的对齐方式,那么任何尝试获取该字段引用的代码都将出错。

请注意,对 N 的某些更改可能不会改变对齐方式或布局,例如,当当前值已等于类型的自然对齐方式时增加它。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[repr(packed(4))]
pub struct Packed {
    pub a: u8,
    pub b: u32,
}

///////////////////////////////////////////////////////////
// After
#[repr(packed(2))] // changed to 2
pub struct Packed {
    pub a: u8,
    pub b: u32,
}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::Packed;

fn main() {
    let p = Packed { a: 1, b: 2 };
    let x = &p.b; // Error: reference to packed field is unaligned
}

主要:如果更改 repr(align(N)) 中的值 N 会改变对齐方式

更改 repr(align(N)) 中的值 N,如果这会改变对齐方式,则是破坏性变更。这可能会改变外部 crate 所依赖的对齐方式。

如果该类型没有明确定义(如存在任何私有字段且对齐方式或布局未文档化),则进行此更改应该是安全的,如 类型布局 中所述。

请注意,对 N 的某些更改可能不会改变对齐方式或布局,例如,当当前值已等于或小于类型的自然对齐方式时减小它。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[repr(align(8))]
pub struct Packed {
    pub a: u8,
    pub b: u32,
}

///////////////////////////////////////////////////////////
// After
#[repr(align(4))] // changed to 4
pub struct Packed {
    pub a: u8,
    pub b: u32,
}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::Packed;

fn main() {
    let p = Packed { a: 1, b: 2 };
    // Some assumption about the size of the type.
    // The alignment has changed from 8 to 4.
    const _: () = assert!(std::mem::align_of::<Packed>() == 8); // Error: evaluation of constant value failed
}

主要:从结构体、联合体或枚举移除 repr(align)

从结构体、联合体或枚举移除 repr(align),如果其布局是明确定义的,则是破坏性变更。这可能会改变外部 crate 所依赖的对齐方式或布局。

如果该类型没有明确定义(如存在任何私有字段且对齐方式未文档化),则进行此更改应该是安全的,如 类型布局 中所述。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[repr(C, align(8))]
pub struct Packed {
    pub a: u8,
    pub b: u32,
}

///////////////////////////////////////////////////////////
// After
#[repr(C)] // removed align
pub struct Packed {
    pub a: u8,
    pub b: u32,
}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::Packed;

fn main() {
    let p = Packed { a: 1, b: 2 };
    // Some assumption about the size of the type.
    // The alignment has changed from 8 to 4.
    const _: () = assert!(std::mem::align_of::<Packed>() == 8); // Error: evaluation of constant value failed
}

主要:更改 repr(C) 类型中公共字段的顺序

更改 repr(C) 类型中公共字段的顺序是破坏性变更。外部 crate 可能依赖于字段的特定顺序。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[repr(C)]
pub struct SpecificLayout {
    pub a: u8,
    pub b: u32,
}

///////////////////////////////////////////////////////////
// After
#[repr(C)]
pub struct SpecificLayout {
    pub b: u32, // changed order
    pub a: u8,
}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::SpecificLayout;

unsafe extern "C" {
    // This C function is assuming a specific layout defined in a C header.
    fn c_fn_get_b(x: &SpecificLayout) -> u32;
}

fn main() {
    let p = SpecificLayout { a: 1, b: 2 };
    unsafe { assert_eq!(c_fn_get_b(&p), 2) } // Error: value not equal to 2
}

mod cdep {
    // This simulates what would normally be something included from a build script.
    // This definition would be in a C header.
    #[repr(C)]
    pub struct SpecificLayout {
        pub a: u8,
        pub b: u32,
    }

    #[no_mangle]
    pub fn c_fn_get_b(x: &SpecificLayout) -> u32 {
        x.b
    }
}

主要:从结构体、联合体或枚举移除 repr(C)

从结构体、联合体或枚举移除 repr(C) 是破坏性变更。外部 crate 可能依赖于类型的特定布局。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[repr(C)]
pub struct SpecificLayout {
    pub a: u8,
    pub b: u32,
}

///////////////////////////////////////////////////////////
// After
// removed repr(C)
pub struct SpecificLayout {
    pub a: u8,
    pub b: u32,
}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::SpecificLayout;

unsafe extern "C" {
    // This C function is assuming a specific layout defined in a C header.
    fn c_fn_get_b(x: &SpecificLayout) -> u32; // Error: is not FFI-safe
}

fn main() {
    let p = SpecificLayout { a: 1, b: 2 };
    unsafe { assert_eq!(c_fn_get_b(&p), 2) }
}

mod cdep {
    // This simulates what would normally be something included from a build script.
    // This definition would be in a C header.
    #[repr(C)]
    pub struct SpecificLayout {
        pub a: u8,
        pub b: u32,
    }

    #[no_mangle]
    pub fn c_fn_get_b(x: &SpecificLayout) -> u32 {
        x.b
    }
}

主要:从枚举移除 repr(<int>)

从枚举移除 repr(<int>) 是破坏性变更。外部 crate 可能假定判别式具有特定大小。例如,对枚举使用 std::mem::transmute 可能会失败。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[repr(u16)]
pub enum Example {
    Variant1,
    Variant2,
    Variant3,
}

///////////////////////////////////////////////////////////
// After
// removed repr(u16)
pub enum Example {
    Variant1,
    Variant2,
    Variant3,
}

///////////////////////////////////////////////////////////
// Example usage that will break.

fn main() {
    let e = updated_crate::Example::Variant2;
    let i: u16 = unsafe { std::mem::transmute(e) }; // Error: cannot transmute between types of different sizes
}

主要:更改 repr(<int>) 枚举的原语表示

更改 repr(<int>) 枚举的原语表示是破坏性变更。外部 crate 可能假定判别式具有特定大小。例如,对枚举使用 std::mem::transmute 可能会失败。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[repr(u16)]
pub enum Example {
    Variant1,
    Variant2,
    Variant3,
}

///////////////////////////////////////////////////////////
// After
#[repr(u8)] // changed repr size
pub enum Example {
    Variant1,
    Variant2,
    Variant3,
}

///////////////////////////////////////////////////////////
// Example usage that will break.

fn main() {
    let e = updated_crate::Example::Variant2;
    let i: u16 = unsafe { std::mem::transmute(e) }; // Error: cannot transmute between types of different sizes
}

主要:从结构体或枚举移除 repr(transparent)

从结构体或枚举移除 repr(transparent) 是破坏性变更。外部 crate 可能依赖于该类型具有透明字段的对齐方式、布局或大小。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[repr(transparent)]
pub struct Transparent<T>(T);

///////////////////////////////////////////////////////////
// After
// removed repr
pub struct Transparent<T>(T);

///////////////////////////////////////////////////////////
// Example usage that will break.
#![deny(improper_ctypes)]
use updated_crate::Transparent;

unsafe extern "C" {
    fn c_fn() -> Transparent<f64>; // Error: is not FFI-safe
}

fn main() {}

主要:当所有当前字段都是公共的时,添加私有结构体字段

当向以前所有字段都是公共的结构体添加私有字段时,任何尝试使用 结构体字面量 构建它的代码都将出错。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub struct Foo {
    pub f1: i32,
}

///////////////////////////////////////////////////////////
// After
pub struct Foo {
    pub f1: i32,
    f2: i32,
}

///////////////////////////////////////////////////////////
// Example usage that will break.
fn main() {
    let x = updated_crate::Foo { f1: 123 }; // Error: cannot construct `Foo`
}

缓解策略

  • 不要向只有公共字段的结构体添加新字段。
  • 在首次引入结构体时将其标记为 #[non_exhaustive],以防止用户使用结构体字面量语法,并提供构造方法和/或 Default 实现作为替代。

主要:当不存在私有字段时,添加公共字段

当向所有字段都是公共的结构体添加公共字段时,任何尝试使用 结构体字面量 构建它的代码都将出错。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub struct Foo {
    pub f1: i32,
}

///////////////////////////////////////////////////////////
// After
pub struct Foo {
    pub f1: i32,
    pub f2: i32,
}

///////////////////////////////////////////////////////////
// Example usage that will break.
fn main() {
    let x = updated_crate::Foo { f1: 123 }; // Error: missing field `f2`
}

缓解策略

  • 不要向只有公共字段的结构体添加新字段。
  • 在首次引入结构体时将其标记为 #[non_exhaustive],以防止用户使用结构体字面量语法,并提供构造方法和/或 Default 实现作为替代。

次要:当至少已存在一个私有字段时,添加或移除私有字段

当结构体已经至少有一个私有字段时,添加或移除私有字段是安全的。

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[derive(Default)]
pub struct Foo {
    f1: i32,
}

///////////////////////////////////////////////////////////
// After
#[derive(Default)]
pub struct Foo {
    f2: f64,
}

///////////////////////////////////////////////////////////
// Example use of the library that will safely work.
fn main() {
    // Cannot access private fields.
    let x = updated_crate::Foo::default();
}

这是安全的,因为现有代码无法使用 结构体字面量 来构造它,也无法穷尽地匹配其内容。

请注意,对于元组结构体,如果元组包含公共字段,并且添加或移除私有字段改变了任何公共字段的索引,则这是一个主要变更

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[derive(Default)]
pub struct Foo(pub i32, i32);

///////////////////////////////////////////////////////////
// After
#[derive(Default)]
pub struct Foo(f64, pub i32, i32);

///////////////////////////////////////////////////////////
// Example usage that will break.
fn main() {
    let x = updated_crate::Foo::default();
    let y = x.0; // Error: is private
}

次要:从所有字段都是私有的元组结构体(至少有一个字段)更改为普通结构体,或反之

如果所有字段都是私有的,将元组结构体更改为普通结构体(或反之)是安全的。

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[derive(Default)]
pub struct Foo(i32);

///////////////////////////////////////////////////////////
// After
#[derive(Default)]
pub struct Foo {
    f1: i32,
}

///////////////////////////////////////////////////////////
// Example use of the library that will safely work.
fn main() {
    // Cannot access private fields.
    let x = updated_crate::Foo::default();
}

这是安全的,因为现有代码无法使用 结构体字面量 来构造它,也无法匹配其内容。

主要:添加新的枚举变体(不使用 non_exhaustive

如果枚举不使用 #[non_exhaustive] 属性,添加新的枚举变体是破坏性变更。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub enum E {
    Variant1,
}

///////////////////////////////////////////////////////////
// After
pub enum E {
    Variant1,
    Variant2,
}

///////////////////////////////////////////////////////////
// Example usage that will break.
fn main() {
    use updated_crate::E;
    let x = E::Variant1;
    match x { // Error: `E::Variant2` not covered
        E::Variant1 => {}
    }
}

缓解策略

主要:向枚举变体添加新的字段

向枚举变体添加新字段是破坏性变更,因为所有字段都是公共的,构造函数和匹配将编译失败。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub enum E {
    Variant1 { f1: i32 },
}

///////////////////////////////////////////////////////////
// After
pub enum E {
    Variant1 { f1: i32, f2: i32 },
}

///////////////////////////////////////////////////////////
// Example usage that will break.
fn main() {
    use updated_crate::E;
    let x = E::Variant1 { f1: 1 }; // Error: missing f2
    match x {
        E::Variant1 { f1 } => {} // Error: missing f2
    }
}

缓解策略

  • 在引入枚举时,将变体标记为 non_exhaustive,以便在不使用通配符的情况下无法构造或匹配它。
    pub enum E {
        #[non_exhaustive]
        Variant1{f1: i32}
    }
  • 在引入枚举时,使用一个显式结构体作为值,这样可以控制字段的可见性。
    pub struct Foo {
       f1: i32,
       f2: i32,
    }
    pub enum E {
        Variant1(Foo)
    }

主要:向 trait 添加非默认项

向 trait 添加非默认项是破坏性变更。这将破坏该 trait 的任何实现者。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub trait Trait {}

///////////////////////////////////////////////////////////
// After
pub trait Trait {
    fn foo(&self);
}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::Trait;
struct Foo;

impl Trait for Foo {}  // Error: not all trait items implemented

缓解策略

  • 对于新的关联 trait 项,始终提供默认实现或值。
  • 在引入 trait 时,使用 密封 trait 技术 来阻止 crate 外部的用户实现该 trait。

主要:对 trait 项签名的任何更改

对 trait 项签名进行任何更改是破坏性变更。这可能会破坏 trait 的外部实现者。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub trait Trait {
    fn f(&self, x: i32) {}
}

///////////////////////////////////////////////////////////
// After
pub trait Trait {
    // For sealed traits or normal functions, this would be a minor change
    // because generalizing with generics strictly expands the possible uses.
    // But in this case, trait implementations must use the same signature.
    fn f<V>(&self, x: V) {}
}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::Trait;
struct Foo;

impl Trait for Foo {
    fn f(&self, x: i32) {}  // Error: trait declaration has 1 type parameter
}

缓解策略

  • 引入带有默认实现的新项来覆盖新功能,而不是修改现有项。
  • 在引入 trait 时,使用 密封 trait 技术 来阻止 crate 外部的用户实现该 trait。

可能破坏性:添加带有默认实现的 trait 项

添加带有默认实现的 trait 项通常是安全的。然而,有时这可能导致编译错误。例如,如果另一个 trait 中存在同名方法,这可能会引入歧义。

// Breaking change example

///////////////////////////////////////////////////////////
// Before
pub trait Trait {}

///////////////////////////////////////////////////////////
// After
pub trait Trait {
    fn foo(&self) {}
}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::Trait;
struct Foo;

trait LocalTrait {
    fn foo(&self) {}
}

impl Trait for Foo {}
impl LocalTrait for Foo {}

fn main() {
    let x = Foo;
    x.foo(); // Error: multiple applicable items in scope
}

请注意,固有实现 的名称冲突不会产生歧义,因为它们优先于 trait 项。

添加 trait 项时,请参阅 trait-object-safety 以考虑特殊情况。

缓解策略

  • 一些项目可能认为这种破坏是可以接受的,特别是如果新项名称不太可能与任何现有代码冲突时。请仔细选择名称以帮助避免这些冲突。此外,要求下游用户在更新依赖项时添加 消歧语法 来选择正确的函数,这可能是可以接受的。

主要:添加使 trait 非对象安全的 trait 项

添加使 trait 不 对象安全 的 trait 项是破坏性变更。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub trait Trait {}

///////////////////////////////////////////////////////////
// After
pub trait Trait {
    // An associated const makes the trait not object-safe.
    const CONST: i32 = 123;
}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::Trait;
struct Foo;

impl Trait for Foo {}

fn main() {
    let obj: Box<dyn Trait> = Box::new(Foo); // Error: the trait `Trait` is not dyn compatible
}

执行相反操作(将非对象安全的 trait 变为对象安全)是安全的。

主要:添加没有默认值的类型参数

向 trait 添加没有默认值的类型参数是破坏性变更。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub trait Trait {}

///////////////////////////////////////////////////////////
// After
pub trait Trait<T> {}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::Trait;
struct Foo;

impl Trait for Foo {}  // Error: missing generics

缓解策略

次要:添加带有默认值的 trait 类型参数

只要 trait 类型参数带有默认值,添加它是安全的。外部实现者将使用默认值,而无需指定参数。

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub trait Trait {}

///////////////////////////////////////////////////////////
// After
pub trait Trait<T = i32> {}

///////////////////////////////////////////////////////////
// Example use of the library that will safely work.
use updated_crate::Trait;
struct Foo;

impl Trait for Foo {}

可能破坏性变更:添加任何固有项

通常向实现添加固有项应该是安全的,因为固有项优先于 trait 项。然而,在某些情况下,如果名称与已实现的 trait 项的签名不同,冲突可能会导致问题。

// Breaking change example

///////////////////////////////////////////////////////////
// Before
pub struct Foo;

///////////////////////////////////////////////////////////
// After
pub struct Foo;

impl Foo {
    pub fn foo(&self) {}
}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::Foo;

trait Trait {
    fn foo(&self, x: i32) {}
}

impl Trait for Foo {}

fn main() {
    let x = Foo;
    x.foo(1); // Error: this method takes 0 arguments but 1 argument was supplied
}

请注意,如果签名匹配,则不会有编译时错误,但运行时行为可能会静默改变(因为它现在正在执行不同的函数)。

缓解策略

  • 一些项目可能认为这种破坏是可以接受的,特别是如果新项名称不太可能与任何现有代码冲突时。请仔细选择名称以帮助避免这些冲突。此外,要求下游用户在更新依赖项时添加 消歧语法 来选择正确的函数,这可能是可以接受的。

主要:收紧泛型约束

收紧类型的泛型约束是破坏性变更,因为这可能会破坏期望使用较宽松约束的用户。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub struct Foo<A> {
    pub f1: A,
}

///////////////////////////////////////////////////////////
// After
pub struct Foo<A: Eq> {
    pub f1: A,
}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::Foo;

fn main() {
    let s = Foo { f1: 1.23 }; // Error: the trait bound `{float}: Eq` is not satisfied
}

次要:放宽泛型约束

放宽类型的泛型约束是安全的,因为它只会扩大允许的范围。

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub struct Foo<A: Clone> {
    pub f1: A,
}

///////////////////////////////////////////////////////////
// After
pub struct Foo<A> {
    pub f1: A,
}

///////////////////////////////////////////////////////////
// Example use of the library that will safely work.
use updated_crate::Foo;

fn main() {
    let s = Foo { f1: 123 };
}

次要:添加带有默认值的类型参数

只要类型参数带有默认值,添加它是安全的。所有现有引用将使用默认值,而无需指定参数。

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
#[derive(Default)]
pub struct Foo {}

///////////////////////////////////////////////////////////
// After
#[derive(Default)]
pub struct Foo<A = i32> {
    f1: A,
}

///////////////////////////////////////////////////////////
// Example use of the library that will safely work.
use updated_crate::Foo;

fn main() {
    let s: Foo = Default::default();
}

次要:将类型泛化以使用泛型(使用相同类型)

结构体或枚举字段可以从具体类型更改为泛型类型参数,前提是对于所有现有用例,更改后得到的类型是相同的。例如,允许以下更改:

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub struct Foo(pub u8);

///////////////////////////////////////////////////////////
// After
pub struct Foo<T = u8>(pub T);

///////////////////////////////////////////////////////////
// Example use of the library that will safely work.
use updated_crate::Foo;

fn main() {
    let s: Foo = Foo(123);
}

因为 Foo 的现有用法是 Foo<u8> 的简写,这会产生相同的字段类型。

主要:将类型泛化以使用泛型(可能使用不同类型)

如果类型可以改变,将结构体或枚举字段从具体类型更改为泛型类型参数可能会导致破坏。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub struct Foo<T = u8>(pub T, pub u8);

///////////////////////////////////////////////////////////
// After
pub struct Foo<T = u8>(pub T, pub T);

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::Foo;

fn main() {
    let s: Foo<f32> = Foo(3.14, 123); // Error: mismatched types
}

次要:将泛型类型更改为更泛型的类型

将泛型类型更改为更泛型的类型是安全的。例如,以下示例添加了一个默认为原始类型的泛型参数,这是安全的,因为所有现有用户将为两个字段使用相同的类型,无需指定带有默认值的参数。

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub struct Foo<T>(pub T, pub T);

///////////////////////////////////////////////////////////
// After
pub struct Foo<T, U = T>(pub T, pub U);

///////////////////////////////////////////////////////////
// Example use of the library that will safely work.
use updated_crate::Foo;

fn main() {
    let s: Foo<f32> = Foo(1.0, 2.0);
}

主要:在 RPIT 中捕获更多泛型参数

RPIT(返回位置 impl Trait)中捕获额外的泛型参数是破坏性变更。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub fn f<'a, 'b>(x: &'a str, y: &'b str) -> impl Iterator<Item = char> + use<'a> {
    x.chars()
}

///////////////////////////////////////////////////////////
// After
pub fn f<'a, 'b>(x: &'a str, y: &'b str) -> impl Iterator<Item = char> + use<'a, 'b> {
    x.chars().chain(y.chars())
}

///////////////////////////////////////////////////////////
// Example usage that will break.
fn main() {
    let a = String::new();
    let b = String::new();
    let iter = updated_crate::f(&a, &b);
    drop(b); // Error: cannot move out of `b` because it is borrowed
}

向 RPIT 添加泛型参数会对其结果类型的使用方式施加额外的约束。

请注意,未指定 use<> 语法时存在隐式捕获。在 Rust 2021 及更早版本中,生命周期参数仅在其语法上出现在 RPIT 类型签名的约束中时才会被捕获。从 Rust 2024 开始,所有生命周期参数都会无条件捕获。这意味着从 Rust 2024 开始,默认情况下是最大限度兼容的,当你想要捕获更少时需要显式说明,这是一项 SemVer 承诺。

有关 RPIT 捕获的更多信息,请参阅 版本指南参考

在 RPIT 中捕获更少泛型参数是次要变更。

注意:所有范围内的类型和 const 泛型参数必须要么隐式捕获(未指定 + use<…>),要么显式捕获(必须列在 + use<…> 中),因此目前不允许更改这些类型的泛型的捕获内容。

主要:添加/移除函数参数

改变函数的参数数量是破坏性变更。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub fn foo() {}

///////////////////////////////////////////////////////////
// After
pub fn foo(x: i32) {}

///////////////////////////////////////////////////////////
// Example usage that will break.
fn main() {
    updated_crate::foo(); // Error: this function takes 1 argument
}

缓解策略

  • 引入具有新签名的新函数,并可能 废弃 旧函数。
  • 引入接受结构体参数的函数,其中结构体使用构建器模式构建。这允许将来向结构体添加新字段。

可能破坏性:引入新的函数类型参数

通常,添加非默认类型参数是安全的,但在某些情况下它可能是破坏性变更

// Breaking change example

///////////////////////////////////////////////////////////
// Before
pub fn foo<T>() {}

///////////////////////////////////////////////////////////
// After
pub fn foo<T, U>() {}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::foo;

fn main() {
    foo::<u8>(); // Error: function takes 2 generic arguments but 1 generic argument was supplied
}

然而,这种显式调用足够罕见(并且通常可以用其他方式编写),因此这种破坏通常是可以接受的。应该考虑相关函数被显式类型参数调用的可能性有多大。

次要:将函数泛化以使用泛型(支持原始类型)

函数参数或其返回值的类型可以被泛化以使用泛型,包括通过引入新的类型参数,只要它可以被实例化为原始类型。例如,以下更改是允许的:

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub fn foo(x: u8) -> u8 {
    x
}
pub fn bar<T: Iterator<Item = u8>>(t: T) {}

///////////////////////////////////////////////////////////
// After
use std::ops::Add;
pub fn foo<T: Add>(x: T) -> T {
    x
}
pub fn bar<T: IntoIterator<Item = u8>>(t: T) {}

///////////////////////////////////////////////////////////
// Example use of the library that will safely work.
use updated_crate::{bar, foo};

fn main() {
    foo(1);
    bar(vec![1, 2, 3].into_iter());
}

因为所有现有用法都是新签名的实例化。

也许有点令人惊讶的是,泛化也适用于 trait 对象,因为每个 trait 都实现了它自己

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub trait Trait {}
pub fn foo(t: &dyn Trait) {}

///////////////////////////////////////////////////////////
// After
pub trait Trait {}
pub fn foo<T: Trait + ?Sized>(t: &T) {}

///////////////////////////////////////////////////////////
// Example use of the library that will safely work.
use updated_crate::{foo, Trait};

struct Foo;
impl Trait for Foo {}

fn main() {
    let obj = Foo;
    foo(&obj);
}

(使用 ?Sized 是必不可少的;否则您将无法恢复原始签名。)

以这种方式引入泛型可能会导致类型推断失败。这些通常很少见,对于某些项目来说可能是可以接受的破坏,因为这可以通过添加额外的类型注解来修复。

// Breaking change example

///////////////////////////////////////////////////////////
// Before
pub fn foo() -> i32 {
    0
}

///////////////////////////////////////////////////////////
// After
pub fn foo<T: Default>() -> T {
    Default::default()
}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::foo;

fn main() {
    let x = foo(); // Error: type annotations needed
}

主要:将函数泛化以使用具有类型不匹配的泛型

如果泛型类型约束或改变了之前允许的类型,则更改函数参数或返回类型是破坏性变更。例如,以下示例添加了一个现有代码可能无法满足的泛型约束:

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub fn foo(x: Vec<u8>) {}

///////////////////////////////////////////////////////////
// After
pub fn foo<T: Copy + IntoIterator<Item = u8>>(x: T) {}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::foo;

fn main() {
    foo(vec![1, 2, 3]); // Error: `Copy` is not implemented for `Vec<u8>`
}

次要:使 unsafe 函数安全

以前的 unsafe 函数可以变得安全而不会破坏代码。

但请注意,这可能会像下面的示例一样触发 unused_unsafe lint,这将导致本地 crate 中指定了 #![deny(warnings)] 的无法编译。根据 引入新的 lint 警告,更新中允许引入新的警告。

反过来(使安全函数变为 unsafe)是破坏性变更。

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub unsafe fn foo() {}

///////////////////////////////////////////////////////////
// After
pub fn foo() {}

///////////////////////////////////////////////////////////
// Example use of the library that will trigger a lint.
use updated_crate::foo;

unsafe fn bar(f: unsafe fn()) {
    f()
}

fn main() {
    unsafe { foo() }; // The `unused_unsafe` lint will trigger here
    unsafe { bar(foo) };
}

将以前结构体/枚举上的 unsafe 关联函数或方法变为安全也是次要变更,而 trait 上的关联函数则不是(参见 对 trait 项签名的任何更改)。

主要:从支持 no_std 切换到需要 std

如果你的库专门支持 no_std 环境,发布需要 std 的新版本是破坏性变更。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
#![no_std]
pub fn foo() {}

///////////////////////////////////////////////////////////
// After
pub fn foo() {
    std::time::SystemTime::now();
}

///////////////////////////////////////////////////////////
// Example usage that will break.
// This will fail to link for no_std targets because they don't have a `std` crate.
#![no_std]
use updated_crate::foo;

fn example() {
    foo();
}

缓解策略

  • 避免这种情况的一种常见做法是包含一个 std Cargo feature,该 feature 可选地启用 std 支持,当该 feature 关闭时,库可以在 no_std 环境中使用。

主要:向现有没有私有字段的枚举、变体或结构体添加 non_exhaustive

将项设置为 #[non_exhaustive] 改变了它们在定义它们的 crate 外部的使用方式:

  • 非穷尽结构体和枚举变体不能使用 结构体字面量 语法构造,包括 功能更新语法
  • 对非穷尽结构体进行模式匹配需要使用 ..,并且对枚举的匹配不计入穷尽性检查。
  • 不允许使用 as 将枚举变体转换为其判别式。

无论是否使用 #[non_exhaustive],带有私有字段的结构体都不能使用 结构体字面量 语法构造。向这样的结构体添加 #[non_exhaustive] 不是破坏性变更。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub struct Foo {
    pub bar: usize,
}

pub enum Bar {
    X,
    Y(usize),
    Z { a: usize },
}

pub enum Quux {
    Var,
}

///////////////////////////////////////////////////////////
// After
#[non_exhaustive]
pub struct Foo {
    pub bar: usize,
}

pub enum Bar {
    #[non_exhaustive]
    X,

    #[non_exhaustive]
    Y(usize),

    #[non_exhaustive]
    Z { a: usize },
}

#[non_exhaustive]
pub enum Quux {
    Var,
}

///////////////////////////////////////////////////////////
// Example usage that will break.
use updated_crate::{Bar, Foo, Quux};

fn main() {
    let foo = Foo { bar: 0 }; // Error: cannot create non-exhaustive struct using struct expression

    let bar_x = Bar::X; // Error: unit variant `X` is private
    let bar_y = Bar::Y(0); // Error: tuple variant `Y` is private
    let bar_z = Bar::Z { a: 0 }; // Error: cannot create non-exhaustive variant using struct expression

    let q = Quux::Var;
    match q {
        Quux::Var => 0,
        // Error: non-exhaustive patterns: `_` not covered
    };
}

缓解策略

工具链和环境兼容性

可能破坏性:更改所需的最低 Rust 版本

在新版本的 Rust 中引入新功能的使用可能会破坏使用旧版本 Rust 的项目。这还包括在新版本的 Cargo 中使用新功能,以及要求使用以前在稳定版中可以工作的 crate 中的仅限 nightly 版本的功能。

出于各种原因,通常建议将其视为次要变更,而不是主要变更。更新到较新版本的 Rust 通常相对容易。Rust 也有快速的 6 周发布周期,一些项目会在一段时间的发布版本中提供兼容性(例如当前稳定版加上 N 个先前版本)。请记住,一些大型项目可能无法快速更新其 Rust 工具链。

缓解策略

  • 使用 Cargo features 使新功能成为可选启用。
  • 为旧版本提供较长的支持窗口。
  • 如果可能,复制新的标准库项的源代码,以便您可以继续使用旧版本,同时利用新功能。
  • 提供一个包含旧次要版本的独立分支,可以接收重要错误修复的反向移植。
  • 关注 [cfg(version(..))]#[cfg(accessible(..))] 功能,它们为新功能提供了可选启用机制。这些功能目前不稳定,仅在 nightly 通道中可用。

可能破坏性:更改平台和环境要求

库对其运行环境(例如主机平台、操作系统版本、可用服务、文件系统支持等)做出了广泛的假设。如果你发布的新版本限制了以前支持的功能,例如要求更高版本的操作系统,这可能是一个破坏性变更。这些变更可能难以跟踪,因为你可能无法确定在未自动测试的环境中某个变更是否会导致中断。

一些项目可能认为这种破坏是可以接受的,特别是如果对大多数用户来说不太可能发生,或者项目没有资源支持所有环境。另一个值得注意的情况是,当供应商停止支持某些硬件或操作系统时,项目也可能认为停止支持是合理的。

缓解策略

  • 明确记录你专门支持的平台和环境。
  • 在 CI 中在各种环境中测试你的代码。

次要:引入新的 lint 警告

库的某些更改可能会在使用该库的用户中触发新的 lint 警告。这通常应被视为兼容性变更。

// MINOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub fn foo() {}

///////////////////////////////////////////////////////////
// After
#[deprecated]
pub fn foo() {}

///////////////////////////////////////////////////////////
// Example use of the library that will safely work.

fn main() {
    updated_crate::foo(); // Warning: use of deprecated function
}

请注意,如果项目明确拒绝了警告,并且更新的 crate 是直接依赖项,这在技术上可能会导致项目失败。拒绝警告应谨慎进行,并理解随着时间的推移可能会引入新的 lint 警告。然而,库作者应谨慎引入新的警告,并可能需要考虑对其用户的潜在影响。

以下是更新依赖项时可能引入的一些 lint 警告示例:

此外,将 rustc 更新到新版本可能会引入新的 lint 警告。

引入新 lint 警告的传递依赖通常不应导致失败,因为 Cargo 使用 --cap-lints 来抑制所有依赖项中的 lint 警告。

缓解策略

  • 如果你构建时拒绝警告,请理解每次更新依赖项时可能需要处理新的警告。如果使用 RUSTFLAGS 传递 -Dwarnings,同时添加 -A 标志以允许可能导致问题的 lint 警告,例如 -Adeprecated
  • feature 后面引入废弃。例如 #[cfg_attr(feature = "deprecated", deprecated="use bar instead")]。然后,当你计划在未来的 SemVer 破坏性变更中移除某个项时,你可以告知用户他们应该在更新之前启用 deprecated feature 来移除已废弃项的使用。这使得用户可以选择何时响应废弃,而无需立即响应。一个缺点是,可能很难告知用户需要执行这些手动步骤来准备进行主要更新。

Cargo

次要:添加新的 Cargo feature

添加新的 Cargo features 通常是安全的。如果 feature 引入了导致破坏性变更的新更改,这可能会给对向后兼容性要求更严格的项目带来困难。在这种情况下,避免将该 feature 添加到“default”列表中,并可能需要文档说明启用该 feature 的后果。

# MINOR CHANGE

###########################################################
# Before
[features]
# ..empty

###########################################################
# After
[features]
std = []

主要:移除 Cargo feature

移除 Cargo features 通常是破坏性变更。这将导致启用该 feature 的任何项目出错。

# MAJOR CHANGE

###########################################################
# Before
[features]
logging = []

###########################################################
# After
[dependencies]
# ..logging removed

缓解策略

  • 清楚地记录你的 features。如果存在内部或实验性 feature,请将其标记出来,以便用户了解其状态。
  • 将旧 feature 保留在 Cargo.toml 中,但移除其功能。文档说明该 feature 已废弃,并在将来的主要 SemVer 版本中移除它。

主要:如果从 feature 列表中移除一个 feature 会改变功能或公共项

如果从另一个 feature 中移除一个 feature,这可能会破坏现有用户,因为他们可能期望该功能通过该 feature 可用。

# Breaking change example

###########################################################
# Before
[features]
default = ["std"]
std = []

###########################################################
# After
[features]
default = []  # This may cause packages to fail if they are expecting std to be enabled.
std = []

可能破坏性:移除可选依赖

移除 可选依赖项 可能会破坏使用你的库的项目,因为另一个项目可能通过 Cargo features 启用该依赖项。

当存在可选依赖项时,cargo 会隐式定义一个同名的 feature,提供一种机制来启用该依赖项并检查它何时启用。可以通过在 [features] 表中使用 dep: 语法来避免这个问题,这会禁用此隐式 feature。使用 dep: 可以将可选依赖项隐藏在语义上更相关的名称下,这些名称可以更安全地修改。

# Breaking change example

###########################################################
# Before
[dependencies]
curl = { version = "0.4.31", optional = true }

###########################################################
# After
[dependencies]
# ..curl removed
# MINOR CHANGE
#
# This example shows how to avoid breaking changes with optional dependencies.

###########################################################
# Before
[dependencies]
curl = { version = "0.4.31", optional = true }

[features]
networking = ["dep:curl"]

###########################################################
# After
[dependencies]
# Here, one optional dependency was replaced with another.
hyper = { version = "0.14.27", optional = true }

[features]
networking = ["dep:hyper"]

缓解策略

  • [features] 表中使用 dep: 语法,从一开始就避免暴露可选依赖项。有关更多信息,请参见 可选依赖项
  • 清楚地记录你的 features。如果可选依赖项未包含在已文档化的 feature 列表中,那么你可以认为更改未文档化的条目是安全的。
  • 保留可选依赖项,但不要在你的库中使用它。
  • 用一个什么都不做的 Cargo feature 替换可选依赖项,并文档说明它已废弃。
  • 使用启用可选依赖项的高级 features,并将这些 features 文档化为启用扩展功能的首选方式。例如,如果你的库可选支持“网络”之类的功能,创建一个通用 feature 名称“networking”,该名称启用实现“网络”所需的可选依赖项。然后文档说明“networking” feature。

次要:更改依赖项 features

更改依赖项上的 features 通常是安全的,只要该 feature 不引入破坏性变更。

# MINOR CHANGE

###########################################################
# Before
[dependencies]
rand = { version = "0.7.3", features = ["small_rng"] }


###########################################################
# After
[dependencies]
rand = "0.7.3"

次要:添加依赖项

添加新依赖项通常是安全的,只要新依赖项不引入导致破坏性变更的新要求。例如,在一个以前在稳定版中工作的项目中添加一个需要 nightly 版本的新依赖项是主要变更。

# MINOR CHANGE

###########################################################
# Before
[dependencies]
# ..empty

###########################################################
# After
[dependencies]
log = "0.4.11"

应用程序兼容性

Cargo 项目也可能包含具有自己的接口(例如 CLI 接口、操作系统级交互等)的可执行二进制文件。由于这些是 Cargo 包的一部分,它们通常使用并共享与包相同的版本。你需要决定是否以及如何在你对应用程序所做的更改中与用户约定 SemVer 契约。应用程序潜在的破坏性变更和兼容性变更数量太多,无法一一列出,因此鼓励你借鉴 SemVer 规范的精神来指导你如何对应用程序应用版本控制的决策,或者至少文档说明你的承诺是什么。