Some Rust Traits

Table of Contents

1. 模块 std::convert

在模块 std::convert 中有几个常用的 traits,如 From/Into/TryFrom/TryInto/AsRef/AsMut ,它们提供了从一种类型转换到另一种类型的方式,下面将介绍它们。

1.1. 值到值的转换(From, Into, TryFrom, TryInto)

1.1.1. From

From trait 中,定义了下面方法:

fn from(_: T) -> Self                     // 从 T 类型对象转换为自己

对于类型 U ,如果它实现了 From<T> ,那么可以通过 let foo = U::from(bar); 这样的代码来从 T 类型对象创建一个 U 类型的对象,这里 barT 类型的对象,而 foo 则是 U 类型对象。

比如 String 类型实现了 From<&str> ,所以可以这样创建 String 类型的对象:

let my_str = "hello world";                   // my_str 是 &str 类型
let my_string = String::from(my_str);         // 由 &str 对象创建 String 对象

下面是自定义转换规则的例子:

use std::convert::From;

#[derive(Debug)]
struct EvenNumber(i32);

#[derive(Debug)]
struct Number {
    value: i32,
}

impl From<EvenNumber> for Number {
    fn from(item: EvenNumber) -> Self {   // 定义如何从 EvenNumber 创建 Number 对象
        Number { value: item.0 }
    }
}

impl From<i32> for Number {
    fn from(item: i32) -> Self {          // 定义如何从 i32 创建 Number 对象
        Number { value: item }
    }
}

fn main() {
    let even = EvenNumber(2);
    let num1 = Number::from(even);        // 从 EvenNumber 对象创建 Number 对象
    let num2 = Number::from(77);          // 从 i32 创建 Number 对象
    println!("My number is {:?}", num1);
    println!("My number is {:?}", num2);
    // println!("even is {:?}", even);    // 这里会报错,因为 even 已经在 Number::from(even) 时转换出了资源所有权
}

1.1.2. Into

Into trait 中,定义了下面方法:

fn into(self) -> T                        // 把自己转换为 T 类型对象。会消耗自己(转移资源所有权)

如果类型上实现了 From ,则不需要显式实现 Into ,编译器会自动调用配对的 from ,如:

use std::convert::From;

#[derive(Debug)]
struct EvenNumber(i32);

#[derive(Debug)]
struct Number {
    value: i32,
}

impl From<EvenNumber> for Number {
    fn from(item: EvenNumber) -> Self {
        Number { value: item.0 }
    }
}

impl From<i32> for Number {
    fn from(item: i32) -> Self {
        Number { value: item }
    }
}

fn main() {
    let even = EvenNumber(2);
    let num1: Number = even.into();       // 把 EvenNumber 类型的 even 转换为 Number 类型的 num1
    let num2: Number = 77.into();         // 把 i32 类型的 77 转换为 Number 类型的 num1
    println!("My number is {:?}", num1);
    println!("My number is {:?}", num2);
    // println!("even is {:?}", even);    // 这里会报错,因为 even 已经在 even.into() 时转换出了资源所有权
}

上面例子中,有几点需要说明一下:

  1. 执行 let num1: Number = even.into(); 时,相当于执行 let num1 = Number::from(even);
  2. let num1: Number = even.into(); 中必须为 num1 显式地指定类型 Number ,这样编译才知道你想转换为 Number 类型;
  3. even.into() 执行完后,会消耗自己(转移资源所有权),不能再访问 even 了。

作为一个库的作者,你最好是实现 From ,而不是 Into ,因为实现了 From 就自动有了对应的 Into

1.1.3. TryFrom, TryInto

如果类型转换可能失败,则可以使用 TryFromTryInto ,它们返回 Result ,可以进行错误处理。

1.2. 引用到引用的转换(AsRef)

AsRef trait 中,定义了下面方法:

fn as_ref(&self) -> &T                    // 把自己转为另一种类型 T 的引用

下面是 AsRef<str> 的一个例子:

fn is_hello<T: AsRef<str>>(s: T) {        // 一个对象的类型实现了 AsRef<str>,就可以作为 is_hello 的参数
    let x: &str = s.as_ref();             // 这里 s.as_ref() 总会得到 &str
    assert_eq!("hello", x);
}

fn main() {
    let my_str: &str = "hello";
    is_hello(my_str);

    let my_string: String = String::from("hello");
    is_hello(my_string);                          // String 类型实现了 fn as_ref(&self) -> &str
}

可见, 把函数 is_hello 的参数类型设置为 AsRef<str> ,可以让函数使用起来更加方便,它既可以接收 &str 参数,又可以接收 String 参数,任何实现了 fn as_ref(&self) -> &str 的对象都可以是 is_hello 的参数。

1.3. 可变引用到可变引用的转换(AsMut)

AsMut trait 中,定义了下面方法:

fn as_mut(&mut self) -> &mut T            // 把自己转为另一种类型 T 的可变引用

AsMut<T>AsRef<T> 的可变引用版本。

下面是 AsMut<u64> 的一个例子:

fn main() {
    fn add_one<T: AsMut<u64>>(num: &mut T) {
        *num.as_mut() += 1;
    }

    let mut boxed_num = Box::new(0);
    add_one(&mut boxed_num);

    assert_eq!(*boxed_num, 1);
}

2. 模块 std::borrow

2.1. Borrow(更加严格的 AsRef)

Borrow trait 中,定义了方法 borrow()

pub trait Borrow<Borrowed: ?Sized> {    // ?Sized 表示 maybe Sized, https://github.com/rust-lang/rfcs/blob/master/text/0490-dst-syntax.md
    fn borrow(&self) -> &Borrowed;
}

AsRef<T> 一样, Borrow<T> 也表示把自己转换为另一个类型 T 的引用。

下面是 Borrow<str> 的一个例子:

use std::borrow::Borrow;

fn is_hello<T: Borrow<str>>(s: T) {       // 一个对象的类型实现了 Borrow<str>,就可以作为 is_hello 的参数
    let x: &str = s.borrow();             // 这里 s.borrow() 总会得到 &str
    assert_eq!("hello", x);
}

fn main() {
    let my_str: &str = "hello";
    is_hello(my_str);

    let my_string: String = String::from("hello");
    is_hello(my_string);                  // String 类型实现了 fn borrow(&self) -> &str
}

Borrow 和 AsRef 很相似,都会得到另一种类型的引用。不过, Borrow 比 AsRef 的限制更多,Borrow 要求两种类型之间必须有“内部等价性”,两种类型要有相同的 Eq, Ord and Hash 实现。AsRef 更加通用,覆盖类型更多,是 Borrow 的超集。 Borrow 和 AsRef 的区别可参考:https://github.com/rust-lang/rust/issues/24140

标准库 HashMap 的方法 get 的参数使用了 Borrow trait:

pub fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V>
where
    K: Borrow<Q>,
    Q: Hash + Eq,
{
    self.base.get(k)
}

下面是 HashMap 的 key 的类型是 String,但也可以使用 &str 进行查询的例子:

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<String, i32> = HashMap::new();

    let x: String = String::from("Foo");
    map.insert(x, 42);

    assert_eq!(map.get("Foo"), Some(&42));        // 尽管 HashMap 的 key 的类型是 String,但也可以使用 &str 查询
}

为什么 HashMap 的 key 使用 AsRef<T> 不合适呢?因为 AsRef<T> 太宽泛了,假设 HashMap 的 key 为 T1,当我们想使用另一种类型 T2 从 HashMap 中查询时,我们还希望 T1 和 T2 的 Hash,Eq 实现是一致的(这样尽管类型不同,也能定位到同一个键值),这个约束正是 Borrow 所要表明的,而 AsRef<T> 并没有体现这一点。

2.2. BorrowMut(更加严格的 AsMut)

BorrowMut 和 Borrow 的关系如同于 AsMut 和 AsRef 的关系。前面提到了 Borrow 是更加严格的 AsRef,类似地 BorrowMut 是更加严格的 AsMut。

2.3. ToOwned(更加通用的 Clone)

ToOwned trait 中定义下面方法:

fn to_owned(&self) -> T

ToOwnedClone 的通用版本,它提供了方法 to_owned() 用于类型转换。我们知道,对于实现了 Clone 的类型 T 可以从 &T 类型对象通过 .clone() 方法,生成具有所有权的 T 的实例,但是它只能由 &T 生成 T 。而对于实现了 ToOwned 的类型 T ,则可以从 &T 生成一个具有所有权的其它类型为 U 的实例。

下面是 to_owned() 的使用例子,从 &[i32] 构造了一个 Vec<i32> 对象:

fn main() {
    let v: &[i32] = &[1, 2];
    let v1: &[i32] = v.clone();       // clone 总返回相同类型的实例
    let v2: Vec<i32> = v.to_owned();  // to_owned 可以返回其它类型的实例,如这里 Slice [T] 上实现了 fn to_owned(&self) -> Vec<T>

    println!("{:?}", v);    // [1, 2]
    println!("{:?}", v1);   // [1, 2]
    println!("{:?}", v2);   // [1, 2]
}

由于 Slice 上实现了 fn to_owned(&self) -> Vec<T> ,所以上面例子中 v.to_owned() 可以得到 Vec<i32> 对象。

目前标准库中,实现了 ToOwned trait 的类型并不多(完整列表可参考 https://doc.rust-lang.org/std/borrow/trait.ToOwned.html#implementors ),下面是一些例子:

str:
fn to_owned(&self) -> String

CStr:
fn to_owned(&self) -> CString

OsStr:
fn to_owned(&self) -> OsString

Path:
fn to_owned(&self) -> PathBuf

[T]:
fn to_owned(&self) -> Vec<T>

2.4. Cow(Clone-On-Write 智能指针)

Cow 是一个具备 Clone-On-Write 特性的智能指针。 它实际上是一个枚举类型:

pub enum Cow<'a, B>
where
    B: 'a + ToOwned + ?Sized,
 {
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

从定义中可知,枚举 Cow 有两个可选值:Borrowed 和 Owned。

设计 Cow 的目的是减少内存复制,提高性能。

下面通过一个例子(摘自:https://hermanradtke.com/2015/05/29/creating-a-rust-function-that-returns-string-or-str.html )来介绍 Cow 的使用。假设要实现一个函数 remove_spaces ,其功能是去掉字符串中的空格。下面是一个简单的实现:

fn remove_spaces(input: &str) -> String {
   let mut output_string = String::with_capacity(input.len());    // 这里总是有内存分配

   for c in input.chars() {
      if c != ' ' {
         output_string.push(c);
      }
   }

   output_string
}

这个实现有个不好的地方:它总是分配内存,就算 input 不包含任何空格,它也总是会分配内存。

这了避免多余的内存分配,我们把返回值修改为 &str 类型,当 input 不包含任何空格时直接返回它,这样可以吗?下面是一个假想的实现(无法通过编译):

fn remove_spaces(input: &str) -> &str {
    if input.contains(' ') {
        let mut output_string = String::with_capacity(input.len());

        for c in input.chars() {
            if c != ' ' {
                output_string.push(c);
            }
        }

        return output_string.as_str();  // 这里会报错,因为 output_string 是在函数内创建的,函数返回时会释放掉,不能返回它的 Slice
    }

    return input;
}

上面代码不能通过编译,原因在注释中有交待。

既然不能改返回类型,那我们可以修改 input 类型为 String ,当 input 不包含任何空格时直接返回它,这样可以吗?

fn remove_spaces(input: String) -> String {
    if input.contains(' ') {
        let mut output_string = String::with_capacity(input.len());

        for c in input.chars() {
            if c != ' ' {
                output_string.push(c);
            }
        }

        return output_string;
    }

    return input;
}

上面代码可以通过编译,但它对用户不友好:如果调用者手里的类型是 &str ,那么不得不在调用之前先转换为 String ,而这个转换操作是有内存分配的。

上面两次尝试都失败了,该 Cow 登场了,返回 Cow<str> 可以解决前面的问题:

fn remove_spaces(input: &str) -> Cow<str> {
    if input.contains(' ') {
        let mut output_string = String::with_capacity(input.len());

        for c in input.chars() {
            if c != ' ' {
                output_string.push(c);
            }
        }

        return Cow::Owned(output_string);        // 返回的是枚举中的 Owned
    }

    return Cow::Borrowed(input);                 // 返回的是枚举中的 Borrowed
}

如果 input 包含空格,则返回函数内创建的 String 的一个包装(Cow::Owned);如果 input 不包含空格,则返回 input 的一个包装(Cow::Borrowed)。 这样,没必要分配内存时( input 不包含空格时),是不会分配内存的。

2.4.1. 调用 immutable 函数

Cow 使用起来很方便的一点是:如果你仅仅想对 Cow<T> 所包装的对象调用 immutable 函数,那么你根本不用关心 Cow<T> 到底是枚举中的 Owned,还是枚举中的 Borrowed,直接调用 immutable 函数即可。如:

    let s1 = remove_spaces("Hello");       // s1 是枚举中的 Borrowed,包装的是 &str
    let s1_len = s1.len();                 // 不用关心 s1 到底是枚举的哪一个,直接调用 immutable 函数 len

    let s2 = remove_spaces("Hello world"); // s2 是枚举中的 Owned,包装的是 String
    let s2_len = s2.len();                 // 不用关心 s2 到底是枚举的哪一个,直接调用 immutable 函数 len

之所以可以这样,是因为 Cow 实现了 Deref trait,利用了 Deref coercion 机制。

2.4.2. 调用 mutable 函数

我们想对 Cow<T> 所包装的对象进行修改,怎么办呢?先使用 into_owned() 获得包装的数据即可。如:

    let s1 = remove_spaces("Hello");       // s1 是枚举中的 Borrowed,包装的是 &str
    let mut x1 = s1.into_owned();          // 得到 String,这里会分配内存
    x1.push('a');

    let s2 = remove_spaces("Hello world"); // s2 是枚举中的 Owned,包装的是 String
    let mut x2 = s2.into_owned();          // 得到 String,这里不会再分配内存了,因为我们已经有了 String
    x2.push('a');

我们看一下 into_owned 的定义,就知道它为什么可以统一地把 Cow<str> 转换为 String 类型了:

pub fn into_owned(self) -> <B as ToOwned>::Owned {
    match self {
        Borrowed(borrowed) => borrowed.to_owned(),    // 这里通过 str 上的 to_owned() 转换成了 String
        Owned(owned) => owned,                        // 这里直接返回 String
    }
}

2.4.3. 使用 into() 简化代码

使用 Into trait(参考节 1.1.2)可以把 &str 转换为 String 。使用 Into trait,也可以把 &str 或者 String 转换为枚举 Cow<str> 合适的成员。

前面实现的 remove_spaces 还可以写为下面形式:

fn remove_spaces(input: &str) -> Cow<str> {
    if input.contains(' ') {
        let mut output_string = String::with_capacity(input.len());

        for c in input.chars() {
            if c != ' ' {
                output_string.push(c);
            }
        }

        return output_string.into();        // 同 Cow::Owned(output_string);
    }

    return input.into();                    // 同 Cow::Borrowed(input);
}

Author: cig01

Created: <2020-12-07 Mon>

Last updated: <2021-01-23 Sat>

Creator: Emacs 27.1 (Org mode 9.4)