离线下载
PDF版 ePub版

极客学院团队出品 · 更新于 2018-08-21 04:00:50

特征

你还记得 impl 关键字吗,它用于调用一个函数的 methodsyntax

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

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

特征几乎都是相似的,除了我们用方法签名去定义一个特征,然后实现该结构的特征。如下面所示:

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)
}
}

如您所见,这个特征块和 impl 块非常相似,但我们不定义一个主体,只是定义一个类型签名。当我们 impl 一个特征时,我们使用 impl 特征项,而不仅仅是 impl 项。

我们可以使用特征约束泛型。思考下面这个函数,它没有编译,只给我们一个类似的错误:

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

Rust 可能会抱怨道:

error: type `T` does not implement any method in scope named `area`

因为 T 可以是任何类型,我们不能确保它实现了 area 的方法。但我们可以添加一个特征约束的泛型 T,确保它已经实现:

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

语法 < T:HasArea > 意味着实现 HasArea 特征的任何类型。因为特征定义函数的类型签名,我们可以肯定,任何实现 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: failed to find an implementation of trait main::HasArea for int

到目前为止,我们只添加特征实现结构,但您可以实现任何类型的特征。所以从技术上讲,我们可以为 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();

我们一般认为用这种基本类型来实现方法是一种不够好的风格,即便这种实现方法是可行的。

这可能看起来比较粗糙,但是有两个其他的限制来控制特征的实现,防止失控。首先,如果特征不是在你的范围中定义的,那么不适用。下面是一个例子:标准库提供了编写特征,这个特征给文件的输入输出增加了额外的功能。默认情况下,文件并不会有自己的方法:

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");

我们需要先使用编写特征:

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 添加方法,也不会影响你,除非你使用这个特征。

实现特征还有其他限制。你为特征或类型所写的 impl 必须由你来定义。所以,我们可以实现 HasArea 类型等,因为 HasArea 是我们的代码。但是如果我们试图实现浮点型,它是 Rust 为 i32 所提供的特征,是不可能的,因为无论特征还是类型都不在我们的代码里面。

最后关于特征:通用函数特征的捆绑使用 “monomorphization”(mono:一个,morph:形式),所以他们都是静态调用。那意味着什么呢?到特征对象那一章查看更多细节。

多个特征边界

如您所看到的,您可以绑定特征与一个泛型的类型参数:

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

如果你需要多个绑定,您可以使用+:

use std::fmt::Debug;

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

T 现在需要复制以及调试。

Where 子句

只用少数泛型和少量的特征界限来写函数并不是太糟糕,但随着数量的增加,语法变得越来越糟糕:

use std::fmt::Debug;

fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) {
x.clone();
y.clone();
println!("{:?}", y);
}
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", "workd");
}

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 "ConvertFrom<i32>"
where i32: ConvertTo<T> {
1i32.convert()
}

这个例子展了示 where 子句的附加特性:它们允许范围内的左边可以是一个任意的类型(在这里是 i32),而不只是一个普通的类型参数(如 T)。

默认的方法

我们的最后一个特征的特性应包括:默认的方法。举个简单的例子更容易说明:

trait Foo {
fn bar(&self);

fn baz(&self) { println!("We called baz."); }
}

Foo 特征的实现者需要实现 bar()方法,但是他们不需要实现 baz()。他们会得到这种默认行为。如果他们这么选择的话就可以覆盖默认的情形:

struct UseDefault;

impl Foo for UseDefault {
fn bar(&self) { println!("We called bar."); }
}

struct OverrideDefault;

impl Foo for OverrideDefault {
fn bar(&self) { println!("We called bar."); }

fn baz(&self) { println!("Override baz!"); }
}

let default = UseDefault;
default.baz(); // prints "We called baz."

let over = OverrideDefault;
over.baz(); // prints "Override baz!"
⇱

继承

有时,想要实现某个特征需要先实现另一个特征:

trait Foo {
fn foo(&self);
}

trait FooBar : Foo {
fn foobar(&self);
}
 FooBar的实现者也要实现Foo,像这样:
struct Baz;

impl Foo for Baz {
fn foo(&self) { println!("foo"); }
}

impl FooBar for Baz {
fn foobar(&self) { println!("foobar"); }
}

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

error: the trait `main::Foo` is not implemented for the type `main::Baz` [E027]
上一篇: 泛型 下一篇: