Skip to content

5.8 标准库中常见的 trait

标准库中有很多很有用的 trait,本节挑几个特别常见的给大家介绍一下。

5.8.1 Display 和 Debug

这两个 trait 在标准库中的定义是这样的:

rust
// std::fmt::Display
pub trait Display {
    fn fmt(&self, f: &mut Formatter) -> Result<(), Error>;
}
// std::fmt::Debug
pub trait Debug {
    fn fmt(&self, f: &mut Formatter) -> Result<(), Error>;
}

它们的主要用处就是用在类似println!这样的地方:

rust
use std::fmt::{Display, Formatter, Error};

#[derive(Debug)]
struct T {
    field1: i32,
    field2: i32,
}

impl Display for T {
    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
        write!(f, "{{ field1:{}, field2:{} }}", self.field1, self.field2)
    }
}

fn main() {
    let var = T { field1: 1, field2: 2 };
    println!("{}", var);
    println!("{:?}", var);
    println!("{:#?}", var);
}

只有实现了 Display trait 的类型,才能用{}格式控制打印出来;只有实现了 Debug trait 的类型,才能用{:?}{:#?}格式控制打印出来。它们之间更多的区别如下。

  • Display 假定了这个类型可以用 utf-8 格式的字符串表示,它是准备给最终用户看的,并不是所有类型都应该或者能够实现这个 trait。这个 trait 的 fmt 应该如何格式化字符串,完全取决于程序员自己,编译器不提供自动 derive 的功能。

  • 标准库中还有一个常用 trait 叫作std::string::ToString,对于所有实现了 Display trait 的类型,都自动实现了这个 ToString trait。它包含了一个方法to_string(&self) -> String。任何一个实现了 Display trait 的类型,我们都可以对它调用to_string()方法格式化出一个字符串。

  • Debug 则是主要为了调试使用,建议所有的作为 API 的“公开”类型都应该实现这个 trait,以方便调试**。它打印出来的字符串不是以“美观易读”为标准,编译器提供了自动 derive 的功能。

5.8.2 PartialOrd/Ord/PartialEq/Eq

在前文中讲解浮点类型的时候提到,因为NaN的存在,浮点数是不具备“total order(全序关系)”的。在这里,我们详细讨论一下什么是全序、什么是偏序。Rust 标准库中有如下解释。

对于集合 X 中的元素 abc

  • 如果a<b则一定有!(a>b);反之,若a>b,则一定有!(a<b),称为反对称性。

  • 如果a<bb<ca<c,称为传递性。

  • 对于 X 中的所有元素,都存在a<ba>b或者a==b,三者必居其一,称为完全性。

如果集合 X 中的元素只具备上述前两条特征,则称 X 是“偏序”。同时具备以上所有特征,则称 X 是“全序”。

从以上定义可以看出,浮点数不具备“全序”特征,因为浮点数中特殊的值NaN不满足完全性。这就导致了一个问题:浮点数无法排序。对于任意一个不是NaN的数和NaN之间做比较,无法分出先后关系。示例如下:

rust
fn main() {
    let nan = std::f32::NAN;
    let x = 1.0f32;
    println!("{}", nan < x);
    println!("{}", nan > x);
    println!("{}", nan == x);
}

以上不论是NaN < xNaN > x还是NaN == x,结果都是false。这是 IEEE754 标准中规定的行为。

因此,Rust 设计了两个 trait 来描述这样的状态:一个是std::cmp::PartialOrd,表示“偏序”,一个是std::cmp::Ord,表示“全序”。它们的对外接口是这样定义的:

rust
pub trait PartialOrd<Rhs: ?Sized = Self>: PartialEq<Rhs> {
    fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;
    fn lt(&self, other: &Rhs) -> bool { //... }
    fn le(&self, other: &Rhs) -> bool { //... }
    fn gt(&self, other: &Rhs) -> bool { //... }
    fn ge(&self, other: &Rhs) -> bool { //... }
}
pub trait Ord: Eq + PartialOrd<Self> {
    fn cmp(&self, other: &Self) -> Ordering;
}

从以上代码可以看出,partial_cmp函数的返回值类型是Option<Ordering>。只有 Ord trait 里面的cmp函数才能返回一个确定的 Ordering。f32 和 f64 类型都只实现了 PartialOrd,而没有实现 Ord。

因此,如果我们写出下面的代码,编译器是会报错的:

rust
let int_vec = [1_i32, 2, 3];
let biggest_int = int_vec.iter().max();

let float_vec = [1.0_f32, 2.0, 3.0];
let biggest_float = float_vec.iter().max();

对整数 i32 类型的数组求最大值是没问题的,但是对浮点数类型的数组求最大值是不对的,编译错误为:

rust
the trait 'core::cmp::Ord' is not implemented for the type 'f32'

笔者认为,这个设计是优点,而不是缺点,它让我们尽可能地在更早的阶段发现错误,而不是留到运行时再去 debug。假如说编译器无法静态检查出这样的问题,那么就可能发生下面的情况,以 Python 为例:

rust
Python 3.4.2 (default, Oct  8 2014, 10:45:20)
[GCC 4.9.1] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> v = [1.0, float("nan")]
>>> max(v)
1.0
>>> v = [float("nan"), 1.0]
>>> max(v)
nan

上面这个示例意味着,如果数组v中有NaN,对它求最大值,跟数组内部元素的排列顺序有关。

Rust 中的 PartialOrd trait 实际上就是 C++20 中即将加入的 three-way comparison 运算符<=>

同理,PartialEq 和 Eq 两个 trait 也就可以理解了,它们的作用是比较相等关系,与排序关系非常类似。

5.8.3 Sized

Sized trait 是 Rust 中一个非常重要的 trait,它的定义如下:

rust
#[lang = "sized"]
#[rustc_on_unimplemented = "`{Self}` does not have a constant size known at compile-time"]
#[fundamental] // for Default, for example, which requires that `[T]: !Default` be evaluatable
pub trait Sized {
    // Empty.
}

这个 trait 定义在std::marker模块中,它没有任何的成员方法。它有#[lang="sized"]属性,说明它与普通 trait 不同,编译器对它有特殊的处理。用户也不能针对自己的类型 impl 这个 trait。一个类型是否满足 Sized 约束是完全由编译器推导的,用户无权指定。

我们知道,在 C/C++ 这一类的语言中,大部分变量、参数、返回值都应该是编译阶段固定大小的。在 Rust 中,但凡编译阶段能确定大小的类型,都满足 Sized 约束。那还有什么类型是不满足 Sized 约束的呢?比如 C 语言里的不定长数组(Variable-length Array)。不定长数组的长度在编译阶段是未知的,是在执行阶段才确定下来的。Rust 里面也有类似的类型[T]。在 Rust 中 VLA 类型已经通过了 RFC 设计,只是暂时还没有实现而已。不定长类型在使用的时候有一些限制,比如不能用它作为函数的返回类型,而必须将这个类型藏到指针背后才可以。但它作为一个类型,依然是有意义的,我们可以为它添加成员方法,用它实例化泛型参数,等等。

Rust 中对于动态大小类型专门有一个名词 Dynamic Sized Type。我们后面将会看到的[T],str 以及 dyn Trait 都是 DST。

5.8.4 Default

Rust 里面并没有 C++ 里面的“构造函数”的概念。大家可以看到,它只提供了类似 C 语言的各种复合类型各自的初始化语法。主要原因在于,相比普通函数,构造函数本身并没有提供什么额外的抽象能力。所以 Rust 里面推荐使用普通的静态函数作为类型的“构造器”。比如,常见的标准库中提供的字符串类型 String,它包含的可以构造新的 String 的方法不完全列举都有这么多:


rust
fn new() -> String
fn with_capacity(capacity: usize) -> String
fn from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error>
fn from_utf8_lossy<'a>(v: &'a [u8]) -> Cow<'a, str>
fn from_utf16(v: &[u16]) -> Result<String, FromUtf16Error>
fn from_utf16_lossy(v: &[u16]) -> String
unsafe fn from_raw_parts(buf: *mut u8, length: usize, capacity: usize) -> String
unsafe fn from_utf8_unchecked(bytes: Vec<u8>) -> String

这还不算Default::default()From::from(s: &’a str)FromIterator::from_iter<I: IntoIterator<Item=char>>(iter: I)Iterator::collect等相对复杂的构造方法。这些方法接受的参数各异,错误处理方式也各异,强行将它们统一到同名字的构造函数重载中不是什么好主意(况且 Rust 坚决反对 ad hoc 式的函数重载)。

不过,对于那种无参数、无错误处理的简单情况,标准库中提供了 Default trait 来做这个统一抽象。这个 trait 的签名如下:


rust
trait Default {
    fn default() -> Self;
}

它只包含一个“静态函数”default()返回 Self 类型。标准库中很多类型都实现了这个 trait,它相当于提供了一个类型的默认值。

在 Rust 中,单词new并不是一个关键字。所以我们可以看到,很多类型中都使用了new作为函数名,用于命名那种最常用的创建新对象的情况。因为这些new函数差别甚大,所以并没有一个 trait 来对这些new函数做一个统一抽象。

Released under the MIT License