【译】Rust标准库Trait指南(四)

500 阅读10分钟

原文标题:Tour of Rust's Standard Library Traits
原文链接:github.com/pretzelhamm…
公众号: Rust 碎碎念
翻译 by: Praying

内容目录 (译注:✅ 表示本文已翻译 ⏰ 表示后续翻译)

  • 引言 ✅
  • Trait 基础 ✅
  • 自动 Trait✅
  • 泛型 Trait✅
  • 格式化 Trait✅
  • 操作符 Trait⏰=>✅
  • 转换 Trait⏰
  • 错误处理 ⏰
  • 迭代器 Trait⏰
  • I/O Trait⏰
  • 总结 ⏰

格式化 Traits (Formatting Traits)

我们可以使用std::fmt中的格式化宏来把类型序列化(serialize)为字符串,其中最为我们熟知的就是println!。我们可以把格式化参数传递给{}占位符,这些占位符用于选择使用哪个 trait 来序列化占位符参数。

TraitPlaceholderDescription
Display{}显示表示
Debug{:?}调试表示
Octal{:o}八进制表示
LowerHex{:x}小写十六进制表示
UpperHex{:X}大写十六进制表示
Pointer{:p}内存地址
Binary{:b}二进制表示
LowerExp{:e}小写指数表示
UpperExp{:E}大写指数表示

Display & ToString

trait Display {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}

Display类型可以被序列化为对用户更为友好的String类型。以Point类型为列:

use std::fmt;

#[derive(Default)]
struct Point {
    x: i32,
    y: i32,
}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    println!("origin: {}", Point::default());
    // prints "origin: (0, 0)"

    // get Point's Display representation as a String
    let stringified_point = format!("{}", Point::default());
    assert_eq!("(0, 0)", stringified_point); // ✅
}

除了使用format!宏让一个类型以String类型显示,我们还可以使用ToString trait:

trait ToString {
    fn to_string(&self) -> String;
}

这个 trait 不需要我们实现,事实上,由于 generic blanket impl,我们也不能去实现它,因为所有实现了Display的类型都会自动实现ToString

impl<T: Display + ?Sized> ToString for T;

Point上使用ToString

#[test] // ✅
fn display_point() {
    let origin = Point::default();
    assert_eq!(format!("{}", origin), "(0, 0)");
}

#[test] // ✅
fn point_to_string() {
    let origin = Point::default();
    assert_eq!(origin.to_string(), "(0, 0)");
}

#[test] // ✅
fn display_equals_to_string() {
    let origin = Point::default();
    assert_eq!(format!("{}", origin), origin.to_string());
}

Debug

trait Debug {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}

DebugDisplay有着相同的签名。唯一的不同在于,只有当我门指定了{:?}才会调用Debug实现。Debug可以被派生:

use std::fmt;

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

// derive macro generates impl below
impl fmt::Debug for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Point")
            .field("x", &self.x)
            .field("y", &self.y)
            .finish()
    }
}

为一个类型实现Debug能够使得这个类型在dbg!中使用,dbg!宏在快速打印日志方面比println!更合适,它的一些优势如下:

  1. dbg!打印到 stderr 而不是 stdout,因此在我们的程序中,能够很容易地和标准输出的输出结果区分。
  2. dbg!会连同传入的表达式和表达式的计算结果一起打印出来。
  3. dbg!会获取传入参数的所有权并将其返回,因此你可以在表达式中使用它:
fn some_condition() -> bool {
    true
}

// no logging
fn example() {
    if some_condition() {
        // some code
    }
}

// println! logging
fn example_println() {
    // 🤦
    let result = some_condition();
    println!("{}", result); // just prints "true"
    if result {
        // some code
    }
}

// dbg! logging
fn example_dbg() {
    // 😍
    if dbg!(some_condition()) { // prints "[src/main.rs:22] some_condition() = true"
        // some code
    }
}

dbg!的唯一缺点就是它不会在 release 构建中自动裁剪,所以如果我们不想在最后生成的二进制包含这些内容,就必须手动移除它。

操作符 Trait(Operator Traits)

Rust 中所有的操作符都和 trait 关联,如果我们想要为我们的类型实现一些操作符,我们就必须实现与之关联的 trait。

Trait(s)分类(Category)操作符(Operator(s))描述(Description)
Eq, PartialEq比较==相等
Ord, PartialOrd比较<, >, <=, >=比较
Add算术+相加
AddAssign算术+=相加并赋值
BitAnd算术&按位与
BitAndAssign算术&=按位与并赋值
BitXor算术^按位异或
BitXorAssign算术^=按位异或并赋值
Div算术/
DivAssign算术/=除并赋值
Mul算术*
MulAssign算术*=乘并赋值
Neg算术-一元求反
Not算术!一元逻辑求反
Rem算术%求余
RemAssign算术%=求余并赋值
Shl算术<<左移
ShlAssign算术<<=左移并赋值
Shr算术>>右移
ShrAssign算术>>=右移并赋值
Sub算术-
SubAssign算术-=减并赋值
Fn闭包(...args)不可变闭包调用
FnMut闭包(...args)可变闭包调用
FnOnce闭包(...args)一次性闭包调用
Deref其他*不可变解引用
DerefMut其他*可变解引用
Drop其他-类型析构
Index其他[]不可变索引
IndexMut其他[]可变索引
RangeBounds其他..区间

比较 Trait (Comparison Traits)

Trait(s)分类(Category)操作符(Operator(s))描述(Description)
Eq, PartialEq比较==相等
Ord, PartialOrd比较<, >, <=, >=比较

PartialEq & Eq

trait PartialEq<Rhs = Self>
where
    Rhs: ?Sized,
{
    fn eq(&self, other: &Rhs) -> bool;

    // provided default impls
    fn ne(&self, other: &Rhs) -> bool;
}

PartialEq<Rhs>类型可以通过==操作符检查是否和Rhs类型相等。

所有的PartialEq<Rhs>实现必须确保相等性是对称的和可传递的。这意味着,对于任意的abc:

  • a == b也意味着b == a(对称性)
  • a == b && b == c 意味着 a == c (传递性)

默认情况下,Rhs = Self,因为我们几乎总是想要比较同一类型的不同实例,而不是不同类型的不同实例。这也保证了我们的实现是对称的和可传递的。

struct Point {
    x: i32,
    y: i32
}

// Rhs == Self == Point
impl PartialEq for Point {
    // impl automatically symmetric & transitive
    fn eq(&self, other: &Point) -> bool {
        self.x == other.x && self.y == other.y
    }
}

如果一个类型的所有成员都实现了PartialEq,则它会派生实现PartialEq

#[derive(PartialEq)]
struct Point {
    x: i32,
    y: i32
}

#[derive(PartialEq)]
enum Suit {
    Spade,
    Heart,
    Club,
    Diamond,
}

一旦我们为自己的类型实现了PartialEq,我们就能够轻松地在类型的引用之间进行相等性比较,这要归功于 generic blanket impls:

// this impl only gives us: Point == Point
#[derive(PartialEq)]
struct Point {
    x: i32,
    y: i32
}

// all of the generic blanket impls below
// are provided by the standard library

// this impl gives us: &Point == &Point
impl<A, B> PartialEq<&'_ B> for &'_ A
where A: PartialEq<B> + ?Sized, B: ?Sized;

// this impl gives us: &mut Point == &Point
impl<A, B> PartialEq<&'_ B> for &'_ mut A
where A: PartialEq<B> + ?Sized, B: ?Sized;

// this impl gives us: &Point == &mut Point
impl<A, B> PartialEq<&'_ mut B> for &'_ A
where A: PartialEq<B> + ?Sized, B: ?Sized;

// this impl gives us: &mut Point == &mut Point
impl<A, B> PartialEq<&'_ mut B> for &'_ mut A
where A: PartialEq<B> + ?Sized, B: ?Sized;

因为这个 trait 是泛型的,所以我们可以在不同的类型之间定义相等性(比较)。标准库利用这一点实现了类字符串类型之间的相互比较,比如String&strPathBuf&PathOsString&OsStr等等。

通常,我们应该仅为特定的不同类型之间实现相等性,这些不同类型包含了相同类型的数据,并且它们之间唯一的区别是表现数据的方式和与数据交互的方式。

下面是一个反面实例,关于某人试图在没有满足上述规则的不同类型之间实现PartialEq用以检查完整性的例子:

#[derive(PartialEq)]
enum Suit {
    Spade,
    Club,
    Heart,
    Diamond,
}

#[derive(PartialEq)]
enum Rank {
    Ace,
    Two,
    Three,
    Four,
    Five,
    Six,
    Seven,
    Eight,
    Nine,
    Ten,
    Jack,
    Queen,
    King,
}

#[derive(PartialEq)]
struct Card {
    suit: Suit,
    rank: Rank,
}

// check equality of Card's suit
impl PartialEq<Suit> for Card {
    fn eq(&self, other: &Suit) -> bool {
        self.suit == *other
    }
}

// check equality of Card's rank
impl PartialEq<Rank> for Card {
    fn eq(&self, other: &Rank) -> bool {
        self.rank == *other
    }
}

fn main() {
    let AceOfSpades = Card {
        suit: Suit::Spade,
        rank: Rank::Ace,
    };
    assert!(AceOfSpades == Suit::Spade); // ✅
    assert!(AceOfSpades == Rank::Ace); // ✅
}

Eq是一个标记 trait,并且是PartialEq<Self>的一个 subtrait。

trait Eq: PartialEq<Self> {}

如果我们为一个类型实现了Eq,在PartialEq所要求的对称性和可传递性之上,我们还保证了反射性(reflexivity),也就是对于任意的a,都有a == a。从这种意义上来说,EqPartialEq进行了细化,因为它表示了一个更为严格的相等性。如果一个类型的所有成员都实现了Eq,那么Eq的实现可以派生到这个类型。

浮点型实现了PartialEq但是没有实现Eq,因为NaN != NaN。几乎所有其他的实现了PartialEq的类型都实现了Eq,除非它们包含浮点类型。

一旦一个类型实现了PartialEqDebug,我们可以就可以在assert_eq!宏中使用它。我们还可以比较实现了PartialEq类型的集合。

#[derive(PartialEq, Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn example_assert(p1: Point, p2: Point) {
    assert_eq!(p1, p2);
}

fn example_compare_collections<T: PartialEq>(vec1: Vec<T>, vec2: Vec<T>) {
    // if T: PartialEq this now works!
    if vec1 == vec2 {
        // some code
    } else {
        // other code
    }
}

Hash

trait Hash {
    fn hash<H: Hasher>(&self, state: &mut H);

    // provided default impls
    fn hash_slice<H: Hasher>(data: &[Self], state: &mut H);
}

这个 trait 没有与任何操作符关联,但是讨论它的最好时机就是在PartialEqEq之后,所以把它写在这里。Hash类型可以通过一个Hasher被(计算)哈希。

use std::hash::Hasher;
use std::hash::Hash;

struct Point {
    x: i32,
    y: i32,
}

impl Hash for Point {
    fn hash<H: Hasher>(&self, hasher: &mut H) {
        hasher.write_i32(self.x);
        hasher.write_i32(self.y);
    }
}

使用派生宏可以生成和上面一样的实现:

#[derive(Hash)]
struct Point {
    x: i32,
    y: i32,
}

如果一个类型同时实现了HashEq,那么这些实现必须达成一致,从而保证对于所有的ab,如果a == b那么a.hash() == b.hash()。因此,当为一个类型同时实现这两个 trait 时,要么都用派生宏,要么都手动实现,但是不要混合,否则我们就有可能破坏上面的不变性。

为一个类型实现EqHash的最大好处是,它让我们能够把类型作为 key 存储在HashMapHashSet中。

use std::collections::HashSet;

// now our type can be stored
// in HashSets and HashMaps!
#[derive(PartialEq, Eq, Hash)]
struct Point {
    x: i32,
    y: i32,
}

fn example_hashset() {
    let mut points = HashSet::new();
    points.insert(Point { x: 0, y: 0 }); // ✅
}

PartialOrd & Ord

enum Ordering {
    Less,
    Equal,
    Greater,
}

trait PartialOrd<Rhs = Self>: PartialEq<Rhs>
where
    Rhs: ?Sized,
{
    fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;

    // provided default impls
    fn lt(&self, other: &Rhs) -> bool;
    fn le(&self, other: &Rhs) -> bool;
    fn gt(&self, other: &Rhs) -> bool;
    fn ge(&self, other: &Rhs) -> bool;
}

PartialOrd<Rhs>类型可以通过<<=>=操作符和Rhs类型比较。所有的PartialOrd<Rhs>实现必须保证比较时非对称和可传递的。这意味着,对于任意的abc

  • a < b意味着!(a>b)(非对称性)
  • a < b && b < c 意味着a < c(传递性)

PartialOrdPartialEq的一个 subtrait,并且它们的实现必须相互一致。

fn must_always_agree<T: PartialOrd + PartialEq>(t1: T, t2: T) {
    assert_eq!(t1.partial_cmp(&t2) == Some(Ordering::Equal), t1 == t2);
}

当比较PartialEq类型时,我们可以检查是否它们相等或者不相等,但是当比较PartialOrd类型时,我们除了可以检查是否它们相等或不相等自己哦之外,如果它们不相等,我们还可以检查它们不相等是因为第一项小于第二项或者是第一项大于第二项。

默认情况下,Rhs == Self,因为我们总是想要比较同一类型的实例,而不是对不同类型的实例。这也自动保证了我们的实现是对称的和可传递的。

use std::cmp::Ordering;

#[derive(PartialEq, PartialOrd)]
struct Point {
    x: i32,
    y: i32
}

// Rhs == Self == Point
impl PartialOrd for Point {
    // impl automatically symmetric & transitive
    fn partial_cmp(&self, other: &Point) -> Option<Ordering> {
        Some(match self.x.cmp(&other.x) {
            Ordering::Equal => self.y.cmp(&other.y),
            ordering => ordering,
        })
    }
}

如果一个类型的所有成员都实现了PartialOrd,那么它就可以被派生:

#[derive(PartialEq, PartialOrd)]
struct Point {
    x: i32,
    y: i32,
}

#[derive(PartialEq, PartialOrd)]
enum Stoplight {
    Red,
    Yellow,
    Green,
}

派生宏PartialOrd根据字典序(lexicographical)对它们的成员进行排序:

// generates PartialOrd impl which orders
// Points based on x member first and
// y member second because that's the order
// they appear in the source code
#[derive(PartialOrd, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

// generates DIFFERENT PartialOrd impl
// which orders Points based on y member
// first and x member second
#[derive(PartialOrd, PartialEq)]
struct Point {
    y: i32,
    x: i32,
}

OrdEqPartialOrd<Self>的一个 subtrait:

trait Ord: Eq + PartialOrd<Self> {
    fn cmp(&self, other: &Self) -> Ordering;

    // provided default impls
    fn max(self, other: Self) -> Self;
    fn min(self, other: Self) -> Self;
    fn clamp(self, min: Self, max: Self) -> Self;
}

如果我们为一个类型实现了Ord,在PartialOrd保证了非对称性和传递性之上,我们还能保证整体的非对称性,即对于任意给定的aba < ba == ba > b中必有一个为真。从这个角度来讲,Ord细化了EqPartialOrd,因为它表示一个更严格的比较。如果一个类型实现了Ord,我们就可以利用这个实现来实现PartialOrdPartialEqEq

use std::cmp::Ordering;

// of course we can use the derive macros here
#[derive(Ord, PartialOrd, Eq, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

// note: as with PartialOrd, the Ord derive macro
// orders a type based on the lexicographical order
// of its members

// but here's the impls if we wrote them out by hand
impl Ord for Point {
    fn cmp(&self, other: &Self) -> Ordering {
        match self.x.cmp(&other.x) {
            Ordering::Equal => self.y.cmp(&other.y),
            ordering => ordering,
        }
    }
}
impl PartialOrd for Point {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}
impl PartialEq for Point {
    fn eq(&self, other: &Self) -> bool {
        self.cmp(other) == Ordering::Equal
    }
}
impl Eq for Point {}

浮点型实现了PartialOrd但是没有实现Ord,因为NaN < 0 == falseNaN >= 0 == false都为真。几乎所有的其他的PartialOrd类型都实现了Ord,除非它们中包含有浮点型。

一旦一个类型实现了Ord,我们就可以把它存储在BTreeMapBTreeSet,还可以在 slice 上使用 sort()方法对其进行排序,这同样适用于其他可以解引用为 slice 的类型,比如数组、VecVecDeque

use std::collections::BTreeSet;

// now our type can be stored
// in BTreeSets and BTreeMaps!
#[derive(Ord, PartialOrd, PartialEq, Eq)]
struct Point {
    x: i32,
    y: i32,
}

fn example_btreeset() {
    let mut points = BTreeSet::new();
    points.insert(Point { x: 0, y: 0 }); // ✅
}

// we can also .sort() Ord types in collections!
fn example_sort<T: Ord>(mut sortable: Vec<T>) -> Vec<T> {
    sortable.sort();
    sortable
}