在这里插入图片描述

现代语言基本都有泛型,是在语言层面实现代码复用的一种高级形式。本文我们了解一下 Rust 里的泛型。

1. 在函数中使用泛型

在函数中使用泛型,在有的语言里也叫“模板方法”、考虑一个求最大值函数,这个函数的逻辑非常简单,从一个集合中取出第一个值赋给变量 largest,然后迭代集合中的每一个元素,只要发现集合中现的元素比这个 largest 大,就把这个元素的值赋给 largest,迭代结束后,largest 的值就是这个集合中的最大值。这个算法是如此的通用,以至于它几乎可以应用在任何类型上,但唯一的一个障碍就是:类型,受不同类型的拖累,在没有泛型的语言里,你可能需要针对每一种类型实现一个版本,而让人“意难平”的是,它们几乎都“长得” 一样,只是换了一下类型。这就是泛型的典型应用场景!我们来看一下在 Rust 中如何实现这个泛型版的函数。首先,作为参照,我们看一下 i32 版本的实现:

fn largest(list: &[i32]) -> i32 {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

然后是泛型版本,和大多数语言一样,Rust 使用一对尖括号来声明一个类型参数:<T>

fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

你会看到:泛型版本几乎是把“hard code”的 i32 类型直接替换成了 T(T 被称作“类型参数”),可见泛型的实现还是非常“丝滑”的。不过,有一点麻烦,这个函数运行时是会报错的,因为并不是任何一种类型都可以比较大小的,也就是可以使用 > 运算符,这解决的方法很简单,收窄一下类型范围,把类型参数 T 限定在可以比较大小的类型上,考虑到过程中使用了赋值操作,安全起见,这个类型还必须得是一个 Copy 类型,这样,我们只需要把 <T> 改为 <T: PartialOrd + Copy> 就没有问题了,这种语法叫 trait bound,意思就是告诉编译器:类型 T 必须是一个既实现了 PartialOrd 又实现了 Copy 这两个 trait 的类型。关于这个语法,我们会在《Rust 语言特性:Trait》一文中专门介绍。

2. 在结构体中使用泛型

考虑一个叫 Point 的结构体,代表一个“点”,它有横、纵两个坐标:x,y,在不同的场景下,坐标值的类型可能会有所不同,有的时候是整型,有时候是浮点型,所以 Point 被设计了泛型结构体:

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 };
}

如果你需要两种类型参数,那就要再多声明一个。假设 Point 的 x, y 坐标支持不同类型的值,那就要这样写:

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

fn main() {
    let p = Point { x: 5, y: 1.0 };
}

3. 在枚举中使用泛型

我们在《Rust 语言特性:泛型》一文中已经介绍了枚举和泛型的结合使用。这里只简单温习一下。枚举中使用泛型是非常普遍的,就跟在结构体中使用泛型一样普遍。典型的枚举例子是 Option<T>:

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

Option<T> 枚举用于表达“一个有可能为空”的值”,表示一个值就得有“类型”,Option 必须支持所有的类型,所以它必须使用泛型!

4. 在方法中使用泛型

如果一个结构体或枚举被声明为了泛型,那么,它的方法大概率也将“泛型化”,因为一旦你实现的方法要依赖到结构体或枚举的泛型字段时,你将不得不被迫使用 T 来处理它们,否则编译就报错了。考虑以下代码:

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

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

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

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

我们先有了一个泛型化的 Point<T> 结构体,然后要为它定一个获取 x 坐标的 getter,就像前面说的,这个方法将不可避免的也要进行泛型处理,因为它要返回的 x 已经是一个泛型了。这里要注意的是:在 impl 后面也有一个类型参数 <T>,这是必须的!这样 Rust 才知道 Point 的尖括号中的类型是泛型而不是具体类型

有时候,结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。在下面的例子中,mixup 方法会从两种类型不同的 Point 中分别提取出 x 和 y,然后组合成一种新的 Point,这里就会涉及到 4 种类型参数:

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

impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c'};

    let p3 = p1.mixup(p2);

    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

5. Rust 泛型的实现方式

很多语言里有泛型机制,但实现方式各有不同。C++ 采用的是“模板实例化”,也就是编译器把“带类型参数的模板”真正变成具体代码的过程,而 Java 采用的是“类型擦除”,Rust 的实现方法式是:单态化(monomorphization):编译器为每一种具体类型生成一份专门代码。这和 C++ 的“模板实例化”是同类型的实现思路。所以,你可以很清楚地知道:Rust 的泛型几乎没有性能损失

更多推荐