Rust 学习笔记(19)-泛型

参考章节《Rust 程序设计语言》第10章 泛型数据类型

泛型能够让你写出更通用的代码,但也会使你的代码更加复杂不易读,一眼看去全是各种符号

我们先来看一个在函数中使用泛型的例子

  • 下面是一个寻找列表中最大值的小程序
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
fn max_i32(list: &[i32]) -> i32 {
    let mut max = list[0];

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

fn max_char(list: &[char]) -> char {
    let mut max = list[0];

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

我们可以看到,两个函数的逻辑一模一样,仅仅只是数据类型不一样,我们就需要写两个函数,这就很不爽

用泛型来改善它

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn max<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut max = list[0];

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

我们该如何理解这段代码?

  1. 首先<T: PartialOrd + Copy>代表这个函数有一个泛型类型参数 T,至于 PartialOrd + Copy 是一个 trait 你可以理解它是一个 接口T 必须是实现了它们的类型
1
2
PartialOrd trait 可以基于排序的目的而比较一个类型的实例。实现了 PartialOrd 的类型可以使用 <、 >、<= 和 >= 操作符。
Copy trait 允许你通过只拷贝存储在栈上的位来复制值而不需要额外的代码。
  1. list: &[T] 代表它有一个参数 list,它是一个 T 类型的 Slice
  2. -> T 代表这个函数返回一个 T 类型

通过上面的泛型改造后,我们就可以这么使用,代码是不是简单了许多,一个函数就能处理多种数据类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn main() {
    let list1 = vec![34, 50, 25, 100, 65];
    let list2 = vec!['y', 'm', 'a', 'q'];

    let result1 = max(&list1); // 传递数字类型的 Slice
    println!("The max is {}", result1);

    let result2 = max(&list2); // 传递字符类型的 Slice
    println!("The max is {}", result2);
}

在结构体中定义泛型

定义一个可以存放一种任何类型的 xy 坐标值的结构体 Pointxy 必须是相同类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 定义一个可以存放一种任何类型的 x 和 y 坐标值的结构体 Point,x 和 y 必须是相同类型
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 };
}

定义一个可以存放两种任何类型的 xy 坐标值的结构体 Point, xy 必须是不同类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 定义一个可以存放两种任何类型的 x 和 y 坐标值的结构体 Point, x 和 y 必须是不同类型
struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}

枚举中定义泛型同理我们已经见过 Option<T>Result<T, E>,这里就不再举例了

在方法中定义泛型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct Point<T> {
    x: T,
    y: T,
}

// 注意必须在 impl 后面声明 T,这样 Rust 就知道 Point 的尖括号中的类型是泛型而不是具体类型。
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

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

我们还可以为特定的泛型类型实现方法

1
2
3
4
5
impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

这段代码意味着 Point<f32> 类型会有一个方法 distance_from_origin,而其他 T 不是 f32 类型的 Point<T> 实例则没有定义此方法。

结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型

这段代码看的懂就看,看不懂就算了,以后再来看,说不定你就理解了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
    // 方法中,Point 结构体的签名和定义时 Point 的签名可以不一致,例如这里是 Point<X2, Y2>
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
        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); // 这样的话, p1 可以传递不同类型的 p2 进去,而不必非要传同类型的 Point 进去

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

在阅读本部分内容的同时,你可能会好奇使用泛型类型参数是否会有运行时消耗。
好消息是:Rust 实现了泛型,使得使用泛型类型参数的代码相比使用具体类型并没有任何速度上的损失。

这就是零成本抽象吗?

updatedupdated2024-10-012024-10-01