Rust:Trait 抽象接口 特征约束
Rust:Trait 抽象接口 & 特征约束
- 抽象接口
- 定义与实现
- 默认实现
- 泛型 Trait
- 关联类型
- 关联常量
- Trait 继承
- 完全限定语法
- 孤儿规则
- NewType 模式
- 特征约束
- where 子句
- Nominal Typing
Trait 是 Rust 最强大的特性之一,既是类型系统的支柱,也是泛型约束的核心机制。它大致有四大功能:抽象接口、泛型约束、抽象类型、标签。本博客聚焦于前两点。
Trait的含义是特征,它是类型所具有特征。你可以理解为,它是某些类型所具有的能力。比如说两个i32类型之间具有做加法的能力,实际上是具有Add这个Trait。但是两个char不能做加法,就说明这个类型不具有加法的能力,实际上是char没有实现Add这个Trait。
现在也许你有一些初印象了,Trait表达的是一个类型具有的某些能力。
抽象接口
抽象接口是Trait的最基础的用法,它的特点如下:
- 接口中可以定义方法,并支持默认实现
- 接口中不能实现另一个接口,但接口之间可以继承
- 同一个接口可以被多个类型实现,但不能被一个类型实现多次
- 使用
impl关键字为类型实现接口方法 - 使用
trait关键字定义接口
接下来我们会一点一点讲解以上内容,构建起这个Trait的知识体系。
定义与实现
Trait定义了一组可以被共享的行为,只要实现了Trait,该类型就能使用这组行为。
- 定义
Trait:
trait t_name {fn func_1(args...) -> ret;fn func_2(&self, args..) -> ret;
}
使用trait关键字,可以定义一个Trait。在它的名字后面,使用{}定义这个Trait所具有的方法。在定义时,往往只写出函数的签名,并且允许使用Self作为第一个参数,函数体暂时用;代替,表示还未实现。
- 实现
Trait:
impl t_name for type {fn func_1(args...) -> ret {// ...}fn func_2(&self, args..) -> ret {// ...}
}
使用impl关键字,可以为指定类型实现指定Trait,例如impl Add for i32就是给i32类型实现Add。在{}中,需要给每一个之前定义的方法做出实现,必须实现所有未提供默认实现的方法。
假设现有两个结构体:
struct Person {name: String,age: u8,can_swim: bool,
}struct Duck {color: String,
}
它们分别表示一个人,以及一只鸭子。Person::can_swim表示这个人是否学会游泳。
随后定义一个Trait,表示一个类型是否有游泳的能力:
trait Swim {fn swim(&self);
}
分别为Duck和Person实现这个Swim的特征:
impl Swim for Duck {fn swim(&self) {println!("I was born to swim.");}
}impl Swim for Person {fn swim(&self) {if self.can_swim {println!("I've learned to swim.");} else {println!("I can't swim.");}}
}
对鸭子来说,天生就会游泳,而人类需要通过后天学习。
let p = Person {name: "zhangsan".to_string(),age: 28,can_swim: true,
};let d = Duck {color: "yellow".to_string(),
};p.swim();
d.swim();
最后,不论是Person还是Duck类型,都可以去调用swim这个方法,并且最后执行了不同的函数逻辑。
默认实现
Trait 可以提供默认实现,这样实现者可以直接使用,也可以选择覆盖。
trait Swim {fn swim(&self) {println!("I was born to swim.");}
}
在定义Swim这个Trait的时候,可以直接为其默认实现,后续实现该Trait的类型,可以选择不实现这个方法,从而使用默认实现。
比如此时Duck类型就可以直接使用默认实现:
impl Swim for Duck { }impl Swim for Person {fn swim(&self) {if self.can_swim {println!("I've learned to swim.");} else {println!("I can't swim.");}}
}
在impl Swim for Duck的时候,没有实现任何方法,使用了默认实现。
泛型 Trait
在Trait中,支持使用泛型,语法如下:
trait t_name<T, ...> {
}impl<T, ...> t_name<T, ...> for type {
}
只需要在名称后面添加泛型声明列表,后续在实现中就可以使用。
例如实现一个MySwap:
trait MySwap<RHS> {fn my_swap(self, other: RHS) -> (RHS, Self);
}impl<RHS> MySwap<RHS> for i32 {fn my_swap(self, other: RHS) -> (RHS, Self) {(other, self)}
}
此处的RHS是一个泛型,表示右操作数Right Hand Side。与基本的泛型语法相同,在impl时需要先impl<>声明泛型,后续才能使用。
要注意的是:当模板参数不同,MySwap是不同的Trait,例如MySwap<i32>与MySwap<i64>是两个不同的Trait实现,这是通过单态化实现的。
关联类型
假设现在我们要实现一个MyAdd,它表示加法操作。
trait MyAdd<RHS, Output> {fn my_add(self, rhs: RHS) -> Output;
}
它涉及两个泛型参数,RHS表示右操作数,Output表示最终返回值。分开两个泛型参数是因为不同情况下所需的返回值可能不同,比如说 i64 + i32和i32 + i64最好返回i64,才能防止溢出。而前者Self = i32、RHS = i64,后者相反。你很难把这个返回值固定下来,以适配所有情况,所以把返回值单独做成了一个泛型。
实现这个Trait:
// i32 + i32
impl MyAdd<i32, i32> for i32 {fn my_add(self, other: i32) -> i32 {self + other}
}// i32 + i64
impl MyAdd<i64, i64> for i32 {fn my_add(self, other: i64) -> i64 {self as i64 + other}
}// i64 + i32
impl MyAdd<i32, i64> for i64 {fn my_add(self, other: i32) -> i64 {self + other as i64}
}
现在就可以正常调用了:
let i3: i32 = 10;let i6: i64 = 10;let r1 = i3.my_add(i3);let r2 = i3.my_add(i6);let r3 = i6.my_add(i3);
现在看来一切正常,这个方案确实可以实现我们的目的,在不同情况下根据两个操作数决定加法返回类型。
但有一个问题是,两个类型相加,那么返回类型也应该是固定的,但这个实现方案,允许已知的两个操作数返回不定的类型。
例如:
// i32 + i32
impl MyAdd<i32, i32> for i32 {fn my_add(self, other: i32) -> i32 {self + other}
}// i32 + i32
impl MyAdd<i32, i64> for i32 {fn my_add(self, other: i32) -> i64 {(self + other) as i64}
}
以上代码,把i32 + i32依据返回值不同,实现了两个返回版本,这就进一步导致两个确定类型的加法,允许返回一个不确定类型。
你可以利用返回值自动推导泛型参数,来调用不同版本:
let i3: i32 = 10;
let r4: i32 = i3.my_add(i3);
let r5: i64 = i3.my_add(i3);
以上代码中,r4调用的是返回i32的版本,而r5调用的是返回i64的版本。因为泛型是调用层来决定调用哪一个版本的。
我们希望的是,当Self、RHS已知,那么Output就是固定的,这才符合Rust对类型系统的安全性要求。也就是说我们希望Self和RHS是可变的,这两个固定则Output固定,但目前Self、RHS、Output都是可变的。
此时就需要引入关联类型。
关联类型允许为不同类型实现Trait时,分别指定一个类型进行关联。
- 在定义时,通过
type关键字定义一个关联类型:
trait MyAdd<RHS> {type Output;fn my_add(self, rhs: RHS) -> Self::Output;
}
此处的Output就是一个关联类型,在定义时,关联类型相当于一个泛型,它的类型是不确定的。
- 在实现时,需要给关联类型指定一个具体的类型:
// i32 + i32
impl MyAdd<i32> for i32 {type Output = i32;fn my_add(self, other: i32) -> Self::Output {self + other}
}// i32 + i64
impl MyAdd<i64> for i32 {type Output = i64;fn my_add(self, other: i64) -> Self::Output {self as i64 + other}
}// i64 + i32
impl MyAdd<i32> for i64 {type Output = i64;fn my_add(self, other: i32) -> Self::Output {self + other as i64}
}
在Self = i32、RHS = i32时,则Output = i32,后两个同理。
这就是关联类型的作用:当Trait确定,实现该Trait的类型也确定,则关联类型是确定的。
要注意的是,前两个实现中,都是i32实现MyAdd,但是它们实现的不是同一个Trait,前者是MyAdd<i32>,后者是MyAdd<i64>,所以两者的Output允许不同。
可以通过下图来理解此处的逻辑:

左侧是Output作为泛型的版本,右侧是Output作为关联类型的版本。整个Trait分为三个步骤,定义,实现,调用。
在泛型中,泛型参数的取值是在调用时决定的(左侧绿色部分),用户可以自由指定RHS和Output的类型,如果有匹配的实现,那么就调用成功。用户传入确定的Self和RHS,依然可以进一步自行决定Output。
而在关联类型中,关联类型的取值是在实现时决定的(右侧绿色部分),那么用户传入确定的Self和RHS后,一定得到的是确定的Output。
两者的核心区别在于,确定这个Output类型的时机不同(绿色部分),一个在用户调用时确定,另一个在实现时就已经确定了。
在实际应用中,如果你确实希望用户可以自行决定这个类型,那么将它作为泛型。如果你希望当Self和Trait已经确定时,某个类型也得到一个确定值,那就将它作为关联类型。
最后,在定义Trait时,可以设置关联类型的默认值:
trait MyAdd<RHS> {type Output = Self;fn my_add(self, rhs: RHS) -> Self::Output;
}
这个特性需要较nightly版本的Rust支持,截止1.90.0依然不稳定。
关联常量
除了关联类型,Trait 还可以定义关联常量,此处的关联常量和之前在impl时讲的关联常量是一样的。
trait Limit {const MAX: u32;
}struct Counter;
impl Limit for Counter {const MAX: u32 = 100;
}
与关联类型同样的是,关联常量在定义Trait阶段定义,在实现阶段确定值。
trait Limit {const MAX: u32 = 100;
}
关联常量可以给一个默认值,并且已经是一个稳定特性了,可以直接使用。
Trait 继承
Rust不支持面向对象中的类型继承,但是在Trait之间允许继承。
语法:
trait t_name: t1 + t2 ... {
}
定义Trait时,在名称后面使用:指明要继承的其它Trait,多个Trait之间使用+分隔。
此处继承的含义为:如果某个类型要实现该Trait,必须实现它继承的所有Trait。
示例:
trait Introduce: Display {fn introduce(&self) {println!("我是:{}", self);}
}
此处的 Introduce 用于自我介绍,它要求类型必须实现Display这个类型,才能保证print的时候不报错。
struct Cat {name: String,
}impl Display for Cat {fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {write!(f, "猫咪 {}", self.name)}
}impl Introduce for Cat {}let my_cat = Cat { name: "咪咪".to_string() };
my_cat.introduce();
在这个Cat类中,首先实现Display,然后才能实现Introduce,并且使用了默认实现。最后my_cat这个实例就可以调用introduce方法。
其实此处的Trait继承与面向对象中的继承区别还是很大的,它更多的是表示一种接口之间的依赖关系,比如introduce依赖Display。
完全限定语法
一个类型的多个Trait是可以分别自由实现方法的,那么也就允许多个Trait中出现同名方法。而当多个 Trait 或者类型本身impl的方法名发生冲突时,编译器就会陷入困境。此时就需要 完全限定语法 (Fully Qualified Syntax)来避免歧义。
Rust 的方法调用有一个推导的过程:
- 编译器会先在当前类型的固有方法里查找,即
impl的内容 - 如果没找到,再去该类型实现的所有
Trait中查找 - 如果存在多个候选,就会报错,提示你需要显式指定
完全限定语法如下:
<Type as Trait>::method(args...)
Type 表示具体的类型,Trait 表示方法来源的Trait,method 是要调用的方法名,args... 是额外的参数。
假设我们有两个 Trait,它们都定义了一个同名方法:
trait Pilot {fn fly(&self);
}trait Wizard {fn fly(&self);
}struct Human;impl Pilot for Human {fn fly(&self) {println!("Pilot flying the plane!");}
}impl Wizard for Human {fn fly(&self) {println!("Wizard flying with magic!");}
}impl Human {fn fly(&self) {println!("Human flapping arms... not very effective.");}
}
此时 Human 类型同时具备三种 fly 方法:
- 自身固有方法
Human::fly - 来自
Pilot的fly - 来自
Wizard的fly
如果直接调用:
let h = Human;
h.fly();
编译器会优先选择固有方法,因此输出:
Human flapping arms... not very effective.
但如果我们想调用 Pilot 或 Wizard 的版本,就必须使用完全限定语法:
Pilot::fly(&h); // 等价于 <Human as Pilot>::fly(&h)
Wizard::fly(&h); // 等价于 <Human as Wizard>::fly(&h)
<Human as Pilot>::fly(&h); // 更显式的写法
此处由于第一个参数是self,所以要传入&h作为参数。通过完全限定语法就能明确告诉编译器要调用的是哪个 Trait 的实现。
孤儿规则
基于impl和trait两个特性,你可以很轻易的给一个类型添加各种方法,这就有可能导致一些不太安全的操作。
比如你的某位同事,已经为一个类型封装好了它的各类接口和Trait。但是你使用这个类型前,又对它的这个Trait进行了实现,导致篡改了某些该类型原本的行为,这就是一种破坏性的改写,可能导致难以预料的Bug,孤儿规则可以避免类似的情况。
如果要实现某个
Trait,那么该Trait和要实现这个Trait的类型,至少有一个要在当前Crate中定义
此处的Crate可以理解为一个库,比如std标准库算一个Crate,这个内容会在后续深入讲解。
- 尝试给
Vec<i32>实现std::fmt::Display:
Vec<i32>是标准库中的一个动态数组类型,而std::fmt::Display是一个标准库的Trait,实现后可以被print输出。
impl std::fmt::Display for Vec<i32> {fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {write!(f, "My custom Vec: {:?}", self)}
}
以上代码尝试给Vec<i32>实现std::fmt::Display,但是这个代码会报错,因为Vec<i32>这个类型不属于本地,而std::fmt::Display也不属于本地,这违背了孤儿规则,编译不通过。
- 给标准库类型实现本地
Trait
trait MyTrait {fn my_method(&self);
}impl MyTrait for Vec<i32> {fn my_method(&self) {println!("Vec length: {}", self.len());}
}
以上代码给Vec<i32>实现MyTrait,这个代码是合法的,因为MyTrait是本地的,符合孤儿规则。
- 给本地类型实现标准库
Trait
struct MyStruct;impl std::fmt::Display for MyStruct {fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {write!(f, "This is MyStruct")}
}let my_instance = MyStruct;
println!("{}", my_instance);
此处的 MyStruct 是自己定义的类型,std::fmt::Display是之前提到的标准库Trait。实现这个Trait后,直接就可以通过print输出结构体。以上过程也是正确的,因为类型是本地的,符合孤儿规则。
NewType 模式
在介绍孤儿规则时我们提到:你不能为外部类型实现外部 trait。这条规则保证了编译器在全局范围内的一致性,但在工程实践中也经常让人卡壳。比如:
- 你想为
String实现某个第三方库的trait - 或者你想为
Vec<T>增加一个外部 trait 的实现
那么该怎么办?Rust 社区的惯用解法就是 Newtype 模式。
所谓 Newtype,就是用一个新的元组结构体把原有类型“包裹”起来:
struct MyString(String);
这样一来,MyString 是你自己定义的本地类型,本地类型自然可以实现任何外部 trait。
假设我们想为 String 实现一个外部库的 Display:
use std::fmt::{self, Display, Formatter};struct MyString(String);impl Display for MyString {fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {write!(f, "MyString says: {}", self.0)}
}fn main() {let s = MyString("hello".to_string());println!("{}", s);
}
输出:
MyString says: hello
这里的关键点在于:String 是外部类型,而Display 是外部 trait。直接 impl Display for String 会违反孤儿规则,但 MyString 是我们自己定义的本地类型,所以 impl Display for MyString 完全合法。
特征约束
在之前讲解过的所有泛型,都是不受约束的泛型,它们可以是任意类型。但这种情况其实反而是少数,在Rust中,大部分泛型都是收到约束的泛型。对泛型的约束,叫做泛型约束,而特征约束(Trait Bounds)属于泛型约束的一种。
特征约束语法如下:
T: Trait_1 + Trait2 + ...
在进行泛型声明时,可以在对应的泛型后面使用:对这个泛型进行约束,只有实现某些Trait的泛型,才能传入。泛型声明主要出现在函数、方法、复合类型、泛型Trait中,接下来一个一个尝试。
- 函数中的特征约束
fn get_max<T: std::cmp::PartialOrd>(a: T, b: T) -> T {if a > b {a} else {b}
}
函数 get_max 用于返回最大值,它接受一个泛型T。但不是所有类型都可以使用>进行比大小的,只有实现了 std::cmp::PartialOrd 这个Trait的类型才能直接比大小,因此对T进行泛型约束。
- 方法中的特征约束
struct Point<T> {x: T,y: T,
}impl<T: std::fmt::Display> Point<T> {fn show(&self) {println!("x = {}, y = {}", self.x, self.y);}
}impl<T: std::ops::Add<Output = T> + Copy> Point<T> {fn add(&self, other: &Point<T>) -> Point<T> {Point {x: self.x + other.x,y: self.y + other.y,}}
}
以上代码,定义了一个Point类,它表示一个二维坐标,并分别实现了add和show方法。
对于show来说,在impl时通过特征约束,约束了T必须实现Display,这样x和y才能正常print输出。
对于add来说,它要求T必须实现Add和Copy。Add<Output = T>要求T必须可以进行加法,而且加法返回值是T,此处<Output = T>是对关联类型的限制,也就是说特征约束还可以限制关联类型。而Copy要求T可以进行拷贝操作,多个约束之间使用+连接。Copy涉及到所有权,会在后面深入了解,但是现在也可以简单讲一讲。
在Add这个Trait中,内部的add方法决定了是否可以用+这个操作符,这个方法的第一个参数是self,而不是一个借用。因此 self.x + other.x 的时候,必须对这两个值进行一次拷贝,那就需要实现Copy这个Trait了。
当使用Point的时候,会根据不同的类型,决定它可以调用哪些方法。
比如i32满足以上所有约束,就可以调用add和show方法:
let mut p1 = Point { x: 5, y: 10 };
let mut p2 = Point { x: 50, y: 100 };
p1.show();
p2.add(&p1);
p2.show();
但是对于String来说,它只实现了Display,没有实现Add<Output = String>和Copy,就只能调用show而不能调用add:
let p_str= Point { x: String::from("hello"), y: String::from("world") };
p_str.show(); // 可以正常调用 show
在之前的博客提到过,一个类型可以有多个impl,多个impl结合特征约束,可以实现不同的类型拥有不同的方法。
- 类型中的特征约束
struct Holder<T: std::fmt::Display> {value: T,
}impl<T: std::fmt::Display> Holder<T> {pub fn print_value(&self) {println!("我持有的值是: {}", self.value);}
}
以上代码中,将特征约束放在了定义类型时,此时只有实现了Display的类型才能作为Holder::value。
但是在定义类型时进行了约束,不代表impl的时候可以省略这个约束,在impl时还是需要写出T的特征约束。这个特性用的比较少,一般不会在类型使用特征约束,而是在impl层面使用。
- 泛型
Trait中的特征约束
Trait 本身也可以是泛型 Trait,也就是说,一个 Trait 的定义可以引入泛型参数。这时如果你希望限制这些泛型参数的范围,就需要为它们添加特征约束。
trait Summary<T: Display> {fn summarize(&self, item: T) -> String;
}struct News;impl Summary<String> for News {fn summarize(&self, item: String) -> String {format!("新闻摘要:{}", item)}
}
以上代码中,Summary是一个泛型Trait,它要求T实现了Display。后续别的类型实现这个Trait的时候,保证参数item是可以直接被输出的。
- 关联类型中的特征约束
除去以上使用泛型的位置,关联类型也是可以被特征约束的。
trait Container {type Item: Display;
}
以上代码中,Container内部有一个关联类型Item,并通过特征约束要求其实现Display,后续impl的时候,具体的Item类型就必须是实现了Display的类型。
最后,可以回想一下Trait继承语法,它也是通过:对Trait进行限定,使用+隔开多个Trait。这两个语法很相似,实际上Trait继承本质上也是一个特征约束,只是有部分人把它当做一种继承,这种说法也被社区接受了。
where 子句
当特征约束变得复杂时,使用 : 语法会让代码可读性变差,特别是当泛型参数较多或多个特征约束组合时。
比如说刚才的:impl<T: std::ops::Add<Output = T> + Copy> Point<T>,这里仅仅涉及到两个Trait,就已经十分难以辨别了。
为此,Rust 提供了 where 子句来更清晰地组织约束。
语法如下:
// 定义类型时
struct MyType<T, U>
whereT: Trait_1 + Trait_2 ... ,U: Trait_3 + Trait_4 ... ,
{}// 定义方法时
impl<T, U> MyType<T, U>
whereT: Trait_1 + Trait_2 ... ,U: Trait_3 + Trait_4 ... ,
{}// 定义函数时
fn func<T>() -> T
whereT: Trait_1 + Trait_2 ... ,
{}// Trait 继承时
trait Mytrait
whereSelf: Trait_1 + Trait_2 ... ,
{type Item where Self::Item: Trait_1; // 关联类型
}
当需要使用特征约束时,可以用where子句语法代替原本的:语法。以上示例展示了四种情况,分别是定义类型、定义方法、定义函数、Trait 继承。但其实它们不用分开记,它们有统一的特点:where子句直接写在{}前面。
使用where定义一个子句,子句内部可以对所有之前声明过的泛型进行特征约束,多个泛型之间用,逗号隔开。对于每个泛型使用:语法表示特征约束。
另外的,关联类型特征约束时也是可以使用where子句的。当Trait继承时,受到约束的类型是Self。而在关联类型中使用where时,必须使用Self::做前缀。
实际上对于关联类型和Trait继承,不使用where子句反而更简洁,这个语法更多的用于有多个泛型的情况下,分别把每个泛型的特征约束列举出来。
Nominal Typing
学完Trait限定,不知道你有没有感觉它有点像鸭子类型。如果你没听过 Duck Typing,简而言之就是一句话:
“如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。”*
在编程语言里,这意味着:只要一个对象拥有某些方法,就可以被当作某个类型来使用,而不需要显式声明它属于这个类型。
比如在 Python 里,你写一个函数 make_it_quack(x),只要传入的对象有 quack() 方法,就能正常运行,根本不管它是什么类型。这就是典型的 Duck Typing,特点是灵活,在运行时对方法进行检查。
而在类似于C++这样的语言,一个函数写出来后,每个参数的类型都必须是写死的。它不以这个类型的功能为依据,而是以类型本身作为依据。
打个比方:某个公司招聘软件工程师。
鸭子类型的逻辑:公司要求受聘者可以进行软件开发。不管你曾经是什么专业,你可以学数学,可以学汉语言文学,甚至哪怕你不是一个人,你是一个会敲代码的猴子。只要你会软件开发,你都可以来面试。但是面试前谁知道你会不会敲代码?因此只能面试过程中对你提问,这就有可能导致面试过程中才发现这个人根本不会敲代码,浪费了时间。
非鸭子类型的逻辑:公司要求受聘者是软件工程专业毕业生。公司不以你是否可以敲代码为依据,而是要求你必须就是该专业的人。这样招募进来的人一定是会敲代码的,但是也会错失某些优秀人才。
因此鸭子类型和非鸭子类型的区别就体现出来了。鸭子类型下更加灵活,但是在运行过程中才能检查出问题,导致安全性和效率降低。而非鸭子类型,在起初就限制好了类型,只要你能通过类型检查,那就保证一定可以完成函数内部的操作,提高了安全性,但也降低了灵活性。
Rust 的特征约束看起来是不是有点像 Duck Typing?
当我们写下:
fn make_it_swim<T: Swim>(x: T) {x.swim();
}
这段代码的语义就是:只要能游泳的类型都能传进来。是不是很像Duck Typing?
但关键的不同在于:Rust 会在编译期就检查 T 是否真的实现了 Swim。换句话说,Rust 提供了 Duck Typing 的表达力,却把它变成了静态 Duck Typing。
这背后体现的是 Rust 的设计思想:
- 它不是动态语言的
Duck Typing,它不会等到运行时才发现对象不会叫。 - 采用的是
Nominal Typing(名义类型):只有当你显式impl Trait for Type时,编译器才承认这个类型具备某个能力。
名义类型的逻辑:公司招募要求用户必须完成一场笔试,笔试通过才有面试机会,而且任何专业的人都可以笔试。这样既可以招募到各式各样的人才,又保证了参与面试的人一定具有开发能力。
而这个笔试的过程,就是Rust中的impl for。
这种方案既保证了安全性,而且在编译期完成检查,对运行时没有任何效率影响,这也实现了Rust最注重的 效率 + 安全。
