Traits

欢马劈雪     最近更新时间:2020-08-04 05:37:59

289

traits.md
commit 6ba952020fbc91bad64be1ea0650bfba52e6aab4

trait 是一个告诉 Rust 编译器一个类型必须提供哪些功能语言特性。

你还记得impl关键字吗,曾用[方法语法](Method Syntax 方法语法.md)调用方法的那个?

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

trait 也很类似,除了我们用函数标记来定义一个 trait,然后为结构体实现 trait。例如,我们为Circle实现HasArea trait:

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

trait HasArea {
    fn area(&self) -> f64;
}

impl HasArea for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

如你所见,trait块与impl看起来很像,不过我们没有定义一个函数体,只是函数标记。当我们impl一个trait时,我们使用impl Trait for Item,而不是仅仅impl Item

泛型函数的 trait bound(Trait bounds on generic functions)

trait 很有用是因为他们允许一个类型对它的行为提供特定的承诺。泛型函数可以显式的限制(或者叫 [bound](Glossary 词汇表.md#界限(bounds)))它接受的类型。考虑这个函数,它并不能编译:

fn print_area<T>(shape: T) {
    println!("This shape has an area of {}", shape.area());
}

Rust抱怨道:

error: no method named `area` found for type `T` in the current scope

因为T可以是任何类型,我们不能确定它实现了area方法。不过我们可以在泛型T添加一个 trait bound,来确保它实现了对应方法:

# trait HasArea {
#     fn area(&self) -> f64;
# }
fn print_area<T: HasArea>(shape: T) {
    println!("This shape has an area of {}", shape.area());
}

<T: HasArea>语法是指any type that implements the HasArea trait(任何实现了HasAreatrait的类型)。因为 trait 定义了函数类型标记,我们可以确定任何实现HasArea将会拥有一个.area()方法。

这是一个扩展的例子演示它如何工作:

trait HasArea {
    fn area(&self) -> f64;
}

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl HasArea for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

struct Square {
    x: f64,
    y: f64,
    side: f64,
}

impl HasArea for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
}

fn print_area<T: HasArea>(shape: T) {
    println!("This shape has an area of {}", shape.area());
}

fn main() {
    let c = Circle {
        x: 0.0f64,
        y: 0.0f64,
        radius: 1.0f64,
    };

    let s = Square {
        x: 0.0f64,
        y: 0.0f64,
        side: 1.0f64,
    };

    print_area(c);
    print_area(s);
}

这个程序会输出:

This shape has an area of 3.141593
This shape has an area of 1

如你所见,print_area现在是泛型的了,并且确保我们传递了正确的类型。如果我们传递了错误的类型:

print_area(5);

我们会得到一个编译时错误:

error: the trait `HasArea` is not implemented for the type `_` [E0277]

泛型结构体的 trait bound(Trait bounds on generic structs)

泛型结构体也从 trait bound 中获益。所有你需要做的就是在你声明类型参数时附加上 bound。这里有一个新类型Rectangle<T>和它的操作is_square()

struct Rectangle<T> {
    x: T,
    y: T,
    width: T,
    height: T,
}

impl<T: PartialEq> Rectangle<T> {
    fn is_square(&self) -> bool {
        self.width == self.height
    }
}

fn main() {
    let mut r = Rectangle {
        x: 0,
        y: 0,
        width: 47,
        height: 47,
    };

    assert!(r.is_square());

    r.height = 42;
    assert!(!r.is_square());
}

is_square()需要检查边是相等的,所以边必须是一个实现了core::cmp::PartialEq trait 的类型:

impl<T: PartialEq> Rectangle<T> { ... }

现在,一个长方形可以用任何可以比较相等的类型定义了。

这里我们定义了一个新的接受任何精度数字的Rectangle结构体——讲道理,很多类型——只要他们能够比较大小。我们可以对HasArea结构体,SquareCircle做同样的事吗?可以,不过他们需要乘法,而要处理它我们需要了解[运算符 trait](Operators and Overloading 运算符和重载.md)更多。

实现 trait 的规则(Rules for implementing traits)

目前为止,我们只在结构体上添加 trait 实现,不过你可以为任何类型实现一个 trait。所以从技术上讲,你可以在i32上实现HasArea

trait HasArea {
    fn area(&self) -> f64;
}

impl HasArea for i32 {
    fn area(&self) -> f64 {
        println!("this is silly");

        *self as f64
    }
}

5.area();

在基本类型上实现方法被认为是不好的设计,即便这是可以的。

这看起来有点像狂野西部(Wild West),不过这还有两个限制来避免情况失去控制。第一是如果 trait 并不定义在你的作用域,它并不能实现。这是个例子:为了进行文件I/O,标准库提供了一个Writetrait来为File增加额外的功能。默认,File并不会有这个方法:

let mut f = std::fs::File::open("foo.txt").ok().expect("Couldn’t open foo.txt");
let buf = b"whatever"; // byte string literal. buf: &[u8; 8]
let result = f.write(buf);
# result.unwrap(); // ignore the error

这里是错误:

error: type `std::fs::File` does not implement any method in scope named `write`
let result = f.write(buf);
               ^~~~~~~~~~

我们需要先use这个Write trait:

use std::io::Write;

let mut f = std::fs::File::open("foo.txt").expect("Couldn’t open foo.txt");
let buf = b"whatever";
let result = f.write(buf);
# result.unwrap(); // ignore the error

这样就能无错误的编译了。

这意味着即使有人做了像给int增加函数这样的坏事,它也不会影响你,除非你use了那个trait。

这还有一个实现trait的限制。不管是trait还是你写的impl都只能在你自己的包装箱内生效。所以,我们可以为i32实现HasAreatrait,因为HasArea在我们的包装箱中。不过如果我们想为i32实现Floattrait,它是由Rust提供的,则无法做到,因为这个trait和类型都不在我们的包装箱中。

关于trait的最后一点:带有trait限制的泛型函数是单态monomorphization)(mono:单一,morph:形式)的,所以它是静态分发statically dispatched)的。这是什么意思?查看[trait对象](Trait Objects trait 对象.md)来了解更多细节。

多 trait bound(Multiple trait bounds)

你已经见过你可以用一个trait限定一个泛型类型参数:

fn foo<T: Clone>(x: T) {
    x.clone();
}

如果你需要多于1个限定,可以使用+

use std::fmt::Debug;

fn foo<T: Clone + Debug>(x: T) {
    x.clone();
    println!("{:?}", x);
}

T现在需要实现CloneDebug

where 从句(Where clause)

编写只有少量泛型和trait的函数并不算太糟,不过当它们的数量增加,这个语法就看起来比较诡异了:

use std::fmt::Debug;

fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) {
    x.clone();
    y.clone();
    println!("{:?}", y);
}

函数的名字在最左边,而参数列表在最右边。限制写在中间。

Rust有一个解决方案,它叫“where 从句”:

use std::fmt::Debug;

fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) {
    x.clone();
    y.clone();
    println!("{:?}", y);
}

fn bar<T, K>(x: T, y: K) where T: Clone, K: Clone + Debug {
    x.clone();
    y.clone();
    println!("{:?}", y);
}

fn main() {
    foo("Hello", "world");
    bar("Hello", "world");
}

foo()使用我们刚才的语法,而bar()使用where从句。所有你所需要做的就是在定义参数时省略限制,然后在参数列表后加上一个where。对于很长的列表,你也可以加上空格:

use std::fmt::Debug;

fn bar<T, K>(x: T, y: K)
    where T: Clone,
          K: Clone + Debug {

    x.clone();
    y.clone();
    println!("{:?}", y);
}

这种灵活性可以使复杂情况变得简洁。

where也比基本语法更强大。例如:

trait ConvertTo<Output> {
    fn convert(&self) -> Output;
}

impl ConvertTo<i64> for i32 {
    fn convert(&self) -> i64 { *self as i64 }
}

// can be called with T == i32
fn normal<T: ConvertTo<i64>>(x: &T) -> i64 {
    x.convert()
}

// can be called with T == i64
fn inverse<T>() -> T
        // this is using ConvertTo as if it were "ConvertTo<i64>"
        where i32: ConvertTo<T> {
    42.convert()
}

这突显出了where从句的额外的功能:它允许限制的左侧可以是任意类型(在这里是i32),而不仅仅是一个类型参数(比如T)。

默认方法(Default methods)

关于trait还有最后一个我们需要讲到的功能。它简单到只需我们展示一个例子:

trait Foo {
    fn is_valid(&self) -> bool;

    fn is_invalid(&self) -> bool { !self.is_valid() }
}

Footrait的实现者需要实现is_valid(),不过并不需要实现is_invalid()。它会使用默认的行为。你也可以选择覆盖默认行为:

展开阅读全文