Skip to content

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: Selfself: &Selfself: &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 Roundimpl<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 章

Released under the MIT License