Rust 的 trait 是个啥子?
作者:@古明地盆
喜欢这篇文章的话,就点个关注吧,或者关注一下我的公众号也可以,会持续分享高质量Python文章,以及其它相关内容。:点击查看公众号
楔子
前面我们提到过 trait,那么 trait 是啥呢?先来看个例子:
#[derive(Debug)]
struct Point<T> {
x: T,
}
impl<T> Point<T> {
fn m(&self) {
let val = self.x;
}
}
fn main() {
let p = Point { x: 123 };
}
你觉得这段代码有问题吗?如果上一篇文章你还有印象的话,那么会很快发现是有问题的。首先结构体在赋值给其它变量的时候会转移所有权,因此为了让结构体实例在调用完方法后依然保持有效,我们会将方法里的 self 声明为引用,即 &self。这样的话,结构体调用方法时实际是传了一个引用过去(如果希望可以修改成员,那么就声明为 &mut self),在方法调用完毕之后,结构体实例依旧保持有效,也意味着实例的所有成员值都保持有效。
但在方法 m 里面,我们将成员 x 的值赋给了变量 val。如果成员 x 的类型不是可 Copy 的,也就是数据不全在栈上,还涉及到堆,那么就会转移所有权,因为 Rust 默认不会拷贝堆数据。所以调用完方法 m 之后,成员 x 的值不再有效,进而使得结构体不再有效。
所以 Rust 为了避免这一点,在赋值的时候强制要求 self.x 的类型必须是可 Copy 的,但泛型 T 可以代表任意类型,它不满足这一特性。或者说 T 最终代表的类型是不是可 Copy 的,Rust 是不知道的,所以 Rust 干脆认为它不是可 Copy 的。
那么问题来了,虽然 T 可以代表任意类型,但如果我们赋的值决定了 T 代表的类型一定是可 Copy 的,那么可不可以告诉 Rust,让编译器按照可 Copy 的类型来处理呢?答案是可以的,而实现这一功能的机制就叫做 trait。
什么是 trait
trait 类似于 Go 里面的接口,相当于告诉编译器,某种类型具有哪些可以与其它类型共享的功能。
#[derive(Debug)]
struct Girl {
name: String,
age: i32
}
// trait 类似 Go 里面的接口,然后里面可以定义一系列的方法
// 这里我们创建了一个名为 Summary 的 trait
// 并在内部定义了一个 summary 方法
trait Summary {
// trait 里面的方法只需要写声明即可
fn summary(&self) -> String;
}
// Go 里面只要实现了接口里面的方法,便实现了该接口
// 但是在 Rust 里面必须显式地指明实现了哪一个 trait
// impl Summary for Girl 表示为类型 Girl 实现 Summary 这个 trait
impl Summary for Girl {
fn summary(&self) -> String {
// format! 宏用于拼接字符串,它的语法和 println! 一样
// 并且这两个宏都不会获取参数的所有权
// 比如这里的 self.name,format! 拿到的只是引用
format!("name: {}, age: {}", self.name, self.age)
}
}
fn main() {
let g = Girl{name: String::from("satori"), age: 16};
println!("{}", g.summary()); // name: satori, age: 16
}
所以 trait 里面的方法只需要写上声明即可,实现交给具体的结构体来做。当然啦,trait 里面的方法也是可以有默认实现的。
[derive(Debug)]
struct Girl {
name: String,
age: i32
}
trait Summary {
// 我们给方法指定了具体实现
fn summary(&self) -> String {
String::from("hello")
}
}
impl Summary for Girl {
// 如果要为类型实现 trait,那么要实现 trait 里面所有的方法
// 这一点和 Go 的接口是相似的,但 Go 里面实现接口是隐式的
// 只要你实现了某个接口所有的方法,那么默认就实现了该接口
// 但在 Rust 里面,必须要显式地指定实现了哪个 trait
// 同时还要实现该 trait 里的所有方法
// 但 Rust 的 trait 有一点特殊,Go 接口里面的方法只能是定义
// 而 trait 里面除了定义之外,也可以有具体的实现
// 如果 trait 内部已经实现了,那么这里就可以不用实现
// 不实现的话则用 trait 的默认实现,实现了则调用我们实现的
// 因此这里不需要定义任何的方法,它依旧实现了 Summary 这个 trait
// 只是我们仍然要通过 impl Summary for Girl 显式地告诉 Rust
// 如果只写 impl Girl,那么 Rust 则不认为我们实现了该 trait
}
fn main() {
let g = Girl{name: String::from("satori"), age: 16};
// 虽然没有 summary 方法,但因为实现了 Summary 这个 trait
// 而 trait 内部有 summary 的具体实现,所以不会报错
// 但如果 trait 里面的方法只有声明没有实现,那么就必须要我们手动实现了
println!("{}", g.summary()); // hello
}
总结一下就是 trait 里面可以有很多的方法,这个方法可以只有声明,也可以同时包含实现。如果要为类型实现某个 trait,那么要通过 impl xxx for
进行指定,并且实现该 trait 内部定义的所有方法。但如果 trait 的某个方法已经包含了具体实现,那么我们也可以不实现,会使用 trait 的默认实现。
trait 作为参数
到目前为止,我们并没有看到 trait 的实际用途,但相信你也能猜出来它是做什么的。假设有一个函数,只要是实现了 info 方法的结构体实例,都可以作为参数传递进去,这时候应该怎么做呢?
struct Girl {
name: String,
age: i32,
}
struct Boy {
name: String,
age: i32,
salary: u32,
}
trait People {
fn info(&self) -> String;
}
// 为 Girl 和 Boy 实现 People 这个 trait
impl People for Girl {
fn info(&self) -> String {
format!("{} {}", &self.name, self.age)
}
}
impl People for Boy {
fn info(&self) -> String {
format!("{} {} {}", &self.name, self.age, self.salary)
}
}
// 定义一个函数,注意参数 p 的类型
// 如果是 p: xxx,则表示参数 p 的类型为 xxx
// 如果是 p: impl xxx,则表示参数 p 的类型任意,只要实现了 xxx 这个 trait 即可
fn get_info(p: impl People) -> String {
p.info()
}
fn main() {
let g = Girl {
name: String::from("satori"),
age: 16,
};
let b = Boy {
name: String::from("可怜的我"),
age: 26,
salary: 3000,
};
// 只要实现了 People 这个 trait
// 那么实例都可以作为参数传递给 get_info
println!("{}", get_info(g)); // satori 16
println!("{}", get_info(b)); // 可怜的我 26 3000
}
然后以 trait 作为参数的时候,还有另外一种写法:
// 如果是 <T> 的话,那么 T 表示泛型,可以代表任意类型
// 但这里是 <T: People>,那么就不能表示任意类型了
// 它表示的应该是实现了 People 这个 trait 的任意类型
fn get_info<T: People>(p: T) -> String {
p.info()
}
以上两种写法是等价的,但是第二种写法在参数比较多的时候,可以简化长度。
fn get_info<T: People>(p1: T, p2: T) -> String {
}
// 否则话要这么写
fn get_info(p1: impl People, p2: impl People) -> String {
}
当然啦,一个类型并不仅仅可以实现一个 trait,而是可以实现任意多个 trait。
struct Girl {
name: String,
age: i32,
gender: String
}
trait People {
fn info(&self) -> String;
}
trait Female {
fn info(&self) -> String;
}
// 不同的 trait 内部可以有相同的方法
impl People for Girl {
fn info(&self) -> String {
format!("{} {}", &self.name, self.age)
}
}
impl Female for Girl {
fn info(&self) -> String {
format!("{} {} {}", &self.name, self.age, self.gender)
}
}
// 这里在 impl People 前面加上了一个 &
// 表示调用的时候传递的是引用
fn get_info1(p: &impl People) {
println!("{}", p.info())
}
fn get_info2<T: Female>(f: &T) {
println!("{}", f.info())
}
fn main() {
let g = Girl {
name: String::from("satori"),
age: 16,
gender: String::from("female")
};
get_info1(&g); // satori 16
get_info2(&g); // satori 16 female
}
不同 trait 内部的方法可以相同也可以不同,而 Girl 同时实现了 People 和 Female 两个 trait,所以它可以传递给 get_info1,也可以传递给 get_info2。然后为 trait 实现了哪个方法,就调用哪个方法,所以两者的打印结果不一样。
那么问题来了,如果我在定义函数的时候,要求某个参数同时实现以上两个 trait,该怎么做呢?
// 我们只需要使用 + 即可
// 表示参数 p 的类型必须同时实现 People 和 Female 两个 trait
fn get_info1(p: impl People + Female) {
// 但由于 Poeple 和 Female 里面都有 info 方法
// 此时就不能使用 p.info() 了,这样 Rust 不知道该使用哪一个
// 应该采用下面这种做法,此时需要手动将引用传过去
People::info(&p);
Female::info(&p);
}
// 如果想接收引用的话,那么需要这么声明
// 因为优先级的原因,需要将 impl People + Female 整体括起来
fn get_info2(p: &(impl People + Female)) {}
// 或者使用类型泛型的写法
fn get_info3<T: People + Female>(p: T) {}
fn get_info4<T: People + Female>(p: &T) {}
最后还有一个更加优雅的写法:
// 显然这种声明方式要更加优雅
// 如果没有 where 的话,那么这个 T 就是可以代表任意类型的泛型
// 但这里出现了 where,因此 T 就表示实现了 People 和 Female 两个 trait 的任意类型
fn get_info<T>(p: T)
where
T: People + Female
{
}
如果要声明多个实现 trait 的类型,那么使用逗号分隔。
fn get_info<T, W>(p1: T, p2: W)
where
T: People + Female,
W: People + Female
{
}
// 如果换成之前的写法就是
fn get_info2(p1: impl People + Female, p2: impl People + Female) {
}
// 或者
fn get_info3<T: People + Female, W: People + Female>(p1: T, p2: W) {
}
// 当然这里只是举例说明,既然都是 People + Female,那么没必要使用两个泛型
// 但很明显,这种情况下使用 where 子句是最清晰和简洁的
可以看出,Rust 的语法表达能力还是挺丰富的。
trait 作为返回值
trait 也是可以作为返回值的。
struct Girl {
name: String,
age: i32,
gender: String,
}
trait People {
fn info(&self) -> String;
}
impl People for Girl {
fn info(&self) -> String {
format!("{} {}", &self.name, self.age)
}
}
fn init() -> impl People {
Girl {
name: String::from("satori"),
age: 16,
gender: String::from("female"),
}
}
fn main() {
let g = init();
println!("{}", g.info()); // satori 16
}
一个 trait 可以有很多种类型实现,返回任意一个都是可以的。
让方法的第一个参数支持引用
回到我们最开始的例子:
#[derive(Debug)]
struct Point<T> {
x: T,
}
impl<K> Point<K> {
fn m(&self) {
let val = self.x;
}
}
fn main() {
let p = Point { x: 123 };
}
这段代码是有问题的,因为结构体变量在赋值的时候会自动转移所有权。
#[derive(Debug)]
struct Girl {
name: String,
age: i32,
}
fn main() {
let g = Girl{name: String::from("satori"), age: 17};
let g2 = g; // 从此处开始,变量 g 也不再有效
}
调用方法也是,如果第一个参数不是引用 &self,而是 self,那么调用完方法之后,结构体实例就没法用了,因为所有权被转移了。所以为了避免这一点,我们会选择将第一个参数声明为 &self,在调用方法时将自身的引用传递给 self。由于传递引用不会失去所有权,因此调用完方法之后,结构体实例依旧有效,这也是我们的目的所在。
但在上面的例子中,调用方法时执行了一个赋值语句 let val = self.x,如果 self.x 是可 Copy 的(数据全部在栈上),那么没有任何问题。但如果不是可 Copy 的(数据涉及到堆),那么由于 Rust 默认不会拷贝堆上数据,于是只能转移所有权。可所有权一旦转移,那么结构体实例的成员 x 将不再有效,进而导致结构体实例不再有效。
因为结构体如果有效,那么它里面的每一个成员也都必须有效,举个例子:
#[derive(Debug)]
struct Girl {
name: String,
age: i32,
}
fn main() {
let g = Girl{name: String::from("satori"), age: 17};
// 结构体内的 name 成员的所有权发生转移,所以 g.name 不再有效,但这行代码本身是合法的
let name = g.name;
// 从此处开始,变量 g 也不再有效,因此代码报错
println!("{:?}", g); // 报错
}
于是一个矛盾就产生了,我们将第一个参数声明引用的目的就是为了让结构体实例在调用完方法之后依旧保持有效,但 let val = self.x 会在成员 x 不可 Copy 时转移所有权,进而导致整个结构体实例都不可用。于是 Rust 要求,这种情况下成员 x 必须是可 Copy 的,否则报错。
struct Point1 {
x: i32,
}
struct Point2 {
x: String,
}
struct Point3<T> {
x: T,
}
// 这段代码不存在任何问题,因为 Point1 的成员 x 的类型是 i32,实现了 Copy trait
// 将 self.x 赋值给变量 val 之后,自身依然有效,因此结构体实例也有效
impl Point1 {
fn m(&self) {
let val = self.x;
}
}
// 这段代码是错误的,因为 Point2 的成员 x 的类型是 String,没有实现 Copy trait
// 换句话说,它的数据存在堆上,栈上的指针指向堆区的内存。赋值给 val 之后,所有权会转移给 val
// 而所有权一旦转移,那么 self.x 就不再有效了,而结构体实例有效的前提是所有成员都有效
// 因此调用完方法后,结构体实例就无效了,但我们声明为引用的目的就是希望它调用完方法仍然保持有效
// 所以此时是矛盾的,于是 Rust 在该场景下就不再转移所有权了,而是强制要求 self.x 必须可 Copy
// 如果发现它不可 Copy,那么直接报错:提示没有实现 Copy trait,而 String 显然不可 Copy
impl Point2 {
fn m(&self) {
let val = self.x;
}
}
// Point3 的成员 x 的类型是 T,它可以代表类型。可以是实现了 Copy,也可以不实现
// 那么这个时候 Rust 编译器就不知道 T 究竟是否是可 Copy 的
// 既然不知道,那么就默认为不可 Copy,因此这段代码仍然是不对的
impl<K> Point3<K> {
fn m(&self) {
let val = self.x;
}
}
IDE 也会给出提示:
当然啦,对于 Point1 和 Point2 没什么可说的,因为它们的成员 x 的类型是具体的,有没有实现 Copy 一开始就知道。但对于 Point3 来讲,由于 T 是泛型,具体代表啥类型不知道,那么是否实现了 Copy 也不知道,既然不知道,则默认没有实现。但问题来了,如果我们能保证 T 一定是可 Copy 的,可不可以将这一信息告诉 Rust 呢?
struct Point<T> {
x: T,
}
// 既然 T 是泛型,那么定义方法时也必须在结构体后面指定 T 的类型
// 这里表示为 T 是 i32 类型的 Point 实现方法,由于已经明确了 i32
// 那么只有当 x 的类型是 i32 时,才能调用此方法
// 因此调用该方法时,x 的类型是 i32,它是可 Copy 的
impl Point<i32> {
fn m(&self) {
let val = self.x;
}
}
// 但问题来了,可 Copy 的类型有很多,总不能每个类型都单独实现一次吧
// 所以这里还是需要使用泛型,但是可以通过 trait 给泛型施加一些制约
// 这里表示为泛型 K 实现方法,但是给 K 增加了一些约束,它必须实现 Copy trait
// 如果不增加 Copy 这个约束,那么 K 就可以代表任意类型,不管成员 x 的类型是什么,都可以调用 m2 方法
// 但在 let val = self.x 的时候会报错,因为不知道它有没有实现 Copy
// 而现在不一样了,我们通过 K: Copy 给泛型 K 施加了一些制约
// 只有 x 的类型实现了 Copy trait,它才能调用 m2 方法,否则压根就不允许调用
// 既然实现了 Copy,那么此时 Rust 就可以大胆的拷贝了
impl<K: Copy> Point<K> {
fn m2(&self) {
let val = self.x;
}
}
fn main() {
// p1 可以调用 m 方法,也可以调用 m2 方法
let p1 = Point { x: 123 };
// p2 不可以调用 m 方法,因为它是为 i32 实现的
// 但调用 m2 是可以的,因为它是为可 Copy 的类型实现的,因为 f64 显然是可 Copy 的
let p2 = Point { x: 3.14 };
// p3 不可以调用 m 和 m2,因为此时的 T 是 String,它既不是 i32、也不是可 Copy 的
let p3 = Point { x: String::from("satori") };
}
通过这个例子,我们就将整个流程串起来了,当然啦,在设置 trait 的时候还有另一种写法。
impl<K> Point<K>
where K: Copy
{
fn m2(&self) {
let val = self.x;
}
}
在泛型参数较多、或者要实现的 trait 较多时,这种写法更加清晰。
但是注意:下面这种写法则是不允许的。
impl<K> Point<K: impl Copy> {
fn m2(&self) {
}
}
这种写法只能出现在函数的参数中,我们知道给函数参数指定 trait 有三种写法:
// 参数的 p 的类型必须实现了 Copy trait
fn func1(p: impl Copy) {
}
// 参数的 p 的类型是泛型 T,可以代表任意类型
// 但不管哪种类型,都必须实现 Copy trait
fn func2<T: Copy>(p: T) {
}
// 和上面等价,p 可以是实现了 Copy trait 的任意类型
fn func3<T>(p: T) where T: Copy {
}
// PS:如果规定返回值也必须实现 trait,那么要使用 impl
// 比如 fn func2<T: Copy>(p: T) -> impl Copy {} 是合法的
// 但是 fn func2<T: Copy>(p: T) -> T {} 则是不合法的,
如果要为实现指定 trait 的泛型对应的结构体实现方法,那么只能用后两种。
struct Point<T> {
x: T,
}
// 为泛型 K 实现方法,泛型 K 要满足可 Copy
impl<K: Copy> Point<K> {
}
// 和上面等价
impl<K> Point<K> where K: Copy{
}
因此这些语法不要搞混了,以上我们就介绍完了 trait。
实现一个 max 函数
下面让我们实现一个 max 函数,返回数组里面的最大元素,并且支持多种类型比较。不过先假定数据元素是 i32 类型,看看代码怎么编写:
// arr 接收一个数组,我们将它声明为 &[i32]
// 这个声明比较特殊,我们举几个例子解释一下
// arr: [i32;5],表示接收类型为 i32 长度为 5 的静态数组
// arr: Vec<f64>,表示接收类型为 f64 的动态数组,长度不限
/* arr: &[i32],表示接收 i32 类型数组的引用
并且数组可以是动态数组,也可以是静态数组,长度不限
对于当前求最大值来说,我们不应该关注数组是静态的还是动态的
所以应该声明为 &[i32],表示都支持
*/
fn max(arr: &[i32]) -> i32{
if arr.len() == 0 {
panic!("数组为空")
}
// 获取数组的第一个元素,然后和后续元素依次比较
let mut largest = arr[0];
for &item in arr {
if largest < item {
largest = item
}
}
largest
}
fn main() {
let largest = max(&vec![1, 23, 13, 4, 15]);
println!("{}", largest); // 23
}
还是很简单的,但问题来了,如果我希望它除了支持整型数组外,还支持浮点型该怎么办呢?难道再定义一个函数吗?显然这是不现实的,于是我们可以考虑泛型。
fn max<T>(arr: &[T]) -> T {
if arr.len() == 0 {
panic!("数组为空")
}
let mut largest = arr[0];
for &item in arr {
if largest < item {
largest = item
}
}
largest
}
使用泛型的话,代码就是上面这个样子,你觉得代码有问题吗?
不用想,问题大了去了,而原因和结构体的第一个参数被声明为引用类似。首先函数接收的是数组的引用,那么函数调用结束后,数组依旧保持有效,那么数组里面的元素显然也是有效的。但在给 largest 赋值的时候,等号右边是 arr[0]。如果数组里面的元素不是可 Copy 的,那么就会失去所有权,因为 Rust 不会拷贝堆数据,那这样的话数组之后就不能用了。所以这种情况 Rust 要求元素是可 Copy 的,但实际情况是不是呢?Rust 是不知道的,所以会报错,认为不是可 Copy 的,这是第一个错误。
然后是 for &item in arr,这段代码的错误和上面相同,在遍历的时候会依次将元素拷贝一份赋值给 item。但要求拷贝之后彼此互不影响,这就意味着数据必须全部在栈上。但 T 代表啥类型,该类型的数据是否全部在栈上 Rust 是不知道的,于是报错。
第三个错误就是 largest < item,因为这涉及到了比较,但 T 类型的数据能否比较呢?Rust 也是不知道的,所以报错。
因此基于以上原因,如果想让上述代码成立,那么必须对 T 进行一个限制。
fn max<T>(arr: &[T]) -> T
where
// 相当于告诉 Rust 这个 T 是可比较的、可 Copy 的
// 或者说 T 实现了 PartialOrd 和 Copy 这两个 trait
T: PartialOrd + Copy,
{
if arr.len() == 0 {
panic!("数组为空")
}
let mut largest = arr[0];
for &item in arr {
if largest < item {
largest = item
}
}
largest
}
fn main() {
let largest = max(&vec![1, 23, 13, 4, 15]);
println!("{}", largest); // 23
let largest = max(&vec![1.1, 23.1, 13.1, 4.1, 15.1]);
println!("{}", largest); // 23.1
}
以上我们就实现了数组求最大值的逻辑,通过对 T 进行限制,告诉 Rust 泛型 T 代表的类型实现了 PartialOrd 和 Copy 这两个 trait。然后当我们调用的时候,Rust 就会检测类型是否合法:
显然当元素类型为 String 的时候就会报错,因为 Rust 检测到该类型没有实现 Copy 这个 trait。那如果我希望,max 函数也支持 String 类型的数组呢?
fn max<T>(arr: &[T]) -> &T
where
// T 只需要实现 PartialOrd 即可
T: PartialOrd,
{
if arr.len() == 0 {
panic!("数组为空")
}
// 这里必须要拿到引用,可能有人觉得调用 clone 可不可以
// 答案是不可以,因为这个函数不仅支持 String
// 还要支持整型、浮点型,所以只能获取引用
let mut largest = &arr[0];
// 因为 arr 是个引用,所以遍历出来的 item 也是元素的引用
for item in arr {
// 虽然这里表面上比较的是引用,但其实比较的是值
// 比如 let (a, b) = (11, 22),那么 a < b 和 &a < &b 的结果是一样的
if largest < item {
largest = item
}
}
largest
}
fn main() {
let arr = &vec![String::from("A"), String::from("Z")];
println!("{}", max(arr)); // Z
let arr = &vec![1, 22, 11, 34, 19];
println!("{}", max(arr)); // 34
let arr = &vec![1.1, 22.1, 11.2, 34.3, 19.8];
println!("{}", max(arr)); // 34.3
}
此时我们就实现了基础类型的比较,还是需要好好理解一下的。
关于 Rust 的 trait 我们就说到这里,下一篇文章来聊一聊生命周期。
如果觉得文章对您有所帮助,可以请囊中羞涩的作者喝杯柠檬水,万分感谢,愿每一个来到这里的人都生活愉快,幸福美满。
微信赞赏
支付宝赞赏
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!