有Bug? Rust 1.81.0新排序实现真能帮程序员避坑?

2024-09-10 20:34:43 浏览数 (3)

讲动人的故事,写懂人的代码

几天前,Rust官网发布了1.81.0稳定版的发布报告(blog.rust-lang.org/2024/09/05/Rust-1.81.0.html)。

小吾很关注这次新发布的稳定版,能帮程序员避什么坑。

发布报告下面这段新特性的描述,吸引了小吾的注意。

新的排序实现 标准库中的稳定和不稳定排序实现都已更新为新算法,提高了它们的运行时性能和编译时间。 此外,这两种新的排序算法都试图检测Ord的不正确实现,这些实现会阻止它们产生有意义的排序结果,现在在这种情况下会引发panic,而不是返回实际上随机排列的数据。遇到这些panic的用户应该审核他们的排序实现,以确保它们满足PartialOrdOrd文档中所记录的要求。

❓什么是稳定排序?什么是不稳定排序?

在稳定排序中,相等元素的相对顺序在排序前后保持不变。例如,如果有两个相等的元素 A 和 B,且 A 在排序前位于 B 之前,那么在排序后 A 仍然会位于 B 之前。通常需要额外的内存来保存原始顺序信息。适合多级排序,如先按年龄排序,再按姓名排序。结果更可预测,尤其是在处理复杂数据结构时。可能比不稳定排序慢。除了适合多级排序,还适合需要保持原始顺序的重要性时,如保持用户输入的顺序;也适合处理复杂数据结构,如排序包含多个字段的结构体。

在不稳定排序中,相等元素的相对顺序可能会改变。排序后,A 可能会出现在 B 之前或之后。通常可以原地排序,不需要额外内存。通常更快,内存使用更少。不适合需要保持原始顺序的场景,多级排序时可能产生不直观的结果。适合性能关键的场景,当排序速度是首要考虑因素时;内存受限的环境,当额外内存使用是个问题时;排序简单数据类型,如整数数组;单次排序,不需要考虑多级排序的情况。

这看起来很好呀。之前“Ord的不正确实现”,会默默返回“实际上随机排列的数据”。在1.81.0之后,就能引发panic中止程序,提醒程序员修复bug,帮程序员避坑。

❓什么是panic

rust的panic是一种错误处理机制,用于处理程序遇到无法恢复的错误情况。它是程序遇到无法继续执行的情况时的一种反应。它会导致当前线程,通常是整个程序的突然终止。当 panic 发生时,程序会开始"展开"(unwind)调用栈。它会打印错误信息和调用栈跟踪。清理当前线程的资源(调用析构函数)。默认情况下,整个程序会在此终止。

那该如何验证一下这个新特性是否真的能帮程序员避坑?

我们可以做一个实验。先分别安装rust 1.80.1和1.81.0两个版本,以便比较运行差异。然后编写一段正常排序的代码。之后引入Ord的不正确实现,并假设这个不正确实现能在1.81.0下引发panic。最后观察实验结果。

安装rust 1.80.1和1.81.0

如果还没有在电脑上安装rust,可以参考rust官网的指导(www.rust-lang.org/tools/install)进行安装。安装好后,可以运行rustc --version进行验证。如果看到类似“rustc 1.81.0 (eeb90cda1 2024-09-04)”的输出,就说明安装成功。

接下来,可以运行下面两行命令,分别安装两个版本的rust。

代码语言:javascript复制
rustup toolchain install 1.81.0
rustup toolchain install 1.80.1

如何验证两个版本是否安装成功?可以运行下面的命令。

代码语言:javascript复制
rustup toolchain list

如果看到类似下面的输出,且里面有那两个版本,就说明安装成功了。

代码语言:javascript复制
stable-aarch64-apple-darwin
1.80.1-aarch64-apple-darwin
1.81.0-aarch64-apple-darwin (default)

正常的排序代码

下面我们以专业rust程序员最常使用的良好实践为标准,写一段正常排序的代码,如代码清单1所示。

代码清单1 专业rust程序员用最常使用的良好实践所编写的正常排序的代码

代码语言:javascript复制
 1 use std::cmp::Ordering;
 2 
 3 #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
 4 struct GoodOrd(i32);
 5 
 6 fn main() {
 7     let mut vec = vec![GoodOrd(3), GoodOrd(2), GoodOrd(4), GoodOrd(1)];
 8 
 9     println!("Before sorting: {:?}", vec);
10 
11     vec.sort();
12 
13     println!("After sorting: {:?}", vec);
14 
15     // Demonstrating correct ordering
16     assert!(GoodOrd(1) < GoodOrd(2));
17     assert!(GoodOrd(2) > GoodOrd(1));
18     assert!(GoodOrd(2) == GoodOrd(2));
19 
20     println!("All assertions passed!");
21 }
// Output:
// Before sorting: [GoodOrd(3), GoodOrd(2), GoodOrd(4), GoodOrd(1)]
// After sorting: [GoodOrd(1), GoodOrd(2), GoodOrd(3), GoodOrd(4)]
// All assertions passed!

如何运行代码

要把代码清单1运行起来,并看到类似代码后边注释里的打印输出,有两种办法。

第一种办法是在mycompiler.io网页上运行。

打开www.mycompiler.io/new/rust网页,把代码清单1所对应的没有行号的代码(可以克隆github.com/wubin28/wuzhenbens_playground代码库,进入wuzhenbens_playground文件夹,再进入文件夹new_sort_implementations_in_1_81_0_stable_rust,将git切换到80ade30提交,找到main.rs源文件,就能看到代码清单1),复制粘贴到网页左侧。然后点击网页右上角的Run按钮即可运行。

第二种办法是在本地电脑上运行。

用上面第一种方法找到没有行号的代码,然后用任何喜爱的IDE(比如Cursor、vscode或rustrover),打开这个main.rs文件。

要想运行这个文件,可以在终端的new_sort_implementations_in_1_81_0_stable_rust文件夹下,运行命令cargo run即可。要是你改动了代码,可以先运行cargo fmt格式化代码,然后运行cargo build进行编译构建,最后再运行cargo run运行程序。

如果你想从零开始,构建这个项目,可以在一个新项目文件夹中,运行命令cargo new new_sort_implementations_in_1_81_0_stable_rust,再进入文件夹new_sort_implementations_in_1_81_0_stable_rust,你就能看到src文件夹下,有一个main.rs文件。里面有一个hello world程序。此时你可以运行cargo run运行一下。之后,就可以把代码清单1所对应的没有行号的代码,复制粘贴进去,然后运行cargo fmt格式化代码,再运行cargo build进行编译构建,最后再运行cargo run运行程序。

代码运行起来后,如果能看到类似代码后边注释掉的打印输出,说明程序就能运行了。

正常排序代码的解释

代码清单1演示了自定义结构体的排序功能。

第1行引入了rust标准库的std::cmp::Ordering模块,用于比较和排序。

第4行定义了一个名为GoodOrd的结构体,它包含一个i32类型的值。

❓什么是结构体?

结构体(struct)是Rust中用于创建自定义数据类型的一种方式。"struct"是"Structure"的简写,可以理解为"结构"或"结构体"。

结构体具有以下特点。自定义数据类型,允许开发者创建包含多个相关值的复合数据类型。命名字段,每个字段都有一个名称和类型。灵活性,可以包含不同类型的数据。内存布局,字段在内存中是连续存储的。可以实现结构体的方法和关联函数。

结构体具有以下优势。组织相关数据,将相关的数据组合在一起,提高代码的可读性和维护性。类型安全,编译器可以检查结构体字段的类型正确性。封装,可以通过pub关键字控制字段的可见性。方法实现,可以为结构体实现方法,增强面向对象编程能力。内存效率,结构体的内存布局是连续的,访问效率高。

结构体也有以下劣势。内存对齐,可能导致一些内存浪费(虽然这通常不是大问题)。修改限制,一旦创建,结构体实例的字段默认是不可变的。复杂性,在某些简单场景下,使用结构体可能会增加不必要的复杂性。

结构体适用于以下场景。表示复杂的数据结构,如用户信息、配置选项等。实现自定义类型,当内置类型无法满足需求时。面向对象编程,结构体可以实现方法,类似于面向对象语言中的类。数据封装,将相关数据组织在一起,并控制访问权限。API设计,作为函数参数或返回值,提供清晰的接口。游戏开发,表示游戏中的实体、状态等。科学计算,表示复杂的数学模型或数据结构。

第3行为GoodOrd结构体派生了DebugEqPartialEqOrdPartialOrdtrait。这些trait使得结构体可以进行比较和排序操作。

❓什么是trait?

trait 是 Rust 中定义共享行为的方式。它类似于其他编程语言中的接口(interface)概念,但有一些独特的特性。Trait 定义了一组方法签名,可以被不同的类型实现。

trait具有以下特点。抽象行为,定义一组方法,而不提供具体实现。可以有默认实现,trait可以为方法提供默认实现。泛型约束,可以用作泛型约束,限制类型必须实现特定的trait。可以被动态分发,通过 trait 对象实现运行时多态。组合能力,可以通过组合多个 trait 来定义复杂的行为。关联类型,可以在 trait 中定义关联类型。

虽然Rust不支持传统意义上的类继承,但trait之间可以有类似继承的关系,即subtrait关系。另外,标记trait(marker trait)是没有任何方法的trait,用于标记类型具有某些属性。

trait具有以下优势。代码复用,允许多个类型共享相同的行为。抽象能力,可以编写不依赖具体类型的通用代码。灵活性,可以为现有类型实现新的trait,即使是在外部crate中定义的类型。组合高于继承,通过组合多个trait实现复杂行为,避免了继承的一些问题。静态分发,编译器可以进行单态化,提高运行时性能。动态分发,通过trait对象支持运行时多态。

trait也有以下劣势。复杂性,在某些情况下,trait的组合可能会导致代码变得复杂。编译时间,大量使用泛型和trait可能会增加编译时间。局限性,某些复杂的设计模式在Rust的trait系统中可能难以实现。

trait适用于以下场景。定义共享行为,当多个类型需要实现相同的功能时。泛型编程,编写可以操作多种类型的通用代码。抽象接口,定义模块或库的公共API。面向对象编程,实现类似于接口的功能。运行时多态,通过trait对象实现动态分发。扩展现有类型,为第三方类型添加新的功能。组合行为,通过组合多个trait来定义复杂的行为。类型约束,在泛型函数或结构体中限制类型必须实现特定的行为。

为何在GoodOrd(i32)结构体前面,派生那么多trait?

先看派生的这些trait,都能干啥。

Debug trait,允许使用 {:?} 格式说明符打印结构体。这使得程序员可以轻松地打印GoodOrd实例,对调试很有帮助。

PartialEqEqPartialOrdOrd这四个trait都与比较操作有关,但它们各自处理不同的比较方面。PartialEqEq 处理相等性比较,PartialOrdOrd 处理顺序比较。

PartialEq定义了部分相等关系,是最基本的相等性比较trait。所具有的主要方法有必须由实现者提供的eq() 和有默认实现的 ne()。允许存在"部分相等"的概念,即可能有些值无法比较。实现了 PartialEq 的类型可以使用 ==!= 运算符。用于大多数需要相等性比较的场景,适用于浮点数等可能存在特殊值(如NaN)的类型。

EqPartialEq 的subtrait。加强了 PartialEq 的要求,表示全等关系(equivalence relation,是一种二元关系,满足以下三个性质。自反性,对于任何元素a,a ∼ a。这里的波浪线代表“等价”,比“等于”更宽泛。对称性,如果a ∼ b,则b ∼ a。传递性:如果a ∼ b且b ∼ c,则a ∼ c。)。没有额外的方法,是一个标记trait。保证自反性,即对任何值 xx == x 总是为 true。用于需要确保完全相等性的场景,常用于哈希映射的键类型。

PartialOrdPartialEq的subtrait。它在部分相等性的基础上,增加了部分顺序比较。要求类型也实现 PartialEq。主要方法有必须由实现者提供的partial_cmp,返回 Option<Ordering>。它的ltlegtge方法都有默认实现。允许存在无法比较的情况。用于可能存在不可比较值的顺序比较,适用于浮点数等类型。

OrdEqPartialOrd 的subtrait。定义了全序关系(total order,是一种二元关系,满足以下四个性质。反对称性,如果a ≤ b且b ≤ a,则a = b。传递性,如果a ≤ b且b ≤ c,则a ≤ c。完全性或连通性,对于任意a和b,要么a ≤ b,要么b ≤ a。反自反性,如果a ≠ b,那么a < b或b < a必有一个成立)。要求类型也实现 EqPartialOrdPartialEq(间接subtrait)。主要方法有必须由实现者提供的cmp,返回 Ordering。它的maxminclamp 方法都有默认实现。保证任意两个值都可以比较。用于需要完全排序的场景(如排序算法),可以作为某些集合类型(如 BTreeMap)键的要求。

这4个trait的关系图如图1所示。

图1 PartialEqEqPartialOrdOrd这四个trait之间的subtrait和supertrait关系

那为什么需要这么多traits?因为下面一些原因。

  • 完整的比较功能。PartialEq, Eq, PartialOrd, 和 Ord 一起提供了完整的比较功能,允许相等性检查和排序。
  • 排序能力。Ord trait是vec.sort()方法所必需的。没有它,向量就不能自动排序。
  • 调试友好。Debug trait使得在开发过程中可以轻松打印和检查GoodOrd实例。
  • 类型安全。通过明确派生这些traits,确保了GoodOrd类型具有预期的行为,减少了运行时错误的可能性。
  • 代码简洁。通过派生这些traits,避免了手动实现它们的复杂性,使代码更加简洁和易于维护。

图1中四个trait的subtrait和supertrait关系出现了菱形。这会不会导致C 臭名昭著的菱形继承问题?

❓多个trait的subtrait和supertrait关系如果出现了菱形,会不会导致菱形继承问题

多重继承在C 中可能会导致以下问题。

菱形继承(Diamond Problem)。这是最常见的问题。当一个类从两个不同的类继承,而这两个类又有一个共同的基类时,就会出现菱形继承,如图2所示。

图2 C 中的菱形继承问题

在图2中,D类会继承A类的两个副本,一个通过B,另一个通过C。这可能导致歧义和因继承导致的数据冗余。

名称冲突。如果两个基类有相同名称的成员或方法,派生类可能会面临歧义。

复杂性增加。多重继承可能使类的设计变得复杂,难以理解和维护。

这些问题的危害包括代码复杂性增加、潜在的运行时开销、可能的逻辑错误,以及可维护性降低。

在Rust中,不存在C 中那样的类,但有能起到相似作用且更加灵活的trait。trait的subtrait与supertrait机制与C 的类继承有很大不同。Rust使用trait作为接口,而不是类。可以回顾一下代码清单1中那四个trait。PartialEq定义部分等价关系。EqPartialEq的subtrait,定义完全等价关系。PartialOrdPartialEq的subtrait,定义部分顺序关系。OrdEqPartialOrd的subtrait,定义完全顺序关系。

在Rust中,这种继承关系由于以下原因,不会导致C 中多重继承的典型问题。

没有菱形继承问题。Rust的trait不包含数据,只定义行为,所以不会出现因继承导致的数据冗余。

不存在状态继承。trait只定义接口,不继承状态。

名称冲突解决。Rust有明确的解决方案,如完全限定语法。

实现清晰。Rust要求显式实现trait,减少了隐式行为。

在图中,Ord虽然是EqPartialOrd的subtrait,但这不会导致C 式的多重继承问题。Rust的trait系统设计避免了这些问题。

上面提到,PartialEq::eqPartialOrd::partial_cmpOrd::cmp方法都是required,即必须由实现者提供,那么为何代码清单1中的代码,却没有实现它们?

❓trait的required方法为何没有实现

虽然 PartialEq::eqPartialOrd::partial_cmpOrd::cmp 方法必须由实现者提供,但在代码清单1中,这些方法是通过派生宏(derive macro)自动实现的。 在 Rust 中,#[derive(...)] 属性是一个强大的功能,它允许编译器自动为简单的结构体和枚举实现某些 trait。代码清单1中第3行告诉编译器,为 GoodOrd 自动实现 DebugEqPartialEqOrdPartialOrd trait。

对于像 GoodOrd 这样的单字段结构体(称为元组结构体),派生宏会生成基于该字段类型(在这里是 i32)的默认实现。i32 已经实现了这些 trait,所以派生宏可以直接使用 i32 的实现来为 GoodOrd 生成相应的方法。生成的代码等效于下面的代码,如代码清单2所示。

代码清单2 派生宏所生成的等效代码

代码语言:javascript复制
 1 impl PartialEq for GoodOrd {
 2     fn eq(&self, other: &Self) -> bool {
 3         self.0 == other.0
 4     }
 5 }
 6 
 7 impl Eq for GoodOrd {}
 8 
 9 impl PartialOrd for GoodOrd {
10     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
11         self.0.partial_cmp(&other.0)
12     }
13 }
14 
15 impl Ord for GoodOrd {
16     fn cmp(&self, other: &Self) -> Ordering {
17         self.0.cmp(&other.0)
18     }
19 }

这种方法大大简化了代码,减少了样板代码。对于简单的数据结构,自动派生通常就足够了。它确保了实现的正确性,避免了手动实现可能引入的错误。

但这种方法也有一些限制。派生宏只能为相对简单的情况生成实现。对于需要自定义行为的复杂类型,仍然需要程序员手动实现这些 trait。

第6-21行main函数创建了一个包含GoodOrd实例的向量vec。然后打印排序前的向量。接着使用sort()方法对向量进行排序。之后打印排序后的向量。接下来使用断言来验证GoodOrd实例之间的比较是否正确(检查小于、大于和相等关系)。最后,如果所有断言都通过,打印成功信息。

❓什么是向量

在Rust中,向量被称为"Vector",通常简写为"Vec"。它是一种可增长的数组类型,可以存储相同类型的多个值。

向量具有以下特点。动态大小,可以在运行时增加或减少元素。连续存储,元素在内存中连续存放。类型安全,只能存储相同类型的元素。索引访问,可以通过索引快速访问元素。所有权语义,遵循Rust的所有权规则。

可以使用下面两种方式,来分别创建空向量,以及用宏来创建并初始化向量。

代码语言:javascript复制
let mut vec = Vec::new();  // 创建空向量
let vec = vec![1, 2, 3];   // 使用宏创建并初始化

可以像下面那样用栈的方式添加和删除向量元素。当然也可以用其他非栈的方式,但通常速度较慢。

代码语言:javascript复制
vec.push(4);       // 在末尾添加元素
vec.pop();         // 移除并返回最后一个元素

访问元素的方式可以有下面两种。

代码语言:javascript复制
let third = vec[2];             // 通过索引访问
let third = vec.get(2);         // 安全访问,返回Option<&T>

向量具有以下优势。灵活性,可以动态调整大小。效率,对于大多数操作,性能接近数组。安全性,提供边界检查,防止越界访问。功能丰富,标准库提供了多种有用的方法。

向量也有下面的劣势。内存开销,比固定大小的数组略高。性能,某些操作(如在中间插入)可能较慢。不能直接用于FFI(Foreign Function Interface,外部函数接口,是一种机制,允许一种编程语言编写的代码调用另一种编程语言编写的代码),与C语言交互时需要转换。

向量适用于以下场景。需要动态增长的数据集合。需要频繁添加或删除元素的情况。不确定最终元素数量的场景。需要按索引快速访问元素的情况。实现栈或队列等数据结构。

这个例子展示了如何为自定义类型实现排序功能,这在Rust中是一个常见且有用的模式。

代码清单1的第7行,创建了一个可变的向量vec,其中的4个元素是 GoodOrd 结构体的实例。let mut vec 声明了一个名为 vec 的可变变量。mut 关键字表示这个变量是可以修改的,这是因为后面要进行向量本身的结构修改(即元素重新排序)。vec! 是 Rust 的宏,用于创建一个向量。[GoodOrd(3), GoodOrd(2), GoodOrd(4), GoodOrd(1)] 是向量的初始内容。这里创建了四个 GoodOrd 结构体的实例,每个实例都包含一个i32类型的整数值。

第9行用于在控制台输出向量 vec 的内容。println! 是 Rust 的宏,用于向标准输出(通常是控制台)打印文本。双引号中的 "Before sorting: {:?}" 是要打印的格式化字符串。"Before sorting: " 是固定的文本,会原样输出。{:?} 是一个格式化占位符,用于打印复杂类型的内容。,vec 是传递给 println! 宏的参数,它会被插入到格式化字符串的 {:?} 占位符位置。

{:?} 中的 :? 是 Debug 格式说明符。它告诉 Rust 使用 Debug trait 来格式化 vec。这对于打印复杂类型(如结构体、枚举或容器)特别有用。

使用 Debug 格式打印 vec 是可能的,因为第3行 GoodOrd 结构体已经通过 #[derive(Debug)] 自动实现了 Debug trait。

第11行 vec.sort(); 是对向量 vec 进行排序的操作。.sort() 是 Rust 标准库中 Vec<T> 类型的一个方法,用于对向量进行原地排序(in-place sorting)。这个方法会直接修改原向量,不会创建新的向量。这就是为什么 vec 需要声明为可变(mut)的原因。

sort() 方法默认使用元素类型实现的 Ord trait 来进行比较和排序。在这个例子中,GoodOrd 结构体通过 #[derive(Ord, PartialOrd)] 自动实现了 Ord trait。排序是按照升序进行的,也就是从小到大排列。对于 GoodOrd 结构体,排序会基于其内部的 i32 值进行。

从代码清单1后面注释里的运行结果能够看出,排序前的向量是:

代码语言:javascript复制
[GoodOrd(3), GoodOrd(2), GoodOrd(4), GoodOrd(1)]

排序后,它会变成:

代码语言:javascript复制
[GoodOrd(1), GoodOrd(2), GoodOrd(3), GoodOrd(4)]

这个排序操作展示了几个重要的 Rust 特性。

  • 结构体可以通过派生宏自动实现比较和排序的能力。
  • 标准库提供了高效的排序算法。
  • Rust 的类型系统和 trait 系统允许对自定义类型进行灵活的操作。

使用 sort() 方法是 Rust 中对向量进行排序的简单有效的方式,它利用了语言和标准库的特性来提供类型安全和高效的排序功能。

第16-18这三行代码使用了 Rust 的 assert! 宏来验证 GoodOrd 结构体的比较行为是否符合预期。第16行断言 GoodOrd(1) 小于 GoodOrd(2)。它验证了 < 运算符对 GoodOrd 实例的正确实现。这个断言期望 GoodOrd 的比较基于其内部的整数值。第17行断言 GoodOrd(2) 大于 GoodOrd(1)。它验证了 > 运算符对 GoodOrd 实例的正确实现。这个断言进一步确认了比较的一致性和正确性。第18行断言两个 GoodOrd(2) 实例是相等的。它验证了 == 运算符对 GoodOrd 实例的正确实现。这个断言确保具有相同内部值的 GoodOrd 实例被视为相等。

这些断言有以下目的。验证 GoodOrd 结构体正确实现了比较操作。确保 <>、和 == 运算符的行为符合预期。通过测试不同的比较场景来增加代码的可靠性。

这些断言验证了 #[derive(Eq, PartialEq, Ord, PartialOrd)] 自动实现的trait的结果。这个派生宏为 GoodOrd 结构体生成了比较和相等性的实现,基于其内部的 i32 值。

如果任何一个断言失败,程序将会 panic,这有助于在开发过程中快速发现和定位问题。在这个例子中,所有的断言都应该通过,因为它们反映了整数的自然排序顺序。

这种做法体现了 Rust 编程中的一个好习惯,即使用断言来验证关键的程序行为,增强代码的正确性和可靠性。

什么是断言?与单元测试有什么区别和联系?

❓什么是断言?与单元测试有什么区别?

断言(assertion)是在程序中插入的一种检查,用于验证某个条件是否为真。

在 Rust 中,断言通常使用 assert! 宏。如果断言失败,程序通常会立即终止或抛出异常。断言是程序代码的一部分,在正常执行流程中运行。

下面是一些断言的常用用法。

代码语言:javascript复制
assert!(condition);
assert_eq!(value1, value2);
assert_ne!(value1, value2);

断言具有以下优势。快速捕获和定位错误。作为程序自我检查的机制。可以作为文档的一部分,说明代码的预期行为。

断言也有一些劣势。在生产环境中可能会影响性能。如果没有适当处理,可能导致程序意外终止。

断言适用于以下场景。验证函数的前置条件和后置条件。检查重要的不变量。在开发和调试阶段进行快速验证。

单元测试(unit test)是针对程序中最小可测试单元(通常是函数或方法)编写的独立测试。

单元测试通常存在于单独的测试模块或文件中。使用专门的测试框架和工具运行。不会影响正常的程序执行流程。

下面是Rust单元测试的常用用法。

代码语言:javascript复制
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_some_function() {
        assert_eq!(some_function(input), expected_output);
    }
}

单元测试具有以下优势。可以全面测试各种情况,包括边界条件和异常情况。有助于重构和维护代码。可以作为回归测试的一部分。不影响生产代码的性能。

单元测试也有一些劣势。编写和维护相比断言需要更多的时间和努力。可能无法捕获集成或系统级别的问题。

单元测试适用于以下场景。验证单个函数或组件的正确性。测试各种输入和边界条件。在持续集成/持续部署(CI/CD)流程中自动化测试。

断言和单元测试之间到底有什么区别和联系?可以考虑下面几个方面。

运行时机。断言在程序运行时执行,而单元测试在开发和测试阶段单独运行。

范围。断言通常用于验证单个条件,而单元测试可以更全面地测试一个函数的行为。

影响。断言可能影响程序的正常运行,而单元测试不会影响生产代码的执行。

维护。单元测试需要单独维护,而断言是代码的一部分。

详细程度。单元测试通常更详细,可以测试多种情况,而断言往往更简单直接。

在实际开发中,这两种方法通常是互补的。断言用于捕获运行时的意外情况,而单元测试用于更系统地验证代码的正确性。结合使用这两种方法可以显著提高代码的质量和可靠性。

前面提到,断言”在生产环境中可能会影响性能”,而且“如果没有适当处理,可能导致程序意外终止”,那么在生产级别的代码中,是不是应该尽量减少断言?

❓在生产级别的代码中,是否应该尽量减少断言

在生产级别的代码中,该如何使用断言,涉及到软件开发中的一个常见权衡问题。需要考虑以下几个方面。

性能影响。断言会带来一定的性能开销,特别是在频繁执行的代码路径上。然而,这个开销通常是很小的,在大多数情况下可能微不足道。

安全性和正确性。断言可以帮助及早发现和诊断问题,防止错误状态进一步扩散,这对于维护系统的整体健康和正确性非常重要。

编译时优化。许多编程语言(包括 Rust)在发布模式(release mode)下会自动禁用或优化掉断言,从而消除生产环境中的性能影响。

关键检查。某些断言可能对于程序的正确性至关重要,即使在生产环境中也应该保留。

考虑到这些因素,以下是一些在生产代码中使用断言的避坑策略。

保留关键断言。对于保证程序正确性和安全性至关重要的检查,应该保留断言,即使在生产环境中也是如此。

使用条件编译。可以使用条件编译来控制哪些断言在生产环境中保留。例如在 Rust 中可以像下面那样写条件编译的断言。

代码语言:txt复制

#[cfg(debug_assertions)] 
assert!(condition, "This assertion only runs in debug mode");  

// 这个断言总是会执行 
debug_assert!(important_condition, "This is a critical check");
 

分级断言。可以根据断言的重要性和性能影响进行分级,只在生产环境中保留最关键的断言。

使用日志替代。对于一些不太关键但仍然有用的检查,可以考虑将它们转换为日志语句,而不是使用断言。

性能关键路径。在性能特别敏感的代码路径上,可以考虑移除或优化断言,但要确保通过其他方式(如单元测试)充分验证这部分代码的正确性。

监控和错误报告。在生产环境中,可以将断言失败转化为错误日志或报告,而不是直接终止程序。

在生产级别的代码中,不应该完全避免使用断言,而是应该谨慎和策略性地使用它们。关键是要在性能、安全性和代码可维护性之间找到平衡。保留重要的断言可以帮助及早发现问题,提高系统的健壮性。同时,通过编译时优化和条件编译,可以最小化断言对性能的影响。

最后,记住断言是防御性编程的一部分,它们与良好的错误处理、日志记录和监控系统一起,构成了保障软件质量的综合策略。

假设引入Ord的不正确实现能在1.81.0下引发panic

还记得rust 1.81.0发布报告里那句话吧。

稳定和不稳定排序的新的排序算法,都试图检测Ord的不正确实现,这些实现会阻止它们产生有意义的排序结果,现在在这种情况下会引发panic,而不是返回实际上随机排列的数据。

代码清单1中第11行,就是一个稳定排序。

为了验证这个新特性是否真的能帮程序员避坑,可以做下面的假设。

假设在代码清单1中引入Ord的不正确的实现,那么当在rust 1.81.0中运行这样的代码时,会引发panic。

为了让Ord的不正确的实现更加离谱,可以把PartialEqPartialOrd的实现也一并搞得不正确。如代码清单3所示。

代码清单3

代码语言:javascript复制
 1 use std::cmp::Ordering;
 2 
 3 #[derive(Debug)]
 4 struct BadOrd(i32);
 5 
 6 impl PartialEq for BadOrd {
 7     fn eq(&self, other: &Self) -> bool {
 8         // Intentionally inconsistent equality
 9         self.0 % 2 == other.0 % 2
10     }
11 }
12 
13 impl Eq for BadOrd {}
14 
15 impl PartialOrd for BadOrd {
16     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
17         // Violates consistency, transitivity, and duality
18         if self.0 % 2 == 0 && other.0 % 2 != 0 {
19             Some(Ordering::Less)
20         } else if self.0 % 2 != 0 && other.0 % 2 == 0 {
21             Some(Ordering::Greater)
22         } else if self.0 == other.0 {
23             Some(Ordering::Equal)
24         } else {
25             None
26         }
27     }
28 }
29 
30 impl Ord for BadOrd {
31     fn cmp(&self, other: &Self) -> Ordering {
32         // Inconsistent with PartialOrd and violates total ordering
33         if self.0 < other.0 {
34             Ordering::Greater
35         } else if self.0 > other.0 {
36             Ordering::Less
37         } else {
38             Ordering::Equal
39         }
40     }
41 }
42 
43 fn main() {
44     let mut vec = vec![BadOrd(3), BadOrd(2), BadOrd(4), BadOrd(1)];
45 
46     println!("Before sorting: {:?}", vec);
47 
48     vec.sort(); // This will likely panic due to inconsistent ordering
49 
50     println!("After sorting: {:?}", vec);
51 
52     // These assertions will fail, demonstrating incorrect ordering
53     assert!(BadOrd(1) < BadOrd(2));
54     assert!(BadOrd(2) > BadOrd(1));
55     assert!(BadOrd(2) == BadOrd(2));
56 
57     println!("All assertions passed!");
58 }
// Output:
// rustup run 1.81.0 cargo run
// Before sorting: [BadOrd(3), BadOrd(2), BadOrd(4), BadOrd(1)]
// After sorting: [BadOrd(2), BadOrd(4), BadOrd(3), BadOrd(1)]
// thread 'main' panicked at src/main.rs:53:5:
// assertion failed: BadOrd(1) < BadOrd(2)
// note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

要想找到没有行号的代码并运行代码清单3,可以克隆github.com/wubin28/wuzhenbens_playground代码库,进入wuzhenbens_playground文件夹,再进入文件夹new_sort_implementations_in_1_81_0_stable_rust,将git切换到1bcf0b97提交,找到main.rs源文件,就能看到代码清单3。

现在解释一下代码清单3中的代码。

代码清单3定义了一个名为 BadOrd 的结构体,并为其实现了 PartialEqEqPartialOrdOrd trait。这些实现故意违反了这些 trait 的预期行为,以展示不正确的排序和比较可能导致的问题。

0 人点赞