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
类型的对象,这里 bar
是 T
类型的对象,而 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() 时转换出了资源所有权 }
上面例子中,有几点需要说明一下:
- 执行
let num1: Number = even.into();
时,相当于执行let num1 = Number::from(even);
; let num1: Number = even.into();
中必须为num1
显式地指定类型Number
,这样编译才知道你想转换为Number
类型;even.into()
执行完后,会消耗自己(转移资源所有权),不能再访问even
了。
作为一个库的作者,你最好是实现 From
,而不是 Into
,因为实现了 From
就自动有了对应的 Into
。
1.1.3. TryFrom, TryInto
如果类型转换可能失败,则可以使用 TryFrom
和 TryInto
,它们返回 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
ToOwned
为 Clone
的通用版本,它提供了方法 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); }