SemVer 兼容性
本章节详细介绍了对于软件包的新版本,通常被认为是兼容或破坏性 SemVer 更改的内容。有关 SemVer 是什么以及 Cargo 如何使用它来确保库的兼容性的详细信息,请参阅 SemVer 兼容性 部分。
这些仅是指南,并非所有项目都必须遵守的硬性规定。 变更类别 部分详细介绍了本指南如何对变更的级别和严重程度进行分类。本指南的大部分内容侧重于将导致 cargo
和 rustc
无法构建先前可以构建的内容的变更。几乎每个变更都存在一定风险,可能会对运行时行为产生负面影响,对于这些情况,通常由项目维护者来判断它是否是 SemVer 不兼容的变更。
变更类别
以下列出的所有策略均按变更级别进行分类
- 主要变更:需要主要 SemVer 版本号提升的变更。
- 次要变更:仅需要次要 SemVer 版本号提升的变更。
- 可能破坏性变更:某些项目可能认为是主要变更,而另一些项目可能认为是次要变更的变更。
“可能破坏性”类别涵盖了在更新期间可能会发生破坏的变更,但并非一定会导致破坏。应仔细考虑这些变更的影响。确切的性质将取决于变更和项目维护者的原则。
某些项目可能会选择仅在次要变更时提升补丁号。 建议遵循 SemVer 规范,并且仅在补丁版本中应用错误修复。但是,错误修复可能需要标记为“次要变更”的 API 变更,并且不应影响兼容性。本指南不对每个“次要变更”应如何处理采取立场,因为次要变更和补丁变更之间的差异是取决于变更性质的约定。
即使某些变更可能存在破坏构建的潜在风险,但仍被标记为“次要”。 这是针对潜在风险极低的情况,并且潜在的破坏性代码不太可能以惯用的 Rust 编写,或者明确不鼓励使用。
本指南使用术语“主要”和“次要”时,假设这与“1.0.0”或更高版本有关。 以“0.y.z”开头的初始开发版本可以将“y”中的变更视为主要版本,将“z”中的变更视为次要版本。“0.0.z”版本始终是主要变更。 这是因为 Cargo 使用的约定是,只有最左边的非零组件中的变更才被视为不兼容。
- API 兼容性
- 条目
- 类型
- 结构体
- 枚举
- trait
- 实现
- 泛型
- 函数
- 属性
- 工具和环境兼容性
- 应用程序兼容性
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
属性,该属性可以根据 条件编译 更改哪些条目或行为可用。
缓解策略
- 将要删除的条目标记为 deprecated,然后在以后的 SemVer 破坏性版本中删除它们。
- 将重命名的条目标记为 deprecated,并使用
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 导入,这可能是破坏性变更。 例如,如果您添加了一个新的 trait,并且项目使用了 glob 导入将该 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 导入是已知的向前兼容性风险。 应避免从外部 crate glob 导入条目。
主要:更改明确定义的类型的对齐方式、布局或大小
更改先前明确定义的类型的对齐方式、布局或大小是破坏性变更。
通常,使用 默认表示形式 的类型没有明确定义的对齐方式、布局或大小。 编译器可以自由更改对齐方式、布局或大小,因此代码不应对其进行任何假设。
注意:即使类型未明确定义,如果外部 crate 对类型的对齐方式、布局或大小做出假设,则可能会发生破坏。 这不被认为是 SemVer 破坏性变更,因为不应做出这些假设。
一些不属于破坏性变更的示例包括(假设未违反本指南中的其他规则)
- 以遵循本指南中的其他规则的方式添加、删除、重新排序或更改默认表示形式的结构体、union 或枚举的字段(例如,使用
non_exhaustive
以允许这些更改,或对已经是私有字段的私有字段进行更改)。 请参阅 struct-add-private-field-when-public、struct-add-public-field-when-no-private、struct-private-fields-with-private、enum-fields-new。 - 如果枚举使用
non_exhaustive
,则向默认表示形式枚举添加变体。 这可能会更改枚举的对齐方式或大小,但这些都不是明确定义的。 请参阅 enum-variant-new。 - 以遵循本指南中的其他规则的方式添加、删除、重新排序或更改
repr(C)
结构体、union 或枚举的私有字段(例如,使用non_exhaustive
,或在已经存在其他私有字段时添加私有字段)。 请参阅 repr-c-private-change。 - 如果枚举使用
non_exhaustive
,则向repr(C)
枚举添加变体。 请参阅 repr-c-enum-variant-new。 - 向默认表示形式的结构体、union 或枚举添加
repr(C)
。 请参阅 repr-c-add。 - 向枚举添加
repr(<int>)
原始表示形式。 请参阅 repr-int-enum-add。 - 向默认表示形式的结构体或枚举添加
repr(transparent)
。 请参阅 repr-transparent-add。
可以使用 repr
属性 的类型可以说具有在某种程度上定义的对齐方式和布局,代码可以对该类型进行一些假设,这些假设可能会因更改该类型而中断。
在某些情况下,具有 repr
属性的类型可能没有明确定义的对齐方式、布局或大小。 在这些情况下,更改类型可能是安全的,但应谨慎行事。 例如,具有不以其他方式记录其对齐方式、布局或大小保证的私有字段的类型不能被外部 crate 依赖,因为公共 API 并未完全定义类型的对齐方式、布局或大小。
具有私有字段的类型明确定义的常见示例是具有单个私有字段和泛型类型的类型,使用 repr(transparent)
,并且文档的文字讨论它对泛型类型是透明的。 例如,请参阅 UnsafeCell
。
一些破坏性变更的示例包括
- 向结构体或 union 添加
repr(packed)
。 请参阅 repr-packed-add。 - 向结构体、union 或枚举添加
repr(align)
。 请参阅 repr-align-add。 - 从结构体或 union 中删除
repr(packed)
。 请参阅 repr-packed-remove。 - 如果
repr(packed(N))
的值 N 更改了对齐方式或布局,则更改该值 N。 请参阅 repr-packed-n-change。 - 如果
repr(align(N))
的值 N 更改了对齐方式,则更改该值 N。 请参阅 repr-align-n-change。 - 从结构体、union 或枚举中删除
repr(align)
。 请参阅 repr-align-remove。 - 更改
repr(C)
类型的公共字段的顺序。 请参阅 repr-c-shuffle。 - 从结构体、union 或枚举中删除
repr(C)
。 请参阅 repr-c-remove。 - 从枚举中删除
repr(<int>)
。 请参阅 repr-int-enum-remove。 - 更改
repr(<int>)
枚举的原始表示形式。 请参阅 repr-int-enum-change。 - 从结构体或枚举中删除
repr(transparent)
。 请参阅 repr-transparent-remove。
次要:repr(C)
添加、删除或更改私有字段
通常可以安全地添加、删除或更改 repr(C)
结构体、union 或枚举的私有字段,假设它遵循本指南中的其他准则(请参阅 struct-add-private-field-when-public、struct-add-public-field-when-no-private、struct-private-fields-with-private、enum-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)
添加枚举变体
如果枚举使用 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 }, // 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)
向具有 默认表示形式 的结构体、union 或枚举添加 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();
}
主要:向结构体或 union 添加 repr(packed)
向结构体或 union 添加 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();
}
主要:向结构体、union 或枚举添加 repr(align)
向结构体、union 或枚举添加 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 },
};
}
主要:从结构体或 union 中删除 repr(packed)
从结构体或 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)] // 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 更改了对齐方式或布局,则更改该值 N
如果 repr(packed(N))
的值 N 更改了对齐方式或布局,则更改该值 N 是破坏性变更。 这可能会更改外部 crate 所依赖的对齐方式或布局。
如果值 N
低于公共字段的对齐方式,则会破坏任何尝试获取该字段引用的代码。
请注意,对 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 更改了对齐方式,则更改该值 N
如果 repr(align(N))
的值 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: evaluation of constant value failed
}
主要:从结构体、union 或枚举中删除 repr(align)
如果结构体、union 或枚举的布局明确定义,则从中删除 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 可能依赖于字段的特定顺序。
主要:从结构体、union 或枚举中删除 repr(C)
从结构体、union 或枚举中删除 repr(C)
是破坏性变更。 外部 crate 可能依赖于类型的特定布局。
主要:从枚举中删除 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;
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 => {}
}
}
缓解策略
- 在引入枚举时,将其标记为
#[non_exhaustive]
,以强制用户使用 通配符模式 来捕获新的变体。
主要:向枚举变体添加新字段
向枚举变体添加新字段是破坏性变更,因为所有字段都是公共字段,并且构造函数和匹配将无法编译。
// 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 时,使用 sealed 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 中存在同名方法,则可能会引入歧义。
// 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: cannot be made into an object
}
进行相反的操作(将非对象安全的 trait 变为安全的 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 类型参数
只要 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 捕获的更多信息,请参阅 edition guide 和 reference。
在 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
函数变为 safe
先前 unsafe
的函数可以变为 safe 而不会破坏代码。
但是请注意,这可能会导致 unused_unsafe
lint 触发,如下例所示,这将导致指定了 #![deny(warnings)]
的本地 crate 停止编译。 根据 引入新的 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) };
}
将先前 unsafe
的关联函数或结构体/枚举上的方法变为 safe 也是次要变更,而 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 外部的使用方式
- 非 exhaustive 结构体和枚举变体无法使用 结构体字面量 语法构造,包括 函数式更新语法。
- 对非 exhaustive 结构体进行模式匹配需要
..
,而对枚举进行匹配则不计入穷尽性。 - 不允许使用
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
};
}
缓解策略
- 在首次引入结构体、枚举和枚举变体时,将其标记为
#[non_exhaustive]
,而不是稍后添加#[non_exhaustive]
。
工具和环境兼容性
可能破坏性:更改所需的最低 Rust 版本
在新的 Rust 版本中引入新功能可能会破坏正在使用旧版本 Rust 的项目。 这也包括在新版本的 Cargo 中使用新功能,以及在以前在 stable 版本上工作的 crate 中要求使用仅限 nightly 版本的功能。
通常建议将此视为小改动,而不是重大改动,原因请参考此处。更新到较新版本的 Rust 通常相对容易。Rust 也有快速的 6 周发布周期,一些项目会在一个发布窗口内提供兼容性(例如,当前稳定版本加上之前的 N 个版本)。请记住,一些大型项目可能无法快速更新其 Rust 工具链。
缓解策略
- 使用 Cargo 功能 使新功能成为可选加入项。
- 为旧版本提供较长的支持窗口。
- 如果可能,复制新的标准库项的源代码,以便您可以继续使用旧版本,但可以利用新功能。
- 提供一个单独的旧次要版本分支,该分支可以接收重要 bug 修复的反向移植。
- 密切关注
[cfg(version(..))]
和#[cfg(accessible(..))]
功能,它们为新功能提供了可选加入机制。这些功能目前不稳定,仅在 nightly channel 中可用。
可能破坏性:更改平台和环境要求
库对其运行环境的假设范围非常广泛,例如主机平台、操作系统版本、可用服务、文件系统支持等。如果您发布的新版本限制了以前支持的内容(例如,需要较新版本的操作系统),则可能构成破坏性更改。这些更改可能难以跟踪,因为您可能并不总是知道更改是否在未自动测试的环境中造成破坏。
一些项目可能会认为这种破坏是可以接受的,特别是如果这种破坏对于大多数用户来说不太可能发生,或者项目没有资源来支持所有环境。另一个值得注意的情况是,当供应商停止对某些硬件或操作系统的支持时,项目可能会认为停止支持也是合理的。
缓解策略
- 记录您明确支持的平台和环境。
- 在 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 是更新依赖项时可能引入的示例
deprecated
— 当依赖项将#[deprecated]
属性 添加到您正在使用的项时引入。unused_must_use
— 当依赖项将#[must_use]
属性 添加到您未消耗结果的项时引入。unused_unsafe
— 当依赖项移除函数的unsafe
限定符,并且那是 unsafe 代码块中调用的唯一 unsafe 函数时引入。
此外,将 rustc
更新到新版本可能会引入新的 lint。
引入新 lint 的传递依赖项通常不应导致失败,因为 Cargo 使用 --cap-lints
来抑制依赖项中的所有 lint。
缓解策略
- 如果您使用拒绝警告的方式进行构建,请理解您可能需要在每次更新依赖项时处理解决新警告的问题。如果使用 RUSTFLAGS 传递
-Dwarnings
,也请添加-A
标志以允许可能导致问题的 lint,例如-Adeprecated
。 - 在 功能 背后引入弃用。例如
#[cfg_attr(feature = "deprecated", deprecated="use bar instead")]
。然后,当您计划在未来的 SemVer 破坏性更改中删除某个项时,您可以与您的用户沟通,他们应该在更新以删除已弃用项的使用之前启用deprecated
功能。这允许用户选择何时响应弃用,而无需立即响应。缺点是,可能难以与用户沟通他们需要采取这些手动步骤来为重大更新做准备。
Cargo
次要:添加新的 Cargo feature
添加新的 Cargo 功能 通常是安全的。如果该功能引入了导致破坏性更改的新更改,则可能会为对向后兼容性有更严格要求的项目造成困难。在这种情况下,避免将该功能添加到“default”列表,并尽可能记录启用该功能的后果。
# MINOR CHANGE
###########################################################
# Before
[features]
# ..empty
###########################################################
# After
[features]
std = []
主要:删除 Cargo feature
删除 Cargo 功能 通常是破坏性更改。这将导致任何启用该功能的项目出现错误。
# MAJOR CHANGE
###########################################################
# Before
[features]
logging = []
###########################################################
# After
[dependencies]
# ..logging removed
缓解策略
- 清楚地记录您的功能。如果存在内部或实验性功能,请将其标记为如此,以便用户了解该功能的状态。
- 将旧功能保留在
Cargo.toml
中,但在其他方面删除其功能。记录该功能已弃用,并在未来的主要 SemVer 版本中将其删除。
主要:如果从 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 功能 启用该依赖项。
当存在可选依赖项时,cargo 隐式定义一个同名的功能,以提供一种机制来启用该依赖项并检查何时启用它。可以通过在 [features]
表中使用 dep:
语法来避免此问题,这会禁用此隐式功能。使用 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:
语法,以避免首先暴露可选依赖项。有关更多信息,请参阅可选依赖项。 - 清楚地记录您的功能。如果可选依赖项未包含在已记录的功能列表中,那么您可以认为更改未记录的条目是安全的。
- 保留可选依赖项,只是不要在您的库中使用它。
- 将可选依赖项替换为不执行任何操作的 Cargo 功能,并记录它已弃用。
- 使用启用可选依赖项的高级功能,并将这些功能记录为启用扩展功能的首选方法。例如,如果您的库可选地支持“网络”之类的功能,请创建一个通用功能名称“networking”,该名称启用实现“网络”所需的可选依赖项。然后记录“networking”功能。
次要:更改依赖项 feature
更改依赖项的功能通常是安全的,只要该功能不引入破坏性更改即可。
# MINOR CHANGE
###########################################################
# Before
[dependencies]
rand = { version = "0.7.3", features = ["small_rng"] }
###########################################################
# After
[dependencies]
rand = "0.7.3"
次要:添加依赖项
添加新的依赖项通常是安全的,只要新的依赖项不引入导致破坏性更改的新要求即可。例如,在一个以前在 stable 版本上运行的项目中添加需要 nightly 版本的依赖项是一项重大更改。
# MINOR CHANGE
###########################################################
# Before
[dependencies]
# ..empty
###########################################################
# After
[dependencies]
log = "0.4.11"
应用程序兼容性
Cargo 项目也可能包含可执行二进制文件,这些二进制文件具有自己的接口(例如 CLI 接口、操作系统级交互等)。由于这些是 Cargo 包的一部分,它们通常使用并共享与包相同的版本。您需要决定是否以及如何对您对应用程序所做的更改采用与用户的 SemVer 协议。应用程序的潜在破坏性和兼容性更改太多,无法一一列举,因此我们鼓励您使用 SemVer 规范的精神来指导您如何将版本控制应用于您的应用程序的决策,或者至少记录您的承诺是什么。