Appearance
5.1 成员方法
trait 中可以定义函数。用例子来说明,我们定义如下的 trait:
rust
trait Shape {
fn area(&self) -> f64;
}
上面这个 trait 包含了一个方法,这个方法只有一个参数,这个&self
参数是什么意思呢?
所有的 trait 中都有一个隐藏的类型Self
,代表当前这个实现了此 trait 的具体类型。 trait 中定义的函数,也可以称作关联函数(associated function)。 函数的第一个参数如果是Self
相关的类型,且命名为self
,这个参数可以被称为“receiver”(接收者)。 具有 receiver 参数的函数,我们称为“方法”(method),可以通过变量实例使用小数点.
来调用。 没有 receiver 参数的函数,我们称为“静态函数”(static function),可以通过类型加双冒号::
的方式来调用。 在 Rust 中,函数和方法没有本质区别。
Rust 中Self
(大写 S)和self
(小写 s)都是关键字,大写S
的是类型名,小写s
的是变量名。请大家一定注意区分。 self
参数同样也可以指定类型,当然这个类型是有限制的,必须是包装在Self
类型之上的类型。
对于第一个self
参数,常见的类型有self: Self
、self: &Self
、self: &mut Self
等类型。 对于以上这些类型,Rust 提供了一种简化的写法,我们可以将参数简写为self
、&self
、&mut self
。
self 参数只能用在第一个参数的位置。 请注意“变量self
”和“类型Self
”的大小写不同。示例如下:
rust
trait T {
fn method1(self: Self);
fn method2(self: &Self);
fn method3(self: &mut Self);
}
// 上下两种写法是完全一样的
trait T {
fn method1(self);
fn method2(&self);
fn method3(&mut self);
}
所以,回到开始定义的那个 Shape trait,上面定义的这个area
方法的参数的名字为self
,它的类型是&Self
类型。我们可以把上面这个方法的声明看成:
rust
trait Shape {
fn area(self: &Self) -> f64;
}
我们可以为某些具体类型实现(impl
)这个 trait。
假如我们有一个结构体类型 Circle,它实现了这个 trait,代码如下:
rust
struct Circle {
radius: f64,
}
impl Shape for Circle {
// Self 类型就是 Circle
// self 的类型是 &Self,即 &Circle
fn area(&self) -> f64 {
// 访问成员变量,需要用 self.radius
std::f64::consts::PI * self.radius * self.radius
}
}
fn main() {
let c = Circle { radius : 2f64};
// 第一个参数名字是 self,可以使用小数点语法调用
println!("The area is {}", c.area());
}
在上面的例子中可以看到,如果有一个 Circle 类型的实例c
,我们就可以用小数点调用函数,c.area()
。在方法内部,我们可以通过self.radius
的方式访问类型的内部成员。
另外,针对一个类型,我们可以直接对它 impl 来增加成员方法,无须 trait 名字。比如:
rust
impl Circle {
fn get_radius(&self) -> f64 { self.radius }
}
我们可以把这段代码看作是为 Circle
类型 impl 了一个匿名的 trait。用这种方式定义的方法叫作这个类型的“内在方法”(inherent methods)。
trait 中可以包含方法的默认实现。如果这个方法在 trait 中已经有了方法体,那么在针对具体类型实现的时候,就可以选择不用重写。当然,如果需要针对特殊类型作特殊处理,也可以选择重新实现来“override”默认的实现方式。比如,在标准库中,迭代器 Iterator 这个 trait 中就包含了十多个方法,但是,其中只有fn next(&mut self)-> Option<Self::Item>
是没有默认实现的。其他的方法均有其默认实现,在实现迭代器的时候只需挑选需要重写的方法来实现即可。
self
参数甚至可以是 Box 指针类型self: Box<Self>
。另外,目前 Rust 设计组也在考虑让self
变量的类型放得更宽,允许更多的自定义类型作为 receiver,比如MyType<Self>
。示例如下:
rust
trait Shape {
fn area(self: Box<Self>) -> f64;
}
struct Circle {
radius: f64,
}
impl Shape for Circle {
// Self 类型就是 Circle
// self 的类型是 Box<Self>,即 Box<Circle>
fn area(self : Box<Self>) -> f64 {
// 访问成员变量,需要用 self.radius
std::f64::consts::PI * self.radius * self.radius
}
}
fn main() {
let c = Circle { radius : 2f64};
// 编译错误
// c.area();
let b = Box::new(Circle {radius : 4f64});
// 编译正确
b.area();
}
impl 的对象甚至可以是 trait。示例如下:
rust
trait Shape {
fn area(&self) -> f64;
}
trait Round {
fn get_radius(&self) -> f64;
}
struct Circle {
radius: f64,
}
impl Round for Circle {
fn get_radius(&self) -> f64 { self.radius }
}
// 注意这里是 impl Trait for Trait
impl Shape for Round {
fn area(&self) -> f64 {
std::f64::consts::PI * self.get_radius() * self.get_radius()
}
}
fn main() {
let c = Circle { radius : 2f64};
// 编译错误
// c.area();
let b = Box::new(Circle {radius : 4f64}) as Box<Round>;
// 编译正确
b.area();
}
注意这里的写法,impl Shape for Round
和impl<T:Round>Shape for T
是不一样的。在前一种写法中,self
是&Round
类型,它是一个 trait object,是胖指针。而在后一种写法中,self
是&T
类型,是具体类型。前一种写法是为 trait object 增加一个成员方法,而后一种写法是为所有的满足T: Round
的具体类型增加一个成员方法。所以上面的示例中,我们只能构造一个 trait object 之后才能调用area()
成员方法。trait object 和“泛型”之间的区别请参考本书第三部分。
题外话,impl Shape for Round
这种写法确实是很让初学者纠结的,Round 既是 trait 又是 type。在将来,trait object 的语法会被要求加上dyn
关键字,所以在 Rust 2018 edition 以后应该写成impl Shape for dyn Round
才合理。关于 trait object 的内容,请参考本书第三部分第 23 章。