泛型数据类型
ch10-01-syntax.md
commit 55d9e75ffec92e922273c997026bb10613a76578
泛型用于通常我们放置类型的位置,比如函数签名或结构体,允许我们创建可以代替许多具体数据类型的结构体定义。让我们看看如何使用泛型定义函数、结构体、枚举和方法,并且在本部分的结尾我们会讨论泛型代码的性能。
在函数定义中使用泛型
定义函数时可以在函数签名的参数数据类型和返回值中使用泛型。以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码。
回到largest
函数上,列表 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。第一个是从列表 10-3 中提取的寻找 slice 中i32
最大值的函数。第二个函数寻找 slice 中char
的最大值:
Filename: src/main.rs
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 numbers = vec![34, 50, 25, 100, 65];
let result = largest_i32(&numbers);
println!("The largest number is {}", result);
# assert_eq!(result, 100);
let chars = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&chars);
println!("The largest char is {}", result);
# assert_eq!(result, 'y');
}
这里largest_i32
和largest_char
有着完全相同的函数体,所以能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现!
为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称T
。任何标识符都可以作为类型参数名,选择T
是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。T
作为“type”的缩写是大部分 Rust 程序员的首选。
当需要在函数体中使用一个参数时,必须在函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。类型参数声明位于函数名称与参数列表中间的尖括号中。
我们将要定义的泛型版本的largest
函数的签名看起来像这样:
fn largest<T>(list: &[T]) -> T {
这可以理解为:函数largest
有泛型类型T
。它有一个参数list
,它的类型是一个T
值的 slice。largest
函数将会返回一个与T
相同类型的值。
列表 10-5 展示一个在签名中使用了泛型的统一的largest
函数定义,并向我们展示了如何对i32
值的 slice 或char
值的 slice 调用largest
函数。注意这些代码还不能编译!
Filename: src/main.rs
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
let result = largest(&numbers);
println!("The largest number is {}", result);
let chars = vec!['y', 'm', 'a', 'q'];
let result = largest(&chars);
println!("The largest char is {}", result);
}
如果现在就尝试编译这些代码,会出现如下错误:
error[E0369]: binary operation `>` cannot be applied to type `T`
|
5 | if item > largest {
| ^^^^
|
note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
注释中提到了std::cmp::PartialOrd
,这是一个 trait。下一部分会讲到 trait,不过简单来说,这个错误表明largest
的函数体不能适用于T
的所有可能的类型;因为在函数体需要比较T
类型的值,不过它只能用于我们知道如何排序的类型。标准库中定义的std::cmp::PartialOrd
trait 可以实现类型的排序功能。在下一部分会再次回到 trait 并讲解如何为泛型指定一个 trait,不过让我们先把这个例子放在一边并探索其他那些可以使用泛型类型参数的地方。
结构体定义中的泛型
同样也可以使用<>
语法来定义拥有一个或多个泛型参数类型字段的结构体。列表 10-6 展示了如何定义和使用一个可以存放任何类型的x
和y
坐标值的结构体Point
:
Filename: src/main.rs
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
的定义中只使用了一个泛型类型,我们想要表达的是结构体Point
对于一些类型T
是泛型的,而且字段x
和y
都是相同类型的,无论它具体是何类型。如果尝试创建一个有不同类型值的Point
的实例,像列表 10-7 中的代码就不能编译:
Filename: src/main.rs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
尝试编译会得到如下错误:
error[E0308]: mismatched types
-->
|
7 | let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integral variable, found
floating-point variable
|
= note: expected type `{integer}`
= note: found type `{float}`
当我们将 5 赋值给x
,编译器就知道这个Point
实例的泛型类型T
是一个整型。接着我们将y
指定为 4.0,而它被定义为与x
有着相同的类型,所以出现了类型不匹配的错误。
如果想要一个x
和y
可以有不同类型且仍然是泛型的Point
结构体,我们可以使用多个泛型类型参数。在列表 10-8 中,我们修改Point
的定义为拥有两个泛型类型T
和U
。其中字段x
是T
类型的,而字段y
是U
类型的:
Filename: src/main.rs
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 };
}
现在所有这些Point
实例都是被允许的了!你可以在定义中使用任意多的泛型类型参数,不过太多的话代码将难以阅读和理解。如果你处于一个需要很多泛型类型的位置,这可能是一个需要重新组织代码并分隔成一些更小部分的信号。
枚举定义中的泛型数据类型
类似于结构体,枚举也可以在其成员中存放泛型数据类型。第六章我们使用过了标准库提供的Option<T>
枚举,现在这个定义看起来就更容易理解了。让我们再看看:
enum Option<T> {
Some(T),
None,
}
换句话说Option<T>
是一个拥有泛型T
的枚举。它有两个成员:Some
,它存放了一个类型T
的值,和不存在任何值的None
。标准库中只有这一个定义来支持创建任何具体类型的枚举值。“一个可能的值”是一个比具体类型的值更抽象的概念,而 Rust 允许我们不引入重复代码就能表现抽象的概念。
枚举也可以拥有多个泛型类型。第九章使用过的Result
枚举定义就是一个这样的例子:
enum Result<T, E> {
Ok(T),
Err(E),
}
Result
枚举有两个泛型类型,T
和E
。Result
有两个成员:Ok
,它存放一个类型T
的值,而Err
则存放一个类型E
的值。这个定义使得Result
枚举能很方便的表达任何可能成功(返回T
类型的值)也可能失败(返回E
类型的值)的操作。回忆一下列表 9-2 中打开一个文件的场景,当文件被成功打开T
被放入了std::fs::File
类型而当打开文件出现问题时E
被放入了std::io::Error
类型。
当发现代码中有多个只有存放的值的类型有所不同的结构体或枚举定义时,你就应该像之前的函数定义中那样引入泛型类型来减少重复代码。
方法定义中的枚举数据类型
可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。列表 10-9 中展示了列表 10-6 中定义的结构体Point<T>
。接着我们在Point<T>
上定义了一个叫做x
的方法来返回字段x
中数据的引用:
Filename: src/main.rs
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());
}
注意必须在impl
后面声明T
,这样就可以在Point<T>
上实现的方法中使用它了。
结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。列表 10-10 中在列表 10-8 中的结构体Point<T, U>
上定义了一个方法mixup
。这个方法获取另一个Point
作为参数,而它可能与调用mixup
的self
是不同的Point
类型。这个方法用self
的Point
类型的x
值(类型T
)和参数的Point
类型的y
值(类型W
)来创建一个新Point
类型的实例:
Filename: src/main.rs
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);
}
在main
函数中,定义了一个有i32
类型的x
(其值为5
)和f64
的y
(其值为10.4
)的Point
。p2
则是一个有着字符串 slice 类型的x
(其值为"Hello"
)和char
类型的y
(其值为c
)的Point
。在p1
上以p2
调用mixup
会返回一个p3
,它会有一个i32
类型的x
,因为x
来自p1
,并拥有一个char
类型的y
,因为y
来自p2
。println!
会打印出p3.x = 5, p3.y = c
。
注意泛型参数T
和U
声明于impl
之后,因为他们于结构体定义相对应。而泛型参数V
和W
声明于fn mixup
之后,因为他们只是相对于方法本身的。
泛型代码的性能
在阅读本部分的内容的同时你可能会好奇使用泛型类型参数是否会有运行时消耗。好消息是:Rust 实现泛型泛型的方式意味着你的代码使用泛型类型参数相比指定具体类型并没有任何速度上的损失。
Rust 通过在编译时进行泛型代码的单态化(monomorphization)来保证效率。单态化是一个将泛型代码转变为实际放入的具体类型的特定代码的过程。
编译器所做的工作正好与列表 10-5 中我们创建泛型函数的步骤相反。编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。
让我们看看一个使用标准库中Option
枚举的例子:
let integer = Some(5);
let float = Some(5.0);
当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给Option
的值并发现有两种Option<T>
:一个对应i32
另一个对应f64
。为此,它会将泛型定义Option<T>
展开为Option_i32
和Option_f64
,接着将泛型定义替换为这两个具体的定义。
编译器生成的单态化版本的代码看起来像这样,并包含将泛型Option
替换为编译器创建的具体定义后的用例代码:
Filename: src/main.rs
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 泛型在运行时极其高效的原因。