每种编程语言都有一些工具可以高效的处理一些重复的概念。在Rust中,这种工具称之为泛型:使用抽象的概念来替换具体的类型和其他属性。我们不需要知道它的位置,就可以表达泛型的行为或如何关联到其他泛型。
函数能够使用泛型作为参数,替换具体的类型,例如:i32和String这些具体的类型。
这一章,我们将研究如何使用泛型定义自己的类型、函数和方法。首先,我们要再次学习如何抽象一个函数来减少代码的重复,使用相同的技术写一个泛型函数。我们也将在结构体和枚举类中展示如何使用泛型。接着我们学习如何在接口中使用泛型定义行为。即使用泛型和接口组合来限制它只接受特别行为的那些类型,而不是所有类型。最后我们讨论生命周期:各种泛型之间如何相互关联的信息,把这些信息给到编译器。生命周期会给到编译器足够的借用值信息以便可以确保借用值在更多的场景下如何保证它是可用的。
12.1 泛型数据类型
将相同功能的代码抽象出来,使用函数来实现,可以减少代码量。但是函数的参数类型被指定为具体的类型,当换一种类型时,该函数则不再有效,为了解决此问题,可以使用泛型来实现。在函数定义时不指定具体类型,使用泛型来代替,在函数的实际使用时,指定具体类型,代码的逻辑相同,功能相同,大大减少了代码量,而不用为每种类型都写一遍。泛型适合多种数据类型实现相同逻辑的代码。它的好处是:
- 减少了代码的重复
- 提高了灵活性
- 保持了类型的安全
- 运行时零性能损耗(Rust在编译时进行单态化,指定具体的类型)
12.1.1 函数中的定义
从功能重复,但是参数不同的函数说起:
rust
fn largest_u8(List:&[u8])->&u8 {/*... */}
fn largest_u32(List:&[u32])->&u32 {/*... */}
fn largest_i32(List:&[i32])->&i32 {/*... */}
fn largest_i64(List:&[i64])->&i64 {/*... */}
fn largest_u64(List:&[u64])->&u64 {/*... */}
fn largest_f32(List:&[f32])->&f32 {/*... */}
fn largest_f64(List:&[f64])->&f64 {/*... */}
fn largest_char(List:&[char])->&char {/*... */}
为了合并上面的函数,可以使用泛型,如下所示:
rust
fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
let mut largest_element = &list[0];
for item in list {
if item > largest_element {
largest_element = item;
}
}
largest_element
}
代码解读:
- 函数名称后面的<T>:声明一个泛型类型的参数。
- list:&[T]:参数类型是T的切片
- 返回值:&T
- if表达式中使用了比较符">",Rust要求使用比较符的泛型必须实现std::cmp::PartialOrd接口。
12.1.2 结构体中的定义
定义一个结构体
rust
struct Point<T> {
x:T,
y:T,
}
这表示:
- Point是某个类型T的点。
- x和y必须是同一类型。
使用示例:
rust
let integer = Point{x:5,y:10};
let float = Point{x:1.2,y:4.9};
println!("{integer:?}");
println!("{float:?}");
如果x和y使用不同的类型该如何定义呢?参见下面的代码:
rust
#[derive(Debug)]
struct Point<T,U> {
x:T,
y:U,
}
fn main() {
let mixed = Point{x:4,y:9.8};
println!("{mixed:?}");
}
12.1.3 枚举类型中的定义
下面我们看看经典的Option是如何实现的:
rust
#[derive(Debug)]
enum Option<T> {
Some(T),
None,
}
fn main() {
let some_number = Option::Some(5);
let some_string = Option::Some("Hello World");
println!("{some_number:?}");
println!("{:?}", some_string);
}
Option表示可能有值,也可能无值。
还有带有错误的返回结果Result内部是如何实现的:
rust
enum Result<T, E> {
Ok(T),
Err(E),
}
成功返回T,错误返回E。
在打开文件的函数open可以看到Result是如何使用的:
rust
File::open("hello.txt")->Result<File, std::io::error>
12.1.4 方法中的定义
在impl<T>中定义方法
rust
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
fn y(&self) -> &T {
&self.y
}
}
fn main() {
let point = Point { x: 4.1, y: 5.2 };
println!("{}-{}", point.x(), point.y());
}
关键点:
- impl<T>告诉编译器:Point是泛型的。
- 方法对所有Point<T>都有效。
对特定类型实现方法
rust
impl<f32> {
fn distance_from_orgin(&self)->f32 {
(self.x.powi(2)+self.y.powi(2)).sqrt()
}
}
这个方法只有当参数类型是f32时才有效。
impl和方法使用不同的泛型参数
rust
impl<X1,Y1> Point<X1,Y1> {
fn mixup<X2,Y2>(self,other:Point<X2,Y2>)->Point<X1,Y2>{
Point{
x:self.x,
y:self.y,
}
}
}
说明:
Point本身有两个泛型,方法有引入了两个新泛型。最终返回一个拼装后的Point。
12.1.5 使用泛型后的性能
Rust并不会真的保留泛型到运行器,它会将其直接编译为对应的具体类型。
rust
let a = Option::Some(5);
let b = Option::Some(3.14);
编译后它会等价于:
Option_i32::Some(5);
Option_f64::Some(3.14);
结果:
没有虚函数
没有类型擦除
后手写多个版本一样快
这是Rust泛型比java泛型更加高效的重要原因之一。
学习要点总结(必记)
|-------------|---------------------------|
| 知识点 | 核心理解 |
| <T> | 类型占位符 |
| 泛型函数 | 一次定义,多种类型 |
| 泛型结构体 | 字段可以是泛型 |
| 泛型枚举 | Option<T>、Result<T,E> |
| impl<T> | 给泛型类型定义方法 |
| 单态化 | 编译期生成具体类型 |