冰芹的 Notes

泛型和特征

创建于 2026-04-18T11:23:56

泛型和特征

泛型 Generics

泛型可以让一个函数为多个不同的数据类型服务:

fn add<T>(a:T, b:T) -> T { 
a + b
}

但在以上代码中,不是所有类型都能进行相加操作,使用我们需要对其进行限制:

fn add<T: std::ops::Add<Output = T>>(a:T, b:T) -> T {
    a + b
}

显式地制定类型

create_and_print::<i64>();

结构体中使用泛型

结构体中的字段类型也可以用泛型来定义,下面代码定义了一个坐标点 Point,它可以存放任何类型的坐标值:

struct Point<T> {
x: T,
y: T,
}

如果想让 x 和 y 既能类型相同,又能类型不同,就需要使用不同的泛型参数:

struct Point<T,U> {
    x: T,
    y: U,
}

枚举中使用泛型

enum Option<T> {
    Some(T),
    None,
}

方法中使用泛型

使用泛型参数前,依然需要提前声明:impl<T>

struct Point<T> {
    x: T,
    y: T,
}

//这里的 Point<T> 不再是泛型声明,而是一个完整的结构体类型
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

为具体的泛型类型实现方法

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        //...
    }
}

const 泛型

const 泛型:也就是针对值的泛型

fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
    println!("{:?}", arr);
}
fn main() {
    let arr: [i32; 3] = [1, 2, 3];
    display_array(arr);

    let arr: [i32; 2] = [1, 2];
    display_array(arr);
}

const fn

通常情况下,函数是在运行时被调用和执行的。然而,在某些场景下,我们希望在编译期就计算出一些值,从而将计算结果直接嵌入到生成的代码中。(例如,定义数组的长度、计算常量值等。)

结合const fn和const 泛型

将 const fn 与 const 泛型 结合,可以实现更加灵活和高效的代码设计。例如,创建一个固定大小的缓冲区结构,其中缓冲区大小由编译期计算确定。

泛型的优缺点

泛型在性能上无损失。但损失了编译速度和文件大小。

特征 Trait

特征定义了一组可以被共享的行为,只要为对象实现了特征,这个对象就能使用这组行为

定义特征

特征只定义行为看起来是什么样的,而不定义行为具体是怎么样的。

pub trait Summary {
    fn summarize(&self) -> String;
}

为类型实现特征

//定义Summary特征
pub trait Summary {
    fn summarize(&self) -> String;
}
//定义post
pub struct Post {
    pub title: String, // 标题
    pub author: String, // 作者
    pub content: String, // 内容
}
//为post实现特征
impl Summary for Post {
    fn summarize(&self) -> String {
        format!("文章{}, 作者是{}", self.title, self.author)
    }
}
//定义Weibo
pub struct Weibo {
    pub username: String,
    pub content: String
}
//为Weibo实现特征
impl Summary for Weibo {
    fn summarize(&self) -> String {
        format!("{}发表了微博{}", self.username, self.content)
    }
}

孤儿规则

孤儿规则如果你想要为类型 A 实现特征 T,那么 A 或者 T 至少有一个是在当前作用域中定义的
这个规则可以确保其它人编写的代码不会破坏你的代码,也确保了你不会莫名其妙就破坏了风马牛不相及的代码。

默认实现

pub trait Summary {
    fn summarize(&self) -> String {
        //定义默认实现
        String::from("(Read more...)")
    }
}
//给Post使用默认方法
impl Summary for Post {}
//给Weibo重写方法
impl Summary for Weibo {
    fn summarize(&self) -> String {
        format!("{}发表了微博{}", self.username, self.content)
    }
}

默认实现允许调用相同特征中的其他方法

//定义一个summarize方法,此方法的默认实现调用待实现的summarize_author方法
pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}
//使用时,只需要定义summarize_author即可
impl Summary for Weibo {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}
println!("1 new weibo: {}", weibo.summarize());

使用特征作为函数参数(特征约束)

下面代码中,你可以使用任何实现了Summary特征的类型作为该函数的参数。同时在函数体内,还可以调用该特征的方法,例如 summarize 方法。

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

特征约束

虽然上面的impl Trait 这种语法非常好理解,但是实际上它只是一个语法糖。完整的语法是这样,形如T: Summary被称为特征约束

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

我们可以用特征约束强制函数的两个参数是同一类型:

pub fn notify<T: Summary>(item1: &T, item2: &T) {}

多重约束

下面代码约束了参数必须同时实现summary和display特征

pub fn notify(item: &(impl Summary + Display)) {}

Where约束

用where可以优化复杂的约束形式,让代码更易读

//不使用where
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}

//使用where
fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{}

使用特征约束有条件地实现方法或特征

cmp_display 方法,并不是所有的 Pair<T> 结构体对象都可以拥有,只有T同时实现了 Display + PartialOrdPair<T>才可以拥有此方法。

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        //...
    }
}

也可以有条件地实现特征

impl<T: Display> ToString for T {
    // --snip--
}

函数返回中的impl Trait

返回的真实类型非常复杂,你不知道该怎么声明时(,此时就可以用 impl Trait 的方式简单返回。

通过derive派生特征

形如#[derive(Debug)]的代码是一种特征派生语法,被 derive 标记的对象会自动实现对应的默认特征代码,继承相应的功能。

derive列表:附录-派生特征

调用方法需要引入特征

use std::convert::TryInto;
如果你要使用一个特征的方法,那么你需要将该特征引入当前的作用域中

自定义类型的打印输出

如果要自定义类型的打印输出格式,就要为自定义类型实现std::fmt::Display

示例:自定义类型的打印输出

特征对象

在C++中,我们可以定义一个Component类,该类上有一个 draw 方法。其他的类比如ButtonImageSelectBox会从Component派生并因此继承draw方法。然后我们就可以把这些东西全部视为component实例并调用它们的draw方法。

Rust并没有继承,取而代之的是特征对象

特征对象指向实现了Draw特征的类型的实例,这种映射关系是存储在一张表中,可以在运行时通过特征对象找到具体调用的类型方法。

Draw 特征对象:Box<dyn Draw>,任何实现了 Draw 特征的类型,都可以存放其中。

  • 泛型+特征约束也可以实现这个功能,但是他只允许放进去的全部东西都是同一类型。
  • 而特征对象`允许往里面同时存储不同的类型。

可以通过 & 引用或者 Box<T> 智能指针的方式来创建特征对象。

// 若 T 实现了 Draw 特征, 则调用该函数时传入的 Box<T> 可以被隐式转换成函数参数签名中的 Box<dyn Draw>
fn draw1(x: Box<dyn Draw>) {
    // 由于实现了 Deref 特征,Box 智能指针会自动解引用为它所包裹的值,然后调用该值对应的类型上定义的 `draw` 方法
    x.draw();
}

fn draw2(x: &dyn Draw) {
    x.draw();
}

用特征对象:Screen中存储了一个动态数组,里面元素的类型是 Draw 特征对象:Box<dyn Draw>,任何实现了 Draw 特征的类型,都可以存放其中。

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

//使用
fn main() {
    let screen = Screen {
        //这样就可以放入多个不同的类型
        components: vec![
            Box::new(SelectBox {
                width: 75,
                height: 10,
                options: vec![
                    String::from("Yes"),
                    String::from("Maybe"),
                    String::from("No")
                ],
            }),
            Box::new(Button {
                width: 50,
                height: 10,
                up: String::from("OK"),
            }),
        ],
    };

    screen.run();
}

如果通过泛型+特征约束实现,会限制Screen实例的Vec<T>中的每个元素都是同一个类型T

pub struct Screen<T: Draw> {
    pub components: Vec<T>,
}

impl<T> Screen<T>
    where T: Draw {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

鸭子类型

鸭子类型:就是只关心值长啥样,而不关心它实际是什么。
(只要它实现了draw特征,就能通过)

特征对象的动态分发

静态分发(static dispatch):在编译期完成的,对于运行期性能完全没有任何影响。(编译器会为每一个泛型参数对应的具体类型生成一份代码)Box<T>

动态分发(dynamic dispatch),在这种情况下,直到运行时,才能确定需要调用什么方法。之前代码中的关键字 dyn 正是在强调这一“动态”的特点。(特征对象:编译器无法知晓所有可能用于特征对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现)
Box(dyn Trait)

特征对象特点:
- 特征对象大小不固定
- 几乎总是使用特征对象的引用方式

特征对象只能用来调用实现于特征的方法

Self 与 self

self指带当前的实例对象
Self指代特征或者方法类型

fn draw(&self) -> Self {
    return self.clone()
}

特征对象的限制

只有对象安全的特征才能拥有特征对象:
- 方法的返回类型不能是 Self
- 方法没有任何泛型参数

深入了解特征

关联类型

Text Elements

泛型和特征 ^8rmCOzoY

泛型 ^zFmKwAKl

特征 ^VBSKp1OL

特征对象 ^eISso5sz

功能:允许不同的数据类型作为参数 ^C5OoDbuq

用处:让一个函数可以为多个不同的数据类型服务 ^Onrt2vrf

功能:一组可以被共享的行为。只要为对象要实现了特征,这个对象就能使用这组行为 ^otEGm4Eh

用处:用来定义会被多个不同类型使用的行为 ^OwBxWsIO

特征约束 ^QLwFIHs7

功能:让泛型参数只能接收实现了某个特征的对象 ^2csMkxHQ

功能:指向实现了某个特征的类型的实例 ^g0KQM6aI

用处:用来批量操作不同但拥有同一个特征的对象 ^sZ1wy6zT

特征约束:也可以指向实现了某个特征类型的实例,但是只能指向同一个类型的对象。 ^Mr51vv3X