INTERACTIVE LAB

rustVIP17 个可运行示例0 次浏览 更新于 2026-09-28

Rust:Trait 特征

介绍 Rust Trait 的定义与用法,并覆盖运算符重载与泛型约束。

一、前言

本文主要详解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!这个宏打印它,是会报错的:

image-20240219142141184

因为它现在还没有实现Display这个trait,这个宏不知道该怎么打印它。

所以下面我们就让其实现一下这个Display:

rust
待运行⌘↵
use std::fmt;

struct Stu {
    name: String,
    age: u32,
    id: String,
}

impl fmt::Display for Stu {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Name: {}, Age: {}, Id: {}", self.name, self.age, self.id)
    }
}

fn main() {
    let s = Stu {
        name: String::from("www.kucoding.com"),
        age: 100,
        id: String::from("10010"),
    };
    println!("{}", s);
}
编辑仅保存在本页,刷新后恢复

上面的代码中,我们为 Stu 实现了 Display,因此可以直接格式化输出。

代码引用traits-display
第 1–12 行
1use std::fmt;
2
3struct Stu {
4 name: String,
5 age: u32,
6 id: String,
7}
8
9impl fmt::Display for Stu {
10 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11 write!(f, "Name: {}, Age: {}, Id: {}", self.name, self.age, self.id)
12 }
只读片段,不影响运行示例

上面的代码中我省略了部分代码,让其看起来更加简洁,由于Display这个trait在标准库std::fmt中,所以我们首先需要将其引入进来

代码引用traits-display
第 1 行
1use 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,不同之处在于,方法是你随意为结构体绑定的,你可以在其中写任何方法:

代码引用traits-display
第 8–12 行
8
9impl fmt::Display for Stu {
10 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11 write!(f, "Name: {}, Age: {}, Id: {}", self.name, self.age, self.id)
12 }
只读片段,不影响运行示例

而如果要实现一个trait,你必须写这个trait内部已经写好的方法:

代码引用traits-display
第 8–12 行
8
9impl fmt::Display for Stu {
10 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11 write!(f, "Name: {}, Age: {}, Id: {}", self.name, self.age, self.id)
12 }
只读片段,不影响运行示例

这里的impl fmt::Display for Stu意思就是为Stu实现fmt::Display这个Trait,而这个Trait中就有一个叫做fmt的函数,格式如下:

在这里插入图片描述

这是写在标准库中的代码,会有很多我们不认识的东西,但这并不重要。

我们只需要将标准库中代码抄过来即可:

代码引用traits-display
第 9–11 行
9impl fmt::Display for Stu {
10 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11 write!(f, "Name: {}, Age: {}, Id: {}", self.name, self.age, self.id)
只读片段,不影响运行示例

对于实现一个trait来说,外面这个框架是固定的,我们要做的就是在这个框架中,写出自己的代码,比如这里:

代码引用traits-display
第 10 行
10 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
只读片段,不影响运行示例

我们直接通过调用一个宏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,一般就要像下面这样写:

代码引用traits-shape
第 9–22 行
9}
10
11struct triangle{ //为了简单,假设其是直角三角形,存放两个直角边
12 a: f32,
13 b: f32,
14}
15
16impl Shape for triangle { //为三角形实现Shape
17 fn area(&self) -> f32 {
18 return (self.a*self.b)/2.0;
19 }
20}
21
22struct square{
只读片段,不影响运行示例

这样为每个结构体都绑定一个area方法,使用的时候,就是像下面这样使用:

代码引用traits-shape
第 1–7 行
1trait Shape {
2 fn area(&self) -> f32{
3 return 0.0;
4 } //该函数实现可写可不写,如果不写,那么实现该Trait的结构就必须写,如果这里写了,那么后面实现该trait的结构就可以不写
5
6 fn test(){
7 println!("不写self参数,则只能通过 :: 的方式进行调用");
只读片段,不影响运行示例

也就是说,只要你有一个新的形状,想要取得这个新的形状变量的面积,我就只能亲自去调用这个函数。

这样做当然可以,但却不具有通用性,正如前面所说的println这个宏,这是官方写好的,官方怎么知道我们写的结构体是什么呢?这就导致代码缺少通用性。

而如果使用了trait后,我们就可以将上面的代码改为如下:

rust
待运行⌘↵
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;
    }
}

fn test_area(shape: &impl Shape) {
    println!("{}", shape.area());
}

fn main() {
    let t = triangle { a: 1.0, b: 2.0 };
    let s = square { a: 4.0 };
    test_area(&t);
    test_area(&s);
    triangle::test();
}
编辑仅保存在本页,刷新后恢复

此时代码看起来更加复杂了,因为我新加了一个叫做Shape的trait,在这个trait中我写了两个函数:area与test

其中area函数的参数带有self,也就是要与具体的结构体对应,调用的时候要用.的方式。

这与为结构体绑定方法的逻辑基本一致,唯一不同的是,这里你可以实现这个函数,也可以不实现。

如果不实现,那么后面的结构体就必须实现,而如果实现了,后面结构体可以不实现,那么调用这个函数的时候就会调用trait本身这个函数,如果也实现了,就会覆盖trait内部的函数代码,调用它自己实现的代码。

调用方法如下:

代码引用traits-shape
第 25–34 行
25
26impl Shape for square { //为正方形实现Shape
27 fn area(&self) -> f32 {
28 return self.a*self.a;
29 }
30}
31
32fn test_area(shape: &impl Shape) {
33 println!("{}", shape.area());
34}
只读片段,不影响运行示例

如果仅仅是这样看的话,它与结构体的方法没有什么区别,所以我们还需要改变一下:

代码引用traits-shape
第 25–27 行
25
26impl Shape for square { //为正方形实现Shape
27 fn area(&self) -> f32 {
只读片段,不影响运行示例
该实验为 VIP 专享内容,
请开通后继续阅读后续 70% 并运行完整示例~