Rust泛型Generics

2023-03-01 15:32:35 浏览数 (1)

泛型

泛型(Generics)是一种程序设计风格,它允许程序员在强类型语言(例如rust,c#,c )中编写代码时使用通用类型。以rust为例,如果你想实现一个通用的add函数,让其在u8, i32, u64等类型中通用。如果没有泛型,虽然它们的逻辑是一致的,但是你需要为不同类型编写不同的函数,而泛型帮助我们只需要编写一个函数,实现通用逻辑即可。例如:

代码语言:javascript复制
fn main() {
    println!("{}", add(1u8, 2u8));
    println!("{}", add(1i32, 2i32));
    println!("{}", add(1f32, 2f32));
}

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

执行这段代码,输出结果如下所示:

代码语言:javascript复制
3
3
3

可以看到这段代码成功执行,add函数接受多种类型的参数,帮我们减少了代码的编写。泛型是rust多态能力的一种体现。在动态语言中,调用方法一般不受类型约束,称其为“鸭子类型”。也就是说一个东西看起来像鸭子,叫起来像鸭子,游起来也像鸭子,那就认为它就是鸭子。

泛型是一个非常强大的工具,但是如何合理的使用它才是问题。在C/C 和Rust里,掌握泛型对于程序员而言是比较困难的一点。(例如泛型的编译错误有时候很难通过编译器的报错信息进行修正)

上面代码的 T 就是泛型参数,实际上在 Rust 中,泛型参数的名称你可以任意起,但是出于惯例,我们都用 T ( T 是 type 的首字母)来作为首选,这个名称越短越好,除非需要表达含义,否则一个字母是最完美的。

使用泛型参数,有一个先决条件,必需在使用前对其进行声明。

代码语言:javascript复制
fn add<T: std::ops::Add<Output = T>>(a:T, b:T) -> T

这个add函数的定义可以这样理解,函数名后面的T是泛型类型,我们在后面的函数参数以及返回值使用了该类型,因此必须在使用前对其进行声明。而std::ops::Add<Output = T>是对泛型的约束。因为不是所有的T类型都可以进行 运算符操作。 上面的示例展示了rust中的函数泛型,下文将介绍rust中各种各样的泛型。

结构体中使用泛型

结构体中的字段类型也可以用泛型来定义。例如:

代码语言:javascript复制
struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

和前面的函数中使用泛型类似,我们依旧需要注意两个点。

  1. 提前声明,在使用泛型类型之前必需要进行声明 Point<T>,接着就可以在结构体的字段类型中使用 T 来替代具体的类型。
  2. x 和 y 是相同的类型,它们都是类型T。

枚举中使用泛型

在Rust中,枚举中很典型的泛型有Option和Results。Option这个枚举类型用来判断一个数据是有值;而Results则是用来判断值是否正确。它们的定义如下所示:

代码语言:javascript复制
enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Result中的泛型类型有两个,分别是T和E。泛型类型可以有多个,但是如果太过复杂,例如<T, U, V, W, X, Y, Z>这种包含超过5种泛型类型的,此时最后考虑将其进行拆分。

方法中使用泛型

一开始的示例是在函数中使用泛型,现在我们来看一下如何在方法中使用泛型。实际上和函数中使用类似。例如:

代码语言:javascript复制
#![allow(unused)]
struct Point<T> {
    x: T,
    y: T,
}

// 提前声明泛型类型T,由于是对泛型结构体Point实现的方法。因此结构体的名称应该是Point<T>
impl<T> Point<T> {

    // 由于在impl处已经提前声明了泛型T,因此在方法中不用再次声明了。
    fn x(&self) -> &T {
        &self.x
    }

    // 由于在impl处已经提前声明了泛型T,因此在关联函数中不用再次声明了。
    fn new(x:T, y:T) -> Point<T> {
        Point { x, y }
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };
    let q = Point::new(123.123, 456.456);

    println!("p.x = {}", p.x());
    println!("q.x = {}", q.x());
}

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

多个泛型参数

泛型类型可以有多个,下面是一个例子:

代码语言:javascript复制
#[derive(Debug)]
struct Point<U, V>{
    x: U,
    y: V,
}

impl<U, V> Point<U, V> {
    fn new(x:U, y: V) -> Point<U, V> {
        Point { x, y}
    }
    // X, Y这两个泛型类型不属于Point<U, V>的方法实现,因此不能写在impl后面,而是需要写在swap后面。
    // swap的两个参数都不是引用,会引起所有权的转移
    fn swap<X, Y>(self, p:Point<X, Y>) -> Point<U, Y> {
        Point {x: self.x, y: p.y}
    }
}

fn main() {
    let p1 = Point::new(1, "2");
    let p2 = Point::new(3.0f32, 4.5);

    let p3 = p1.swap(p2);
    println!("{:?}", p3);

}

结构体可以有多个泛型类型,方法和关联函数等也可以拥有多个泛型类型。需要注意的是,swap函数的写法,因为X, Y这两个泛型类型不属于Point<U, V>的方法实现,因此不能写在impl后面,而是需要写在swap后面。

泛型性能

Rust 通过在编译时进行泛型代码的 单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。

这个过程中,编译器所做的工作正好与我们在代码中所做的工作相反,编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。

在编译时就确定的多态,势必会导致编译时间变长,但是它带来的好处则是代码执行时速度的提高;而Trait则可以带来运行时的多态,实现原理类似于C 的虚表,虚指针。

参考资料

  1. Rust语言圣经
  2. Rust程序设计语言

0 人点赞