一、前言
本文主要详解Rust中Trait
这个概念,之所以为它单开一个章节,就是因为它非常的重要。
如果与其它语言类比来看,这个Trait
类似于Java
中的接口,C++
中的纯虚类,但却又不完全相同。
因为rust中没有类的概念,trait
作用的是结构体、乃至枚举。
二、初识Trait
trait
这个单词,本意为特征,在代码中的含义就是,让某个结构拥有某个特征。
比如我们之所以能用println!
这个宏打印出String
中的字符串,就是因为String
实现了一个叫做display
的特征。
标准库提供了很多基本的trait
,比如还有复制(copy
),克隆(clone
),调试(debug
),默认值(Default
)等等等等。
本章会对这些常见的trait
做出基本介绍,并学习如何写出与使用一个属于自己的trait
。
如下就是一个使用标准库Display
这个trait
的实例:
struct Stu {
name: String,
age: u32,
id: String,
}
fn main() {
let s = Stu {
name: String::from("www.kucoding.com"),
age: 100,
id: String::from("10010"),
};
println!("{}", s); //错误,Stu这个结构没有实现Display这个trait,所以无法打印
}
上面的代码,我自定义了一个结构体Stu
,并让其创建了一个变量s
,然后我直接用println!
这个宏打印它,是会报错的:
因为它现在还没有实现Display
这个trait
,这个宏不知道该怎么打印它。
所以下面我们就让其实现一下这个Display
:
use std::fmt;
struct Stu{ /*省略*/ }
impl fmt::Display for Stu {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "Name: {}, Age: {}, Id: {}", self.name, self.age,self.id);
}
}
fn main() {
let s=Stu{/*省略*/};
println!("{}", s); //正确,Stu这个结构实现了Display这个trait,所以可以打印
}
上面的代码中我省略了部分代码,让其看起来更加简洁,由于Display
这个trait
在标准库std::fmt
中,所以我们首先需要将其引入进来
use std::fmt;
库在rust
中被称为crate
,两者基本等价,就是换了个名字,使用的方式就是用use
关键字进行引入,标准crate为std
,然后fmt
这个crate又在std
里面,所以用两个:
来拿出它。
这类似于C++中的命名空间,比如using std::string;
,就类似于rust中的use std:fmt;
,而C++中的using namespace std
就类似于rust中的use std::*
,也就是让std
内所有内容都可用。
为这个结构体实现trait
的方式很简单,与前面介绍的为结构体绑定方法所用到的关键字一致,都是impl
,不同之处在于,方法是你随意为结构体绑定的,你可以在其中写任何方法:
impl Stu {
/*
可以写任何方法
*/
}
而如果要实现一个trait
,你必须写这个trait
内部已经写好的方法:
impl fmt::Display for Stu {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "Name: {}, Age: {}, Id: {}", self.name, self.age,self.id);
}
}
这里的impl fmt::Display for Stu
意思就是为Stu
实现fmt::Display
这个Trait
,而这个Trait中就有一个叫做fmt
的函数,格式如下:
这是写在标准库中的代码,会有很多我们不认识的东西,但这并不重要。
我们只需要将标准库中代码抄过来即可:
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
//我们要写的代码
}
对于实现一个trait来说,外面这个框架是固定的,我们要做的就是在这个框架中,写出自己的代码,比如这里:
return write!(f, "Name: {}, Age: {}, Id: {}", self.name, self.age,self.id);
我们直接通过调用一个宏write!
来实现这个字符串的格式化,将其格式化的内容存放在了f
这个参数中。
这样,我们就为自己的结构体实现了一个trait
,println
宏识别到了它已经实现了该trait
,就会通过调用该trait
中的fmt
函数来实现打印功能。
所以本质上来说,println
宏仅仅只是一个函数调用器,会将你的函数调用结果输出到控制台。
三、Trait的基本用法
其实从上面的示例代码中,你应该也基本感觉到了Trait
的作用:为不同的结构体统一方法。
因为println!
这个宏是官方推出的,官方怎么会知道我们自己写的结构体长什么样呢?所以正常情况下它自然不能输出非官方的结构体。
为了解决这种问题,就出现了Trait
,这时println
这个宏就不再管你是哪个结构体了,你想要怎么输出内容,你就写在Display
这个trait
中的fmt
函数中就行。
当我看到你实现了Display
这个Trait
,我就在要打印这个结构体的时候去调用这个函数就行了。
一个结构体可以被视为属性(字段)的集合,而一个Trait
则可以视为一组方法的集合,写法如下:
trait Test {
fn Show(self) -> String;
/*
可以继续写其它函数
*/
}
使用trait
关键字就可以定义一个trait
,后面紧跟这个trait
的名字,在内部,就可以写入你想要让这个Trait实现的功能。
举一个比较简单的例子,对于三角形、正方形、圆形等等,都需要求面积,如果不使用Trait
,一般就要像下面这样写:
struct triangle{ //为了简单,假设其是直角三角形,存放两个直角边
a: f32,
b: f32,
}
impl triangle { //绑定方法
fn area(&self) -> f32 {
return (self.a*self.b)/2.0;
}
}
//正方形
struct square{
a: f32
}
impl square { //绑定方法
fn area(&self) -> f32 {
return self.a*self.a;
}
}
/*等等其它形状也是类似的代码*/
这样为每个结构体都绑定一个area
方法,使用的时候,就是像下面这样使用:
fn main() {
let t=triangle{a: 1.0, b: 2.0};
let s=square{a:4.0};
t.area();
s.area();
}
也就是说,只要你有一个新的形状,想要取得这个新的形状变量的面积,我就只能亲自去调用这个函数。
这样做当然可以,但却不具有通用性,正如前面所说的println
这个宏,这是官方写好的,官方怎么知道我们写的结构体是什么呢?这就导致代码缺少通用性。
而如果使用了trait
后,我们就可以将上面的代码改为如下:
trait Shape {
fn area(&self) -> f32{
return 0.0;
} //该函数实现可写可不写,如果不写,那么实现该Trait的结构就必须写,如果这里写了,那么后面实现该trait的结构就可以不写
fn test(){
println!("不写self参数,则只能通过 :: 的方式进行调用");
}
}
struct triangle{ //为了简单,假设其是直角三角形,存放两个直角边
a: f32,
b: f32,
}
impl Shape for triangle { //为三角形实现Shape
fn area(&self) -> f32 {
return (self.a*self.b)/2.0;
}
}
struct square{
a: f32
}
impl Shape for square { //为正方形实现Shape
fn area(&self) -> f32 {
return self.a*self.a;
}
}
此时代码看起来更加复杂了,因为我新加了一个叫做Shape
的trait
,在这个trait
中我写了两个函数:area
与test
其中area函数的参数带有self
,也就是要与具体的结构体对应,调用的时候要用.
的方式。
这与为结构体绑定方法的逻辑基本一致,唯一不同的是,这里你可以实现这个函数,也可以不实现。
如果不实现,那么后面的结构体就必须实现,而如果实现了,后面结构体可以不实现,那么调用这个函数的时候就会调用trait
本身这个函数,如果也实现了,就会覆盖trait
内部的函数代码,调用它自己实现的代码。
调用方法如下:
fn main() {
let t=triangle{a: 1.0, b: 2.0};
let s=square{a:4.0};
//调用带有self参数的函数
t.area();
s.area();
//调用没有self参数的函数
triangle::test();
square::test();
}
如果仅仅是这样看的话,它与结构体的方法没有什么区别,所以我们还需要改变一下:
fn main() {
//省略其它代码
//调用带有self参数的函数
test_area(&t);
test_area(&s);
}
fn test_area(shape: &impl Shape){
shape.area();
}
这次,我用一个函数来调用输出其各种形状内部的area
函数,这个函数的参数为 &impl Shape
,意思是:接受实现了这个Shape
trait的结构体的引用。
这是不是就和println!
宏非常像了!
现在只要你的结构体实现了这个Shape
的trait,那么我就能用一个统一的方法来输出你的内容!
当然,这并不是它的全部意义,另一个意义就在于:方便管理。
因为这些形状都是实现的同一个trait
,共用的一个函数名称,无论是增删改查,都便于管理。
四、常见trait用法
1.用宏简化代码
从上面我们实现了一下Display
这个trait的过程中,我们可以发现这个过程其实是很无聊的,但你在代码中却不得不反复的书写它。