The Common Rust Traits

这篇文章主要内容来自The Common Rust Traits

1. What is a Trait?

在Rust中,数据类型 - 原始类型,结构体,枚举类型和其他聚合类型例如元组和数据本身都非常单一。尽管也可以为它们实现方法,但是方法仅是函数的变体。类型之间并不存在关系。

而引入Traits这种抽象机制的目的就在于为类型增加功能,同时构建类型之间的关系。具体来说有下面两种运作模式

  • 接口:traits支持接口继承,但不是实现继承。
  • 泛型约束:traits被用于泛型约束。泛型函数定义于实现了具体traits的类型之上,也即避免了c++模板的“compile-time duck typing”。如果我们传参传的是一只鸭子,那么它必须实现Duck。仅仅有quack()方法是不够的。

2. Converting Things to Strings

考虑定义了to_string方法的ToStringtrait。为了传入实现该trait的类型的引用作为参数,有两种写法。

第一种是通过泛型或者说单态(monomorphic):

1
2
3
4
5
6
7
use std::string::ToString;

fn to_string1<T: ToString> (item: &T) -> String {
    item.to_string()
}
println!("{}", to_string1(&42));
println!("{}", to_string1(&"hello"));

item是到某个实现了ToString的类型的引用。

第二种是通过动态或者说多态(polymorphic):

1
2
3
4
5
fn to_string2(item: &ToString) -> String {
    item.to_string()
}
println!("{}", to_string2(&42));
println!("{}", to_string2(&"hello"));

第一种情况,类似于c++的模板,编译器会为不同类型生成不同的代码。这种方法最为高效,to_string可以是内联的。

第二种情况,代码只被生成一次,但是实际的to_string被动态调用。这里的&ToString类似于java的接口或者c++带虚方法的基类。

对某个类型的引用被称为trait objecttrait object含有两部分:

  • 原始的引用
  • 虚方法表:包含了trait的方法
1
let d: &Display = &10;

对于trait object,rust采用一个更加明确的表示&dyn ToString

3. Printing Out: Display and Debug

为了能够通过{}格式化打印某个值,需要实现Displaytrait。而{:?}要求实现Debugtrait。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::fmt;

#[derive(Debug)]
struct MyType {
    x: u32,
    y: u32
}

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

let t = MyType{x:1,y:2};
println!("{}", t);

write!宏和println!关系紧密,其第一个参数是任何实现了Writetrait的类型。

大多数标准库类型都实现了Debug,且能够给出开发者友好的字符串表示。

任何实现了Displaytrait的类型会自动实现ToString,因此42.to_string()"hello".to_string()也能工作。

4. Default

Defaulttrait给出某个类型的默认值,例如:0是数值的默认值,空向量是vectors的默认值,”“是String的默认值。

一种间接生命整型变量并设置为0的方法如下,default是一个返回T的泛型方法。因此rust需要知道T的类型:

1
let n: u64 = Default::default();

Default也是通过derive实现。

1
2
3
4
5
6
#[derive(Default)]
struct MyStruct {
    name: String,
    age: u16
}
let mine: MyStruct = Default::default();

Rust喜欢明确,因而对变量初始化并赋值为默认值不会自动发生。例如,let n: u64;Rust会预期在之后初始化,而不会自动初始化。

rust中不存在”named function parameters”(即python中类似于f(a=1,b=2)。但是有一种惯用法能够达到相同的目的,例如有一个函数需要非常多配置参数,因此你定义了一个结构体称为Config,若Config实现了Default,则调用函数时就不用说明Config的每一个域,而是使用下面的方法

1
2
3
4
5
my_function(Config {
    job_name: "test",
    output_dir: Path::new("/tmp"),
    ...Default::default()
})

5. Conversion: From and Into

From给出使用from方法将某个值转换为另一个的方法。因而可以使用String::from("hello")。如果From被实现了,那么Intotrait会被自动实现。

因为String实现了From<&str>&str自动实现了Into<String>

1
2
let s = String::from("hello");
let s: String = "hello".into();

一个例子如下,json对象使用字符串作为索引,新的域可以被创建然后插入JsonValue值中:

1
2
3
obj["surname"] = JsonValue::from("Smith");
obj["name"] = "Joe".into();
obj["age"] = 35.into();

这里因为rust能够推断出左值的类型,使用into方法更加方便(更容易写和阅读)。

From表示了一种总是能够成功的类型转换。然而它开销较大:将字符串切片转换为String需要分配buffer并按字节拷贝。

From/Into在Rust错误处理时经常会使用,对于Result<T,E>:

1
let res = returns_some_result()?

上面?运算符实际上是下面代码的语法糖:

1
2
3
4
let res = match return_some_result() {
    Ok(r) => r,
    Err(e) => return Err(e.into())
}

即一个error类型能够转换为能被返回的error类型E

一种有用的错误处理策略是让函数返回Result<T, Box<Error>>。任何实现了Errortrait的类型都能够被转换为trait objectBox<Error>

6. Making Copies: Clone and Copy

From/Into描述了不同得类型是如何互相转换得。Clone描述了相同类型得一个新值是如何被创建的。Rust喜欢让所有可能开销很大的操作比较明显,因此需要使用val.clone()

让类型能够clone很方便,只需要

1
2
3
4
5
#[derive(Debug, Clone)]
struct Person {
    first_name: String,
    last_name: String,
}

Copy是一个marker trait(不存在需要实现的方法),

1
2
3
4
5
6
#[derive(Debug,Clone,Copy)]
struct Point {
    x: f32,
    y: f32,
    z: f32
}

当然只有所有域都实现了Copy,才能为该类型实现Copy

7. Fallible Conversions - FromStr

某些情况下的类型转换会存在错误,例如:整数42转换为字符串可以使用ToStringtrait定义的to_string方法。然而若是从”42”转换为i32类型则就是fallible转换。

这种转换的方法由FromStr提供,需要实现者:

  1. 定义from_str方法
  2. 设置当转换失败时需要返回的关联类型Err

通常,该trait隐含地通过parse方法使用,该方法具有泛型的返回值,因而需要使用turbofish运算符表明返回值的类型:

1
2
3
4
let answer = match "42".parse::<i32>() {
    Ok(n) => n,
    Err(e) => panic!("'42' was not 42!");
}

或者使用?,更加优雅一些:

1
let answer: i32 = "42".parse()?;

Rust标准库为数值类型和网络地址定义了FromStr

8. Reference Conversions - AsRef

AsRef用于两种类型的引用之间相互转换的情况,且转换的开销相对较少。

最常用的情景是和&Path一起使用。Rust使用专门的PathBuf存储文件系统路径名,底层使用的是OsString(用于存储不被信任的操作系统字符串)。&PathPathBuf的引用。而从常规的Rust字符串获取&Path引用开销很小。

1
2
3
4
5
6
7
8
9
10
// asref.rs
fn exists(p: impl AsRef<Path>) -> bool {
    p.as_ref().exists()
}

assert!(exists("asref.rs"));
assert!(exists(Path::new("asref.rs")));
let ps = String::from("asref.rs");
assert!(exists(&ps));
assert!(exists(PathBuf::from("asref.rs")));

使用impl AsRef<Path>处理文件系统路径的函数或方法能够以任何实现了AsRef<Path>的类型作为参数。根据文档

1
2
3
4
5
6
impl AsRef<Path> for Path
impl AsRef<Path> for OsStr
impl AsRef<Path> for OsString
impl AsRef<Path> for str
impl AsRef<Path> for String
impl AsRef<Path> for PathBuf

String也实现了AsRef<str>,因而可以使用

1
2
3
4
5
fn is_hello(s: impl AsRef<str>) {
    assert_eq("hello", s.as_ref());
}
is_hello("hello");
is_hello(String::from("hello"));

但是,rust程序员一般使用&str作为字符串参数的类型,并能通过deref coercion机制传参。

9. Overloading * - Deref

Rust的很多字符串方法并非直接定义在String上。String的方法通常会修改字符串,例如pushpush_str。但是类似于starts_with的函数也能被应用在字符串切片上。

Dereftrait用于实现”dereference”操作符*这和c语言中的解引用具有相同的语义:从引用指向的内存中提取出值。r是一个引用,则有r.foo(),但是如果你想获取值,就必须使用*r

Deref最常见的用例是被使用在智能指针例如Box<T>Rc<T>中。智能指针表现的像对其内存储值的引用,因而它们可以对Box<T>调用T的方法。

String实现了Deref。若s类型是String&*s的类型是&str

Deref coercion意味着&String会被隐含地转换为&str:

1
2
let s: String = "hello".into();
let rs: &str = &s;

然而,&String是一个和&str不同的类型。当使用match运算符明确地匹配类型时,仍必须使用s.as_str()&s在这里无法工作

1
2
3
4
5
let s = "hello".to_string();
match s.as_str() {
    "hello" => {....},
    "dolly" => {....},
}

Deref coericion也被用于决议方法,若某个方法不是定义在String上,则可以尝试使用&str。这表现地就像有限形式的继承。

除了String&strVec<T>&[T]也具有类似关系。

10. Ownership: Borrow

String“own”它们的数据,像&str这样的类型能够从owned类型”borrow”数据。

Borrow解决了一个maps和sets的问题。通常我们会把owned字符串保存在HashSet中以避免borrowing规则。

1
2
3
4
5
6
let mut set = HashSet::new();
set.insert("one".to_string());
// set is noew HashSet<String>
if set.contains("two") {
    println!("got two!");
}

Borrowtrait使得能使用&str类型的值查询set或maps。和AsRef不同

  • Borrow的实现要求更严格,会要求owned和borrowed的值的hash和ordering值相同
  • Borrow为上面的集合数据结构提供了blanket实现T:Borrow<T>.AsRef提供了另一种不同实现,基本上只要T: AsRef<U>就有&T: AsRef<U>

11. I/O: Read and Write

std::fs::Filestd::io::Stdin不同。Rust并没有将stdin视作文件的一种。它们相同之处在于实现了traitRead

基本的read方法读取一些字节到buffer中,并返回Result<usize>

Read提供了read_to_string方法,其会读取整个文件作为Stringread_to_end方法读取整个文件作为Vec<u8>(若不能保证文件是utf-8编码的,使用read_to_end更好)。

Read不是Rust prelude的一部分。可以使用use std::io::prelude::*;获取所有I/O traits。

Rust I/O 默认是unbuffered的。

例如,若想以最快的可能方式从stdin中读取内容,首先lock它

1
2
3
let stdin = io::stdin();
let mut lockin = stdin.lock();
// lockin is buffered

Locked stdin实现了ReadBuf(定义了buffered reading)。lines()方法迭代遍历输入的所有行,但是它为每一行分配一个新的字符串,因而效率很低。为了最佳的性能,使用read_line,因为它允许重用单个字符串buffer。

类似地,为了从文件中获取buffered reading:

1
let mut rdr = io::BufReader::new(File::open(file)?);

注意,Rust默认不适用buffered io的目的在于让buffering和allocation更加明确。

对于Writetrait,文件、sockets和标准流(stdout和stderr)实现了它。同样,它是unbufferd且io::BufWriter用于为实现了Write的类型增加buffering。

为了避免不同线程产生相互干扰的输出,println宏会获取唯一的锁,这导致较低的性能。若需要高性能,使用buffer和write宏。

12. Iteration: Iterator and IntoInterator

Iterator方法只需要实现一个next方法,其返回值类型是Option

使用迭代器的一种罗嗦的方法是

1
2
3
4
let mut iter = [10, 20, 30].iter();
while let Some(n) = iter.next() {
    println!("got {}", n);
}

for语句的使用更加精简

1
2
3
for n in [10, 20, 30].iter() {
    println!("got {}", n);
}

for语句需要提供的表达式其实是:“任何能够转换为迭代器的东西”,即由IntoIterator描述。所以,for n in &[10, 20, 30] {...}也能正常工作 - slice实现了IntoIterator。上面的代码因为迭代器本身也实现了IntoIterator

因此,Rust的for语句和一个trait紧密相关。

Rust中迭代器属于0开销抽象。事实上,如果明确地通过索引循环访问切片会更慢,因为rust会添加运行时的索引检查。

Iterator的提供方法有很多默认实现,例如mapfilter。使用它们可以避免写出循环,例如求和:

1
let res: i64 = (0..n).into_iter().sum();

传递一系列值给函数的通用方式是使用IntoIterator。使用&[T]限制太多而且要求调用者构建buffer(不方便且开销大):

1
2
3
4
5
fn sum(ii: impl IntoIterator<Item=i32>) -> i32 {
    ii.into_iter().sum()
}
println!("{}", sum(0..9));
println!("{}", sum(vec![1, 2, 3]));

13. Conclusion: Why are there So Many Ways to Create a String?

1
2
3
4
let s = "hello".to_string(); // ToString
let s = String::from("hello"); // From
let s: String = "hello".into(); // Into
let s = "hello".to_owned(); // ToOwned

有非常多方法能够创建一个字符串。但是值得注意的是这些方法都不是String类型本身的方法。它们所对应的trait都是必须的,因为它们让泛型编程得以工作。When you create strings in codde, just pick one way and use it consistently.

Rust的traits在使用前需要先引入作用域中。例如,在某个实现了Errortrait的类型调用description()之前需要使用use std::error::Error