5.20.Traits

Traits

你还记得impl关键字吗,曾用方法语法调用方法的那个?

struct Circle {    x: f64,    y: f64,    radius: f64,}impl Circle {    fnarea(&self) -> f64 {        std::f64::consts::PI * (self.radius * self.radius)    }}

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

struct Circle {    x: f64,    y: f64,    radius: f64,}trait HasArea {    fnarea(&self) -> f64;}impl HasArea for Circle {    fnarea(&self) -> f64 {        std::f64::consts::PI * (self.radius * self.radius)    }}

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

那么这有什么重要的呢?还记得我们使用泛型inverse函数得到的错误吗?

error: binary operation `==` cannot be applied to type `T`

我们可以用trait来约束我们的泛型。考虑下这个函数,它不能编译并给出一个类似的错误:

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

Rust抱怨说:

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

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

fnprint_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 {    fnarea(&self) -> f64;}struct Circle {    x: f64,    y: f64,    radius: f64,}impl HasArea for Circle {    fnarea(&self) -> f64 {        std::f64::consts::PI * (self.radius * self.radius)    }}struct Square {    x: f64,    y: f64,    side: f64,}impl HasArea for Square {    fnarea(&self) -> f64 {        self.side * self.side    }}fnprint_area<T: HasArea>(shape: T) {    println!("This shape has an area of {}", shape.area());}fnmain() {    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.141593This shape has an area of 1

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

print_area(5);

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

error: failed to find an implementation of trait main::HasArea for int

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

trait HasArea {    fnarea(&self) -> f64;}impl HasArea for i32 {    fnarea(&self) -> f64 {        println!("this is silly");        *self as f64    }}5.area();

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

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

let mut f = std::fs::File::open("foo.txt").ok().expect("Couldn’t open foo.txt");let result = f.write("whatever".as_bytes());

这里是错误:

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

我们需要先useWritetrait:

use std::io::Write;let mut f = std::fs::File::open("foo.txt").ok().expect("Couldn’t open foo.txt");let result = f.write("whatever".as_bytes());

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

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

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

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

多trait限定(Multiple trait bounds)

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

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

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

use std::fmt::Debug;fnfoo<T: Clone + Debug>(x: T) {    x.clone();    println!("{:?}", x);}

T现在需要实现CloneDebug

where从句(Where clause)

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

use std::fmt::Debug;fnfoo<T: Clone, K: Clone + Debug>(x: T, y: K) {    x.clone();    y.clone();    println!("{:?}", y);}

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

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

use std::fmt::Debug;fnfoo<T: Clone, K: Clone + Debug>(x: T, y: K) {    x.clone();    y.clone();    println!("{:?}", y);}fnbar<T, K>(x: T, y: K) where T: Clone, K: Clone + Debug {    x.clone();    y.clone();    println!("{:?}", y);}fnmain() {    foo("Hello", "world");    bar("Hello", "workd");}

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

use std::fmt::Debug;fnbar<T, K>(x: T, y: K)    where T: Clone,          K: Clone + Debug {    x.clone();    y.clone();    println!("{:?}", y);}

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

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

trait ConvertTo<Output> {    fnconvert(&self) -> Output;}impl ConvertTo<i64> for i32 {    fnconvert(&self) -> i64 { *self as i64 }}// can be called with T == i32fnnormal<T: ConvertTo<i64>>(x: &T) -> i64 {    x.convert()}// can be called with T == i64fninverse<T>() -> T        // this is using ConvertTo as if it were "ConvertFrom<i32>"        where i32: ConvertTo<T> {    1i32.convert()}

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

默认方法(Default methods)

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

trait Foo {    fnbar(&self);    fnbaz(&self) { println!("We called baz."); }}

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

struct UseDefault;impl Foo for UseDefault {    fnbar(&self) { println!("We called bar."); }}struct OverrideDefault;impl Foo for OverrideDefault {    fnbar(&self) { println!("We called bar."); }    fnbaz(&self) { println!("Override baz!"); }}let default = UseDefault;default.baz(); // prints "We called bar."let over = OverrideDefault;over.baz(); // prints "Override baz!"

继承(Inheritance)

有时,实现一个trait要求实现另一个trait:

trait Foo {    fn foo(&self);}trait FooBar : Foo {    fn foobar(&self);}

FooBar的实现也必须实现Foo,像这样:

struct Baz;impl Foo for Baz {    fnfoo(&self) { println!("foo"); }}impl FooBar for Baz {    fnfoobar(&self) { println!("foobar"); }}

如果我们忘了实现Foo,Rust会告诉我们:

error: the trait `main::Foo` is not implemented for the type `main::Baz` [E0277]
文章导航