Rust Trait 学习

概述

特征(trait)是rust中的概念,类似于其他语言中的接口(interface)。特征定义了一个可以被共享的行为,只要实现了特征,你就能使用该行为。
如果不同的类型具有相同的行为,那么我们就可以定义一个特征,然后为这些类型实现该特征。定义特征是把一些方法组合在一起,目的是定义一个实现某些目标所必需的行为的集合。例如,我们现在有圆形和长方形两个结构体,它们都可以拥有周长,面积。因此我们可以定义被共享的行为,只要实现了特征就可以使用。

pub trait Figure { // 为几何图形定义名为Figure的特征
fn girth(&self) -> u64; // 计算周长
fn area(&self) -> u64; // 计算面积
}

这里使用 trait 关键字来声明一个特征,Figure 是特征名。在大括号中定义了该特征的所有方法,在这个例子中有两个方法,分别是fn girth(&self) -> u64;和fn area(&self) -> u64;,特征只定义行为看起来是什么样的,而不定义行为具体是怎么样的。因此,我们只定义特征方法的签名,而不进行实现,此时方法签名结尾是 ;,而不是一个 {}。
接下来,每一个实现这个特征的类型都需要具体实现该特征的相应方法,编译器也会确保任何实现 Figure 特征的类型都拥有与fn girth(&self) -> u64;和fn area(&self) -> u64;签名的定义完全一致的方法。

Rust语言中的trait是非常重要的概念。在Rust中,trait这一个概念承 担了多种职责。很类似Go中的interface,但trait职责远比interface更多。trait中可以包含:函数、常量、类型等。

1,成员方法

们在特质中定义了一个成员方法,代码如下:

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

所有的trait中都有一个隐藏的类型Self(大写S),代表当前这个实 现了此trait的具体类型。

trait中定义的函数,也可以称作关联函数 (associated function)。

函数的第一个参数如果是Self相关的类型,且 命名为self(小写s),这个参数可以被称为“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”的大小写不同。比如:

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

我们可以为某些具体类型实现(impl)这个Shape trait。假如我们有一个结构体类型Circle,它实现了这个trait,代码如下:

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

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

另外,针对一个类型,我们可以直接对它impl来增加成员方法,无 须trait名字。比如:

impl Circle {
    fn get_radius(&self) -> f64 { self.radius }
}

我们可以把这段代码看作是为Circle类型impl了一个匿名的trait。用这种方式定义的方法叫作这个类型的“内在方法”(inherent methods)。

trait中可以包含方法的默认实现。如果这个方法在trait中已经有了 方法体,那么在针对具体类型实现的时候,就可以选择不用重写。

当然,如果需要针对特殊类型作特殊处理,也可以选择重新实现 来“override”默认的实现方式。比如,在标准库中,迭代器Iterator这个 trait中就包含了十多个方法,但是,其中只有fn next(&mut self)- >OptionSelf::Item是没有默认实现的。

其他的方法均有其默认实 现,在实现迭代器的时候只需挑选需要重写的方法来实现即可。

self参数甚至可以是Box指针类型self:Box。另外,目前Rust 设计组也在考虑让self变量的类型放得更宽,允许更多的自定义类型作为receiver,比如MyType。看下面的代码:

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。示例如下:

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 { //为满足T: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()成员方法。
impl Shape for Round这种写法确实是很让初学者纠结的, Round既是trait又是type。在将来,trait object的语法会被要求加上dyn关键字。

2,静态方法

没有receiver参数的方法(第一个参数不是self参数的方法)称作“静态方法”。

静态方法可以通过Type::FunctionName()的方式调用。

需要注意的是,即便我们的第一个参数是Self相关类型,只要变量名字不是self,就不能使用小数点的语法调用函数。

struct T(i32);
impl T {
// 这是一个静态方法
    fn func(this: &Self) {
    println!{"value {}", this.0};
    }
}
fn main() {
let x = T(42);
// x.func(); 小数点方式调用是不合法的
T::func(&x);
}

在标准库中就有一些这样的例子。Box的一系列方法Box:: into_raw(b:Self)   Box::leak(b:Self),

以及Rc的一系列方法 Rc::try_unwrap(this:Self)Rc::downgrade(this:&Self),都是这种情况。
它们的receiver不是self关键字,这样设计的目的是强制用户 用Rc::downgrade(&obj)的形式调用,而禁止obj.downgrade()形 式的调用。

这样源码表达出来的意思更清晰,不会因为Rc里面的成员方法和T里面的成员方法重名而造成误解问题。

trait中也可以定义静态函数。下面以标准库中的std::default:: Default trait为例,介绍静态函数的相关用法:

pub trait Default {
fn default() -> Self;
}

上面这个trait中包含了一个default()函数,它是一个无参数的函 数,返回的类型是实现该trait的具体类型。Rust中没有“构造函数”的念。Default trait实际上可以看作一个针对无参数构造函数的统一抽象.比如在标准库中,Vec::default()就是一个普通的静态函数。

impl<T> Default for Vec<T> {
fn default() -> Vec<T> {
Vec::new()
}
}

跟C++相比,在Rust中,定义静态函数没必要使用static关键字,因 为它把self参数显式在参数列表中列出来了。

作为对比,C++里面成员 方法默认可以访问this指针,因此它需要用static关键字来标记静态方 法。

Rust不采取这个设计,主要原因是self参数的类型变化太多,不同写法语义差别很大,选择显式声明self参数更方便指定它的类型。

3,扩展方法

我们还可以利用trait给其他的类型添加成员方法,哪怕这个类型不 是我们自己写的。比如,我们可以为内置类型i32添加一个方法:

trait Double {
fn double(&self) -> Self;
}
impl Double for i32 {
fn double(&self) -> i32 { *self * 2 }
}
fn main() {
// 可以像成员方法一样调用
let x : i32 = 10.double();
println!("{}", x);
}

哪怕这个类型不是在当前 的项目中声明的,我们依然可以为它增加一些成员方法。

但我们也不是随随便便就可以这么做的,Rust对此有一个规定:

在声明trait和 impltraitl的时候,Rust规定CoherenceRule(一致性规则)或称为OrphanRule(孤儿规则):

imp块要么与trait的声明在同一个crate中,要么与类型的声明在同一个crate中。

这是有意的设计。如果我们在使用其他的crate的时候, 强行把它们“拉郎配”,是会制造出bug的。比如说,我们写了一个程 序,引用了外部库lib1和lib2,lib1中声明了一个trait T,lib2中声明了一 个struct S,我们不能在自己的程序中针对S实现T。这也意味着,上游开 发者在给别人写库的时候,尤其要注意,一些比较常见的标准库中的 trait,如Display Debug ToString Default等,应该尽可能地提供好。否 则,使用这个库的下游开发者是没办法帮我们把这些trait实现的。同理,如果是匿名impl,那么这个impl块必须与类型本身存在于同一个crate中。

Rust是一种用户可以对内存有精确控制能力的强类型语言。我们可 以自由指定一个变量是在栈里面,还是在堆里面,变量和指针也是不同 的类型。类型是有大小(Size)的。有些类型的大小是在编译阶段可以 确定的,有些类型的大小是编译阶段无法确定的。目前版本的Rust规 定,在函数参数传递、返回值传递等地方,都要求这个类型在编译阶段 有确定的大小。否则,编译器就不知道该如何生成代码了。 而trait本身既不是具体类型,也不是指针类型,它只是定义了针对 类型的、抽象的“约束”。不同的类型可以实现同一个trait,满足同一个 trait的类型可能具有不同的大小。因此,trait在编译阶段没有固定大小,目前我们不能直接使用trait作为实例变量、参数、返回值。比如:

let x: Shape = Circle::new(); // Shape 不能做局部变量的类型
fn use_shape(arg : Shape) {} // Shape 不能直接做参数的类型
fn ret_shape() -> Shape {} // Shape 不能直接做返回值的类型

这样的写法是错误的,请一定要记住。trait的大小在编译阶段是不固定的,需要写成dynShape形式,即编译的时候把不确定大小的东西通过胖指针来代替,而指针在编译期是确定的。

4,完整函数调用方法

Fully Qualified Syntax提供一种无歧义的函数调用语法,允许程序员精确地指定想调用的是那个函数。以前也叫UFCS(universal function call syntax),也就是所谓的“通用函数调用语法”。这个语法可以允许使用类似的写法精确调用任何方法,包括成员方法和静态方法。其他一切 函数调用语法都是它的某种简略形式。它的具体写法为::item。示例如下:

trait Cook {
fn start(&self);
}
trait Wash {
fn start(&self);
}
struct Chef;
impl Cook for Chef {
fn start(&self) { println!("Cook::start");}
}
impl Wash for Chef {
fn start(&self) { println!("Wash::start");}
}
fn main() {
let me = Chef;
me.start(); //error,出现歧义,编译其器不知道调用哪个方法
}

//有必要使用完整的函数调用语法来进行方法调用
fn main() {
let me = Chef;
// 函数名字使用更完整的path来指定,同时,self参数需要显式传递 <Cook>::start(&me);
<Chef as Wash>::start(&me);
}

由此我们也可以看到,所谓的“成员方法”也没什么特殊之处,它跟 普通的静态方法的唯一区别是,第一个参数是self,而这个self只是一个 普通的函数参数而已。只不过这种成员方法也可以通过变量加小数点的 方式调用。变量加小数点的调用方式在大部分情况下看起来更简单更美 观,完全可以视为一种语法糖。
需要注意的是,通过小数点语法调用方法调用,有一个“隐藏 着”的“取引用”步骤。虽然我们看起来源代码长的是这个样子 me.start(),但是大家心里要清楚,真正传递给start()方法的参数是 &me而不是me,这一步是编译器自动帮我们做的。\color{red}不论这个方法接受 的self参数究竟是Self、&Self还是&mut Self,最终在源码上,我们都是 统一的写法:variable.method()。而如果用UFCS语法来调用这个方 法,我们就不能让编译器帮我们自动取引用了,必须手动写清楚。下面用一个示例演示一下成员方法和普通函数其实没什么本质区别。

struct T(usize);
impl T {
fn get1(&self) -> usize {self.0}
fn get2(&self) -> usize {self.0}
}
fn get3(t: &T) -> usize { t.0 }
fn check_type( _ : fn(&T)->usize ) {}
fn main() {
check_type(T::get1);
check_type(T::get2);
check_type(get3);
}

可以看到,get1、get2和get3都可以自动转成fn(&T)→usize类型。

5,trait 约束和继承

Rust的trait的另外一个大用处是,作为泛型约束使用。

未完待完善。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值