入门 The Cargo Team 2026-09-13 15:48:22 · 0 阅读

第26章 SemVer 兼容性指南

本章详细说明了对新发布的软件包而言,哪些变更通常被视为兼容的 SemVer 变更,哪些被视为破坏性变更。有关 SemVer 的定义以及 Cargo 如何利用它来确保库的兼容性,请参阅 SemVer 兼容性 章节。

以下内容仅为指导方针,并非所有项目都必须遵守的硬性规则。变更分类章节详细说明了本指南如何对变更的级别和严重程度进行分类。本指南主要关注那些会导致 cargorustc 构建失败、使原本正常的功能无法运行的变更。几乎每次变更都存在对运行时行为产生负面影响的潜在风险,对于这类情况,通常由项目维护者自行判断其是否属于不兼容的 SemVer 变更。

变更分类

下列所有策略均按变更级别进行分类:

  • 主要变更:需要提升 SemVer 主版本号。
  • 次要变更:仅需提升 SemVer 次版本号。
  • 可能破坏兼容性的变更:某些项目可能将其视为主要变更,而另一些项目则视为次要变更。

“可能破坏兼容性”类别涵盖了那些在更新时有可能导致破坏、但不一定实际发生破坏的变更。应仔细评估这些变更的影响。其具体性质取决于变更本身以及项目维护者的原则。

一些项目可能选择在次要变更时仅提升修订号。建议遵循 SemVer 规范,仅在修订版本中应用错误修复。然而,错误修复可能需要更改 API,而这通常被标记为“次要变更”,且不应影响兼容性。本指南不对各个“次要变更”应如何处理采取统一立场,因为次要变更与修订变更之间的区别取决于变更性质,属于约定俗成的范畴。

尽管某些变更被标记为“次要”,但它们确实存在导致构建失败的潜在风险。这种情况适用于潜在风险极低、且可能出现破坏的代码在惯用 Rust 中不太可能被编写出来、或明确不鼓励使用的情形。

本指南中的「major」和「minor」术语,默认基于「1.0.0」或更高版本的发布场景。对于以「0.y.z」开头的早期开发版本,可将「y」的变化视为 major 版本变更,「z」的变化视为 minor 版本变更。「0.0.z」的发布则始终属于 major 级别变更。这是因为 Cargo 遵循如下约定:只有最左侧非零组件的变化才被视为不兼容。

API 兼容性

以下所有示例均包含三个部分:原始代码、修改后的代码,以及在其他项目中可能出现的代码使用示例。对于次要版本变更,使用示例应当能在新旧两个版本中成功编译。

主要变更:重命名、移动或移除任何公共项

若缺少原本公开暴露的 ,任何对该项的使用都将导致编译失败。

// 主要变更

///////////////////////////////////////////////////////////
// 修改前
pub fn foo() {}

///////////////////////////////////////////////////////////
// 修改后
// ... 该项已被移除

///////////////////////////////////////////////////////////
// 使用示例:此处的代码将因此失效。
fn main() {
    updated_crate::foo(); // 错误:找不到函数 `foo`
}

这还包括添加任何形式的 cfg 属性,因为基于条件编译,这些属性会改变可用项或行为。

缓解策略:

  • 将计划移除的项标记为 已弃用,并在后续的 SemVer 破坏性版本中正式移除。
  • 将重命名的项标记为 已弃用,并通过 pub use 项重新导出至旧名称。

次要变更:新增公共项

添加新的公共属于次要变更。

// 次要变更

///////////////////////////////////////////////////////////
// 修改前
// ... 该项原本不存在

///////////////////////////////////////////////////////////
// 修改后
pub fn foo() {}

///////////////////////////////////////////////////////////
// 使用示例:可安全运行。
// 由于 `foo` 先前不存在,因此未被使用。

需要注意的是,在少数情况下,由于通配符导入(glob imports)的影响,这可能构成 破坏性变更。例如,如果你添加了一个新的 trait,而某个项目使用了通配符导入将该 trait 引入作用域,且新 trait 引入了一个与实现它的任何类型相冲突的关联项,这就可能因歧义而引发编译时错误。示例如下:

// 破坏性变更示例

///////////////////////////////////////////////////////////
// 变更前
// ... 缺少该 trait

///////////////////////////////////////////////////////////
// 变更后
pub trait NewTrait {
    fn foo(&self) {}
}

impl NewTrait for i32 {}

///////////////////////////////////////////////////////////
// 会导致问题的用法示例。
use updated_crate::*;

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

impl LocalTrait for i32 {}

fn main() {
    123i32.foo(); // 错误: 多个适用项在作用域内
}

这不被视为主版本变更,因为根据惯例,通配符导入被视为已知的向前兼容风险。应避免从外部 crates 导入项目。

主版本:更改良定义类型的对齐、布局或大小

更改先前良定义类型的对齐、布局或大小属于破坏性变更。

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

注意:即使类型的布局不是良定义的,外部 crates 若对其对齐、布局或大小做出假设,也可能被破坏。由于不应做出这些假设,因此这不视为 SemVer 破坏性变更。

以下是不会构成破坏性变更的一些示例(假设未违反本指南中的其他规则):

使用了 repr 属性的类型,其对齐方式和内存布局会以某种方式确定下来,代码可能会对此做出一些假设,而修改该类型可能导致这些假设失效。

在某些情况下,带有 repr 属性的类型可能没有明确定义的内存对齐、布局或大小。 在这种情况下,修改这些类型可能是安全的,但仍需谨慎操作。 例如,如果某类型的私有字段未在文档中明确说明其对齐、布局或大小的保证,则外部 crate 不能依赖该保证,因为其公共 API 并未完整定义该类型的对齐、布局或大小。

一个常见的例子是:具有私有字段的类型在某些情况下定义是明确的,即该类型只有一个带泛型类型的私有字段,使用了 repr(transparent),且文档正文说明了其对泛型类型是透明的。 示例参见 UnsafeCell

以下是一些破坏性变更的示例:

小版本:为 repr(C) 类型添加、移除或更改私有字段

如果遵循本指南中的其他规范(参见 struct-add-private-field-when-publicstruct-add-public-field-when-no-privatestruct-private-fields-with-privateenum-fields-new),通常可以安全地向 repr(C) 结构体、联合体或枚举添加、删除或修改私有字段。

例如,仅当结构体已包含其他私有字段,或标记为 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) 枚举添加变体

如果枚举使用了 non_exhaustive,通常可以安全地向 repr(C) 枚举添加变体。更多讨论见 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 }, // 新增
}

///////////////////////////////////////////////////////////
// 库的安全使用示例。
fn main() {
    // 注意:用户不应假设其大小或对齐方式,
    // 因为这些并没有明确规定。例如,这次改动就让大小从 8 字节增加到了 16 字节。
    let f = updated_crate::Example::Variant2 { f1: 123 };
}

Minor:给默认表示的类型添加 repr(C)

给使用默认表示的 struct、union 或 enum 添加 repr(C) 是安全的。 原因在于,使用默认表示的类型,其大小、布局和对齐方式本来就不应被用户依赖。

// MINOR CHANGE

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

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

///////////////////////////////////////////////////////////
// 库的安全使用示例。
fn main() {
    let f = updated_crate::Example { f1: 123, f2: 456 };
}

Minor:给 enum 添加 repr(<int>)

给使用默认表示的 enum 添加 repr(<int>) 这种基本整数表示是安全的。 原因在于,使用默认表示的 enum,其大小、布局和对齐方式本来就不应被用户依赖。

// 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 };
}

Minor: Adding repr(transparent) to a default representation struct or enum

It is safe to add repr(transparent) to a struct or enum with the default representation. This is safe because users should not make assumptions about the alignment, layout, or size of a struct or enum with the default representation.

// MINOR CHANGE

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

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

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

Major: Adding repr(packed) to a struct or union

It is a breaking change to add repr(packed) to a struct or union. Making a type repr(packed) makes changes that can break code, such as being invalid to take a reference to a field, or causing truncation of disjoint closure captures.

// 重大变更

///////////////////////////////////////////////////////////
// 变更前
pub struct Example {
    pub f1: u8,
    pub f2: u16,
}

///////////////////////////////////////////////////////////
// 变更后
#[repr(packed)] // 新增
pub struct Example {
    pub f1: u8,
    pub f2: u16,
}

///////////////////////////////////////////////////////////
// 会导致错误的用法示例。
fn main() {
    let f = updated_crate::Example { f1: 1, f2: 2 };
    let x = &f.f2; // 错误: error[E0793]: 引用 packed 结构体字段未对齐
}
// 重大变更

///////////////////////////////////////////////////////////
// 变更前
pub struct Example(pub i32, pub i32);

///////////////////////////////////////////////////////////
// 变更后
#[repr(packed)]
pub struct Example(pub i32, pub i32);

///////////////////////////////////////////////////////////
// 会导致错误的用法示例。
fn main() {
    let mut f = updated_crate::Example(123, 456);
    let c = || {
        // 若未使用 repr(packed),闭包将精确捕获 `&f.0`。
        // 若使用了 repr(packed),为避免未定义行为,闭包将捕获 `&f`。
        let a = f.0;
    };
    f.1 = 789; // 错误: 无法为 `f.1` 赋值,因为它已被借用
    c();
}

重大变更:为结构体、联合体或枚举添加 repr(align)

为结构体、联合体或枚举添加 repr(align) 属于破坏性变更。 使某个类型变成 repr(align) 会破坏在 repr(packed) 类型中使用该类型的场景,因为这种组合不被允许。

// MAJOR CHANGE(重大变更)

///////////////////////////////////////////////////////////
// Before(变更前)
pub struct Aligned {
    pub a: i32,
}

///////////////////////////////////////////////////////////
// After(变更后)
#[repr(align(8))] // 新增
pub struct Aligned {
    pub a: i32,
}

///////////////////////////////////////////////////////////
// Example usage that will break.(会因此出问题的用法示例)
use updated_crate::Aligned;

#[repr(packed)]
pub struct Packed { // 错误:packed 类型不能间接包含带 `#[repr(align)]` 的类型
    f1: Aligned,
}

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

重大变更:从 struct 或 union 上移除 repr(packed)

从 struct 或 union 上移除 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)] // 移除了 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 };
    // 对该类型大小的某种假设。
    // 没有 `packed` 后这段代码会失败,因为大小变成了 4。
    const _: () = assert!(std::mem::size_of::<Packed>() == 3); // 错误:assertion failed
}
// 重大变更

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

///////////////////////////////////////////////////////////
// 变更后
#[repr(C)] // 移除了 packed
pub struct Packed {
    pub a: *mut i32,
    pub b: i32,
}
unsafe impl Send for Packed {}

///////////////////////////////////////////////////////////
// 会因此破坏的用法示例。
use updated_crate::Packed;

fn main() {
    let mut x = 123;

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

    // 当结构体为 packed 时,闭包捕获的 `p` 是 Send。
    // 移除 `packed` 后,代码实际上捕获了 `p.a`,而它不是 Send。
    std::thread::spawn(move || unsafe {
        *(p.a) += 1; // 错误:无法安全地在线程间发送
    });
}

重大变更:更改 repr(packed(N)) 中 N 的值且改变了内存对齐或布局

如果更改 repr(packed(N)) 中 N 的值导致内存对齐或布局发生变化,则属于破坏性变更。 这可能会影响外部 crate 所依赖的对齐方式或布局结构。

如果 N 的值降低到低于某个公共字段的对齐要求,任何尝试对该字段取引用的代码都将无法正常工作。

需要注意的是,某些对 N 的修改可能不会改变对齐或布局,例如当当前值已经等于该类型的自然对齐值时再增大它。

// 重大变更

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

///////////////////////////////////////////////////////////
// 变更后
#[repr(packed(2))] // 改为 2
pub struct Packed {
    pub a: u8,
    pub b: u32,
}

///////////////////////////////////////////////////////////
// 会因此破坏的用法示例。
use updated_crate::Packed;

fn main() {
    let p = Packed { a: 1, b: 2 };
    let x = &p.b; // 错误:error[E0793]: 对 packed 结构体字段的引用未对齐
}

重大变更:更改 repr(align(N)) 中 N 的值且改变了内存对齐

如果改变 repr(align(N))N 的值会改变对齐方式,这属于破坏性变更。这可能会影响外部 crate 依赖的对齐。

如果该类型定义不明确,如类型布局章节所讨论的那样(例如包含私有字段,或者未记录对齐方式和布局),那么此变更通常是安全的。

需要注意的是,某些对 N 的修改可能不会改变对齐或布局。例如,当当前值已经小于或等于类型的自然对齐值时,减小 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: assertion failed
}

重大变更:从结构体、联合体或枚举中移除 repr(align)

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

如果该类型定义不明确,如类型布局章节所讨论的那样(例如包含私有字段,或者未记录对齐方式),那么此变更通常是安全的。

// 重大变更(MAJOR CHANGE)

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

///////////////////////////////////////////////////////////
// 变更后
#[repr(C)] // 移除了 align
pub struct Packed {
    pub a: u8,
    pub b: u32,
}

///////////////////////////////////////////////////////////
// 会因此出问题的示例代码。
use updated_crate::Packed;

fn main() {
    let p = Packed { a: 1, b: 2 };
    // 对类型大小的某种假设。
    // 对齐方式已从 8 变为 4。
    const _: () = assert!(std::mem::align_of::<Packed>() == 8); // 错误:断言失败
}

重大变更:改变 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, // 字段顺序改变
    pub a: u8,
}

///////////////////////////////////////////////////////////
// 会导致兼容性问题 的用法示例。
use updated_crate::SpecificLayout;

unsafe extern "C" {
    // 这个 C 函数假定特定的布局,该布局定义在 C 头文件中。
    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) } // 错误:值不等于 2
}

mod cdep {
    // 模拟通常由构建脚本包含的内容。
    // 这个定义本应位于 C 头文件中。
    #[repr(C)]
    pub struct SpecificLayout {
        pub a: u8,
        pub b: u32,
    }

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

Major: 移除 struct、union 或 enum 上的 repr(C)

从 struct、union 或 enum 上移除 repr(C) 是一个破坏性变更,因为外部 crate 可能依赖该类型的特定内存布局。

// 主版本变更 (MAJOR CHANGE)

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

///////////////////////////////////////////////////////////
// 变更后
// 移除了 repr(C)
pub struct SpecificLayout {
    pub a: u8,
    pub b: u32,
}

///////////////////////////////////////////////////////////
// 示例:以下用法将会失效。
use updated_crate::SpecificLayout;

unsafe extern "C" {
    // 此 C 函数假设布局由 C 头文件中的定义确定。
    fn c_fn_get_b(x: &SpecificLayout) -> u32; // 错误:不具备 FFI 安全性
}

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

mod cdep {
    // 模拟通常由构建脚本引入的内容。
    此定义本应位于 C 头文件中。
    #[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 可能会失败。

// 重大变更

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

///////////////////////////////////////////////////////////
// 变更后
// 移除了 repr(u16)
pub enum Example {
    Variant1,
    Variant2,
    Variant3,
}

///////////////////////////////////////////////////////////
// 会破坏的用法示例。

fn main() {
    let e = updated_crate::Example::Variant2;
    let i: u16 = unsafe { std::mem::transmute(e) }; // 错误:不能在不同大小的类型之间进行 transmute
}

Major: 修改 repr(<int>) 枚举的底层表示类型

修改 repr(<int>) 枚举的底层表示类型是破坏性变更。 外部 crate 可能假设判别值是特定大小的,例如对枚举使用 std::mem::transmute 时可能失败。

// 重大变更

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

///////////////////////////////////////////////////////////
// 变更后
#[repr(u8)] // 修改了 repr 大小
pub enum Example {
    Variant1,
    Variant2,
    Variant3,
}

///////////////////////////////////////////////////////////
// 会破坏的用法示例。

fn main() {
    let e = updated_crate::Example::Variant2;
    let i: u16 = unsafe { std::mem::transmute(e) }; // 错误:不能在不同大小的类型之间进行 transmute
}

Major: 从 struct 或 enum 上移除 repr(transparent)

从 struct 或 enum 上移除 repr(transparent) 是破坏性变更。 外部 crate 可能依赖该类型拥有与透明字段相同的对齐方式、内存布局或大小。

// 重大变更

///////////////////////////////////////////////////////////
// 变更前
#[repr(transparent)]
pub struct Transparent<T>(T);

///////////////////////////////////////////////////////////
// 变更后
// 移除了 repr
pub struct Transparent<T>(T);

///////////////////////////////////////////////////////////
// 会导致破坏的用法示例。
#![deny(improper_ctypes)]
use updated_crate::Transparent;

unsafe extern "C" {
    fn c_fn() -> Transparent<f64>; // 错误:并非 FFI-safe
}

fn main() {}

重大变更:当所有当前字段均为公开时,添加私有结构体字段

如果在一个原本所有字段均为公开的结构体中新增私有字段,任何试图使用结构体字面量来构造它的代码都将因此中断。

// 重大变更

///////////////////////////////////////////////////////////
// 变更前
pub struct Foo {
    pub f1: i32,
}

///////////////////////////////////////////////////////////
// 变更后
pub struct Foo {
    pub f1: i32,
    f2: i32,
}

///////////////////////////////////////////////////////////
// 会导致破坏的用法示例。
fn main() {
    let x = updated_crate::Foo { f1: 123 }; // 错误:无法构造 `Foo`
}

缓解策略:

  • 不要在所有字段均为公开的结构体中添加新字段。
  • 在首次引入结构体时,将其标记为#[non_exhaustive],以防止用户直接使用结构体字面量语法,转而提供构造函数和/或Default实现。

重大变更:在无私有字段的情况下添加公开字段

如果在一个所有字段均为公开的结构体中新增公开字段,任何试图使用结构体字面量来构造它的代码都将因此中断。

// 重大变更(MAJOR)

///////////////////////////////////////////////////////////
// 变更前
pub struct Foo {
    pub f1: i32,
}

///////////////////////////////////////////////////////////
// 变更后
pub struct Foo {
    pub f1: i32,
    pub f2: i32,
}

///////////////////////////////////////////////////////////
// 会导致编译报错的用法示例
fn main() {
    let x = updated_crate::Foo { f1: 123 }; // 错误:缺少字段 `f2`
}

缓解策略:

  • 不要向全公有字段的结构体(all-public field structs)中添加新字段。
  • 在首次引入结构体时,将其标记为 #[non_exhaustive],以防止用户直接使用结构体字面量语法进行构造。改为提供构造函数和/或实现 Default 特性。

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

如果结构体中已经存在至少一个私有字段,那么添加或移除私有字段是安全的。

// 次要变更(MINOR)

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

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

///////////////////////////////////////////////////////////
// 示例:该库的用法可安全正常工作
fn main() {
    // 无法访问私有字段。
    let x = updated_crate::Foo::default();
}

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

请注意,对于元组结构体(tuple structs),如果元组包含公有字段,且添加或移除私有字段会导致任何公有字段的索引发生变化,则这属于 重大变更

// MAJOR CHANGE(破坏性变更)

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

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

///////////////////////////////////////////////////////////
// 会出错的用法示例
fn main() {
    let x = updated_crate::Foo::default();
    let y = x.0; // 错误:字段是私有的
}

Minor(非破坏性):全私有字段(至少有一个字段)的元组结构体改为普通结构体,或反之

只要所有字段都是私有的,把元组结构体改成普通结构体(或反过来)就是安全的。

// MINOR CHANGE(非破坏性变更)

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

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

///////////////////////////////////////////////////////////
// 可以安全运行的库用法示例
fn main() {
    // 无法访问私有字段。
    let x = updated_crate::Foo::default();
}

这样做是安全的,因为已有代码既无法用结构体字面量构造它,也无法对其内容做模式匹配。

Major(破坏性):新增 enum 变体(未使用 non_exhaustive

如果 enum 没有使用 #[non_exhaustive] 属性,那么新增变体就是破坏性变更。

// MAJOR CHANGE(破坏性变更)

///////////////////////////////////////////////////////////
// 之前
pub enum E {
    Variant1,
}

///////////////////////////////////////////////////////////
// 之后
pub enum E {
    Variant1,
    Variant2,
}

///////////////////////////////////////////////////////////
// 会出错的用法示例
fn main() {
    use updated_crate::E;
    let x = E::Variant1;
    match x { // 错误:未覆盖 `E::Variant2`
        E::Variant1 => {}
    }
}

规避方法:

主版本:向枚举变体添加新字段

向枚举变体添加新字段属于破坏性变更,因为所有字段都是公开的,这会导致构造函数和模式匹配无法编译。

// 主版本变更

///////////////////////////////////////////////////////////
// 变更前
pub enum E {
    Variant1 { f1: i32 },
}

///////////////////////////////////////////////////////////
// 变更后
pub enum E {
    Variant1 { f1: i32, f2: i32 },
}

///////////////////////////////////////////////////////////
// 将失效的示例用法。
fn main() {
    use updated_crate::E;
    let x = E::Variant1 { f1: 1 }; // 错误:缺少 f2
    match x {
        E::Variant1 { f1 } => {} // 错误:缺少 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 的实现者。

// 主版本变更

///////////////////////////////////////////////////////////
// 变更前
pub trait Trait {}

///////////////////////////////////////////////////////////
// 变更后
pub trait Trait {
    fn foo(&self);
}

///////////////////////////////////////////////////////////
// 将失效的示例用法。
use updated_crate::Trait;
struct Foo;

impl Trait for Foo {}  // 错误:并非所有 trait 项都已实现

缓解策略:

  • 为新添加的 trait 项始终提供默认实现或默认值。
  • 在引入该 trait 时,使用 sealed 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 时,使用 sealed trait 技术,防止 crate 外部用户实现该 trait。

潜在破坏性:添加带默认值的 trait 项

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

// 破坏性变更示例

///////////////////////////////////////////////////////////
// 变更前
pub trait Trait {}

///////////////////////////////////////////////////////////
// 变更后
pub trait Trait {
    fn foo(&self) {}
}

///////////////////////////////////////////////////////////
// 会出错的用法示例。
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

缓解策略:

  • 某些项目可能认为这种破坏可以接受,尤其是新条目的名称几乎不可能与现有代码冲突时。选择名称时要谨慎,尽量避免冲突。此外,更新依赖后要求下游用户添加消歧语法来选择正确的函数,通常也是可以接受的。

Major:添加导致 trait 不再 object safe 的条目

如果添加的 trait 条目会使 trait 不再满足 object safe,就属于破坏性变更。

// MAJOR CHANGE

///////////////////////////////////////////////////////////
// 变更前
pub trait Trait {}

///////////////////////////////////////////////////////////
// 变更后
pub trait Trait {
    // 关联常量会让 trait 不再 object safe。
    const CONST: i32 = 123;
}

///////////////////////////////////////////////////////////
// 会出错的用法示例。
use updated_crate::Trait;
struct Foo;

impl Trait for Foo {}

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

反过来操作(把不 object safe 的 trait 改成 object safe)则是安全的。

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

为 Trait(特征/特质)添加没有默认值的类型参数属于破坏性变更。

// 主要变更

///////////////////////////////////////////////////////////
// 修改前
pub trait Trait {}

///////////////////////////////////////////////////////////
// 修改后
pub trait Trait<T> {}

///////////////////////////////////////////////////////////
// 会导致破坏的示例用法。
use updated_crate::Trait;
struct Foo;

impl Trait for Foo {}  // 错误:缺少泛型

缓解策略:

次要变更:添加带默认值的 Trait 类型参数

只要类型参数有默认值,为 Trait 添加类型参数就是安全的。外部实现者使用默认值时无需显式指定该参数。

// 次要变更

///////////////////////////////////////////////////////////
// 修改前
pub trait Trait {}

///////////////////////////////////////////////////////////
// 修改后
pub trait Trait<T = i32> {}

///////////////////////////////////////////////////////////
// 库的安全使用示例。
use updated_crate::Trait;
struct Foo;

impl Trait for Foo {}

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

通常,向实现中添加固有项是安全的,因为固有项的优先级高于 Trait 项。然而在某些情况下,如果新添加的固有项名称与已实现的某个 Trait 项相同但签名不同,可能会引发冲突问题。

// 破坏性变更示例

///////////////////////////////////////////////////////////
// 修改前
pub struct Foo;

///////////////////////////////////////////////////////////
// 修改后
pub struct Foo;

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

///////////////////////////////////////////////////////////
// 会导致破坏的示例用法。
use updated_crate::Foo;

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

impl Trait for Foo {}

fn main() {
    let x = Foo;
    x.foo(1); // 错误:该方法接收 0 个参数,但提供了 1 个参数
}

注意,即使签名匹配,编译时也不会报错,但运行时行为可能会发生静默改变(因为现在执行的是另一个函数)。

缓解策略:

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

主要变更:收紧泛型约束

收紧类型的泛型约束属于破坏性变更,因为它会打破那些依赖较宽松约束的用户代码。

// 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)

///////////////////////////////////////////////////////////
// 之前
pub struct Foo<T>(pub T, pub T);

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

///////////////////////////////////////////////////////////
// 该库的使用示例,可以安全兼容。
use updated_crate::Foo;

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

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

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

// 主要变更 (MAJOR CHANGE)

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

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

///////////////////////////////////////////////////////////
// 该变更会导致以下使用示例出错。
fn main() {
    let a = String::new();
    let b = String::new();
    let iter = updated_crate::f(&a, &b);
    drop(b); // 错误:无法将 `b` 移出,因为它已被借用
}

向 RPIT 添加泛型参数会对其结果类型的用法施加更多约束。

需要注意,当未显式指定 use<> 语法时,存在隐式捕获。在 Rust 2021 及更早版本中,只有当生命周期参数在 RPIT 类型签名的某个边界(bound)中语法上出现时,才会被捕获。从 Rust 2024 开始,所有生命周期参数都会无条件地被捕获。这意味着在 Rust 2024 中,默认行为是完全兼容的;如果想减少捕获,必须显式声明,这属于 SemVer 承诺。

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

在 RPIT 中捕获更少的泛型参数属于次要变更。

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

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

更改函数参数个数属于破坏性变更。

// 主要变更

///////////////////////////////////////////////////////////
// 变更前
pub fn foo() {}

///////////////////////////////////////////////////////////
// 变更后
pub fn foo(x: i32) {}

///////////////////////////////////////////////////////////
// 将导致兼容性问题调用示例
fn main() {
    updated_crate::foo(); // 错误:此函数需要一个参数
}

缓解策略:

  • 引入具有新签名的函数,并考虑 弃用旧函数。
  • 引入接收结构体参数的函数,该结构体采用建造者模式构建。这允许将来向结构体添加新字段。

潜在破坏性变更:引入新的函数类型参数

通常,添加默认值不为空类型参数是安全的,但在某些情况下可能导致破坏性变更:

// 破坏性变更示例

///////////////////////////////////////////////////////////
// 变更前
pub fn foo<T>() {}

///////////////////////////////////////////////////////////
// 变更后
pub fn foo<T, U>() {}

///////////////////////////////////////////////////////////
// 将导致兼容性问题调用示例
use updated_crate::foo;

fn main() {
    foo::<u8>(); // 错误:函数需要 2 个泛型参数,但仅提供了 1 个
}

不过,这种显式调用很少见(而且通常有其他写法),所以这类破坏一般是可接受的。需要考虑的是目标函数被显式传入类型参数的可能性有多大。

Minor:将函数泛型化(仍支持原类型)

函数参数或返回值的类型可以泛型化,包括引入新的类型参数,只要新泛型能够实例化为原类型即可。例如以下改动是允许的:

// 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 是必不可少的,否则就无法还原出原来的签名。)

以这种方式引入泛型可能会导致类型推断失败。这种情况通常很少见,对某些项目来说也可以接受,因为只需补充类型标注即可修复。

// 破坏性变更示例

///////////////////////////////////////////////////////////
// 变更前
pub fn foo() -> i32 {
    0
}

///////////////////////////////////////////////////////////
// 变更后
pub fn foo() -> T {
    Default::default()
}

///////////////////////////////////////////////////////////
// 会报错的示例用法。
use updated_crate::foo;

fn main() {
    let x = foo(); // 错误:需要类型注释
}

Major:泛化函数使用泛型并引入类型不匹配

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

// MAJOR CHANGE(主要版本变更)

///////////////////////////////////////////////////////////
// 变更前
pub fn foo(x: Vec) {}

///////////////////////////////////////////////////////////
// 变更后
pub fn foo>(x: T) {}

///////////////////////////////////////////////////////////
// 会报错的示例用法。
use updated_crate::foo;

fn main() {
    foo(vec![1, 2, 3]); // 错误:`Vec` 没有实现 `Copy`
}

Minor:将 unsafe 函数改为 safe

将原本的 unsafe 函数变为 safe 函数,不会导致代码破坏。

但需注意,这可能会触发 unused_unsafe 警告(见下例)。若本地 crate 指定了 #![deny(warnings)],这将导致编译失败。根据 引入新 lint 的原则,版本更新可以引入新的警告。

反方向变更(将 safe 函数变为 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) };
}

将 struct / enum 上原本标记为 unsafe 的关联函数或方法改为 safe,同样属于次要版本变更;但 trait 上的关联函数并非如此(参见 trait 项签名的任何变更)。

Major: 从支持 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 支持的 std Cargo feature;当该 feature 关闭时,库即可在 no_std 环境中使用。

Major: 为没有私有字段的现有 enum、variant 或 struct 添加 non_exhaustive

将某些项标记为 #[non_exhaustive],会改变它们在定义所在 crate 外部被使用的方式:

  • 非穷尽(non-exhaustive)的 struct 和 enum 变体不能用 struct 字面量语法构造,包括 函数式更新语法
  • 对非穷尽的 struct 进行模式匹配时必须写 ..,匹配 enum 时也不计入穷尽性检查。
  • 不允许用 as 把 enum 变体转换为其判别值。

含有私有字段的 struct 本来就无法用 struct 字面量语法构造,与是否使用 #[non_exhaustive] 无关。因此给这类 struct 添加 #[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 新发布中使用新特性,或者要求在一个之前兼容 stable 的 crate 中使用仅 nightly 可用的特性。

由于多种原因,通常建议将此视为次要变更而非主要变更。 升级 Rust 到较新版本通常相对容易。Rust 拥有快速的 6 周发布周期,有些项目会在一定的发布窗口内提供兼容性(例如当前 stable 版本加上前 N 个版本)。但需注意,一些大型项目可能无法迅速更新其 Rust 工具链。

缓解策略:

  • 通过设置 package.rust-version 来记录包所支持的最低 Rust 版本,以便在必要时允许 Cargo 的依赖解析选择你包的旧版本。 这样做时务必考虑 支持期望
  • 使用 Cargo 特性使新特性变为可选(opt-in)。
  • 为旧版本提供较长的支持窗口。
  • 如果可能,复制新标准库项目的源码,这样你就能继续使用旧版本,同时利用新特性。
  • 提供一个独立的旧次要版本分支,用于接收重要 bug 修复的回移(backports)。
  • 留意 [cfg(version(..))]#[cfg(accessible(..))] 这两个特性,它们为启用新功能提供了可选机制。目前这些特性尚处于不稳定状态,仅在 nightly 频道可用。

可能破坏兼容性:变更平台与环境要求

库对运行环境会有很多假设,例如主机平台、操作系统版本、可用服务、文件系统支持等。如果新版本限制了之前支持的功能(例如要求更高版本的操作系统),就会造成破坏性变更。这类变更难以追踪,因为你并不总是知道某个环境中的变更是否破坏了兼容性,尤其是那些未自动测试的环境。

有些项目可能认为这种破坏是可以接受的,特别是当大多数用户不太可能受到影响,或者项目没有资源支持所有环境时。另一种显著情况是,当厂商停止支持某些硬件或操作系统时,项目也可能合理选择停止支持。

缓解策略:

  • 记录你明确支持的平台和环境。
  • 在 CI 中广泛测试代码的运行环境。

轻微变更:引入新的 lints

库的某些变更可能会在用户侧触发新的 lints。这通常应被视为兼容变更。

// 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
}

需要注意,如果项目明确将警告设为拒绝(deny),而更新后的 crate 又是直接依赖,那么在技术上确实可能导致构建失败。 拒绝警告时应保持谨慎,并明白随着时间推移可能会有新的 lint 出现。 当然,库作者在引入新警告时也应克制,考虑这会给用户带来的潜在影响。

以下是一些更新依赖后可能出现的 lint 示例:

此外,将 rustc 升级到新版本也可能引入新的 lint。

传递依赖引入的新 lint 通常不会导致失败,因为 Cargo 会使用 --cap-lints 来屏蔽依赖中的所有 lint。

一些缓解策略:

  • 如果你在拒绝警告的模式下构建,就要明白每次更新依赖时都可能需要处理新出现的警告。 如果通过 RUSTFLAGS 传递 -Dwarnings,可以同时加上 -A 标志来放行那些容易出问题的 lint,例如 -Adeprecated
  • 功能开关(feature)背后引入弃用(deprecation)标记。例如使用 #[cfg_attr(feature = "deprecated", deprecated="use bar instead")]。这样,当你计划在未来的 SemVer 重大变更(breaking change)中移除某项内容时,就可以告知用户:在升级到移除弃用项之前,先启用 deprecated 开关。这允许用户自主决定何时响应弃用,而不必立即处理。不过,难点在于向用户传达:他们需要手动执行这些步骤来为重大版本更新做准备。

Cargo

次要变更:添加新的 Cargo feature

通常,添加新的Cargo feature是安全的。如果该 feature 引入了会导致破坏性变更(breaking change)的新改动,那么对于有严格向后兼容需求的项目可能会造成困难。在这种情况下,应避免将该 feature 加入“默认”列表,并尽可能记录启用该 feature 的后果。

# 次要变更

###########################################################
# 变更前
[features]
# ..空

###########################################################
# 变更后
[features]
std = []

重大变更:移除 Cargo feature

移除Cargo feature通常属于破坏性变更。任何启用了该 feature 的项目都会因此报错。

# 重大变更

###########################################################
# 变更前
[features]
logging = []

###########################################################
# 变更后
[dependencies]
# ..移除 logging

缓解策略:

  • 清晰记录你的 feature。如果有内部或实验性 feature,请明确标注,以便用户了解其状态。
  • Cargo.toml 中保留旧 feature 的定义,但移除其实际功能。记录该 feature 已弃用,并在未来的 SemVer 重大版本中将其完全移除。

重大变更:如果移除 feature 列表中的某项会改变功能或公共接口,则属于破坏性变更

如果从某个功能中移除子功能,且用户默认该功能可用,则可能破坏现有使用方的兼容性。

# 破坏性变更示例

###########################################################
# 变更前
[features]
default = ["std"]
std = []

###########################################################
# 变更后
[features]
default = []  # 若下游包默认 std 已启用,此变更可能导致其构建失败。
std = []

可能破坏兼容:移除可选依赖

移除 可选依赖可能破坏使用方项目,因为其他项目可能通过 Cargo feature启用了该依赖。

当存在可选依赖时,cargo 会隐式创建同名的 feature,用于控制依赖的启用与状态检测。可通过在 [features] 表中使用 dep: 语法禁用该隐式 feature,从而规避此问题。dep: 允许将可选依赖隐藏在语义更明确、更易于安全修改的名称之下。

# 破坏性变更示例

###########################################################
# 变更前
[dependencies]
curl = { version = "0.4.31", optional = true }

###########################################################
# 变更后
[dependencies]
# ..移除 curl
# 次要变更
#
# 此示例演示如何避免在替换可选依赖时引入破坏性变更。

###########################################################
# 变更前
[dependencies]
curl = { version = "0.4.31", optional = true }

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

###########################################################
# 变更后
[dependencies]
# 用一个可选依赖替换另一个。
hyper = { version = "0.14.27", optional = true }

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

缓解策略:

  • [features] 表中使用 dep: 语法,避免直接暴露可选依赖。详见 可选依赖章节。
  • 清楚记录你的 features。如果某个可选依赖没有出现在已记录的 features 列表中,你可以自行决定是否将改动未记录的条目视为安全操作。
  • 保留可选依赖,但在库内部不使用它。
  • 用一个没有任何作用的 Cargo feature 替代该可选依赖,并在文档中标注其已弃用。
  • 使用高层 feature 来启用可选依赖,并在文档中说明这是启用扩展功能的首选方式。例如,如果你的库对“网络”之类的功能提供可选支持,可以创建一个通用的 feature 名称“networking”,用它启用实现“网络”所需的各可选依赖,然后在文档中说明“networking”这个 feature。

Minor:修改依赖的 features

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

# MINOR CHANGE

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


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

Minor:新增依赖

只要新依赖不引入会导致破坏性变更的新要求,新增依赖通常是安全的。例如,在一个原本能在 stable 上正常构建的项目里新增一个依赖 nightly 的依赖,就属于 major 变更。

# MINOR CHANGE

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

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

应用兼容性

Cargo 项目还可能包含拥有独立接口的可执行二进制文件(例如 CLI 界面或操作系统级交互)。由于这些二进制文件属于 Cargo 包的一部分,它们通常与包本身共享相同的版本号。你需要决定在应用变更时,是否以及如何通过 SemVer 契约管理用户预期。应用层潜在的破坏性变更和兼容性变更种类繁多,无法一一列举,因此建议你在为应用制定版本策略时,参考 SemVer 规范的核心理念来辅助决策,至少也要清晰记录你对用户的承诺。

评论 (0)