通过对《Rust 程序设计语言》,《通过例子学 Rust 中文版》以及令狐一冲老师对相关知识点的学习总结而成。
rust-泛型数据类型学习
泛型是具体类型或者其他属性的抽象替代,用于减少代码的重复。可以使用泛型为函数签名或结构体等项创建定义,这样它们就可以用于多种不同的具体数据类型。
1 在函数定义中使用泛型
1.1 未使用泛型情况
如果我们在未使用泛型的情况下,当需要选出不同类型的数组的最大值时就需要实现各种不同数据类型的函数。
fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn largest_char(list: &[char]) -> char {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest_i32(&number_list);
println!("The largest number is {}", result);
assert_eq!(result, 100);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&char_list);
println!("The largest char is {}", result);
assert_eq!(result, 'y');
}
1.2 使用泛型数据类型
很明显,在1.1所介绍的例子中,largest_i32
和largest_char
两个函数的区别仅仅是参数的数据类型不一样,但是两者在数据处理上存在相似之处,因此可以使用泛型数据类型来处理。
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
2 结构体定义中的泛型
2.1 结构体内的成员数据类型一致
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
println!("integer is {:#?}", integer);
let float = Point { x: 1.0, y: 4.0 };
println!("float is {:#?}", float);
}
2.2 结构体内的成员数据类型不一致
#[derive(Debug)]
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let both_integer = Point { x: 5, y: 10 };
println!("both_integer is {:#?}", both_integer);
let both_float = Point { x: 1.0, y: 4.0 };
println!("both_integer is {:#?}", both_integer);
let integer_and_float = Point { x: 5, y: 4.0 };
println!("integer_and_float is {:#?}", integer_and_float);
}
3 枚举定义中的泛型
和结构体类似,枚举也可以在成员中存放泛型数据类型。
3.1 枚举拥有一个泛型
enum Option<T> {
Some(T),
None,
}
Option<T>
是一个拥有泛型 T
的枚举,它有两个成员:Some
,它存放了一个类型 T
的值,和不存在任何值的 None
。
3.2 枚举拥有多个泛型
enum Result<T, E> {
Ok(T),
Err(E),
}
Result 枚举有两个泛型类型,T 和 E。Result 有两个成员:Ok,它存放一个类型 T 的值,而 Err 则存放一个类型 E 的值。这个定义使得 Result 枚举能很方便的表达任何可能成功(返回 T 类型的值)也可能失败(返回 E 类型的值)的操作。
4 方法定义中的泛型
在为结构体和枚举实现方法时,一样也可以用泛型。
4.1 方法中泛型的简单使用
#[derive(Debug)]
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
的方法来返回字段 x
中数据的引用:
注意必须在 impl 后面声明 T,这样就可以在 Point 上实现的方法中使用它了。在 impl 之后声明泛型 T ,这样 Rust 就知道 Point 的尖括号中的类型是泛型而不是具体类型。
4.2 构建一个只用于拥有泛型参数 T 的结构体的具体类型的 impl 块
结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。在结构体 Point<T, U> 上定义了一个方法 mixup。这个方法获取另一个 Point 作为参数,而它可能与调用 mixup 的 self 是不同的 Point 类型。这个方法用 self 的 Point 类型的 x 值(类型 T)和参数的 Point 类型的 y 值(类型 W)来创建一个新 Point 类型的实例:
#[derive(Debug)]
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'};
println!("p1 is {:#?}", p1);
println!("p2 is {:#?}", p2);
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
5 泛型代码的性能
Rust 通过在编译时进行泛型代码的 单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。
让我们看看一个使用标准库中 Option 枚举的例子:
let integer = Some(5);
let float = Some(5.0);
当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给 Option 的值并发现有两种 Option:一个对应 i32 另一个对应 f64。为此,它会将泛型定义 Option 展开为 Option_i32 和 Option_f64,接着将泛型定义替换为这两个具体的定义。
编译器生成的单态化版本的代码看起来像这样,并包含将泛型 Option 替换为编译器创建的具体定义后的用例代码:
enum Option_i32 {
Some(i32),
None,
}
enum Option_f64 {
Some(f64),
None,
}
fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}
可以使用泛型来编写不重复的代码,而 Rust 将会为每一个实例编译其特定类型的代码。这意味着在使用泛型时没有运行时开销;当代码运行,它的执行效率就跟好像手写每个具体定义的重复代码一样。这个单态化过程正是 Rust 泛型在运行时极其高效的原因。