泛型数据类型
我们使用泛型为函数签名或 结构体,然后我们可以将其与许多不同的具体数据类型一起使用。让我们 首先看看如何使用 泛 型。然后,我们将讨论泛型如何影响代码性能。
在函数定义中
在定义使用泛型的函数时,我们将泛型放在 签名,我们通常会指定 parameters 和 return value 的 API 中。这样做使我们的代码更加灵活,并提供 为函数的调用者提供更多功能,同时防止代码重复。
继续我们的函数,示例 10-4 展示了两个函数,它们
两者都能找到 slice 中的最大值。然后,我们将这些组合成一个
使用泛型的函数。largest
文件名: src/main.rs
fn largest_i32(list: &[i32]) -> &i32 { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn largest_char(list: &[char]) -> &char { let mut largest = &list[0]; for item in list { 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'); }
该函数是我们在示例 10-3 中提取的函数,它找到
切片中最大的。该函数在切片中查找最大的。函数体具有相同的代码,因此让我们消除
通过在单个函数中引入泛型类型参数来进行复制。largest_i32
i32
largest_char
char
要在新的单个函数中参数化类型,我们需要将类型命名为
parameter 的值,就像我们对函数的值 parameters 所做的那样。您可以使用
any identifier 作为类型参数名称。但是我们将使用 because,通过
约定,Rust 中的类型参数名称很短,通常只有一个字母,而
Rust 的类型命名约定是 UpperCamelCase。type 的缩写是
大多数 Rust 程序员的默认选择。T
T
当我们在函数体中使用参数时,我们必须声明
parameter name 的签名,以便编译器知道该名称的含义。
同样,当我们在函数签名中使用类型参数名称时,我们有
来声明类型参数名称。为了定义泛型函数,我们将类型名称声明放在函数名称和参数列表之间的尖括号 , 内,如下所示:largest
<>
fn largest<T>(list: &[T]) -> &T {
我们将这个定义理解为:函数在某种类型上是泛型的。此函数有一个名为 的参数,它是一个值切片
的类型 。该函数将返回对
相同类型 .largest
T
list
T
largest
T
示例 10-5 显示了使用泛型
数据类型。清单还显示了我们如何调用函数
替换为 values 或 values 切片。请注意,此代码不会
编译,但我们将在本章后面修复它。largest
i32
char
文件名: src/main.rs
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
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}");
}
如果我们现在编译这段代码,我们将收到这个错误:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
--> src/main.rs:5:17
|
5 | if item > largest {
| ---- ^ ------- &T
| |
| &T
|
help: consider restricting type parameter `T`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
| ++++++++++++++++++++++
For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
帮助文本提到了 ,这是一个特征,我们是
将在下一节中讨论 traits。现在,请知道这个错误
表示 body of 不适用于所有可能的类型。因为我们想要比较 body 中 type 的值,所以我们可以
仅使用其值可以排序的类型。为了启用比较,标准
library 具有 Trait that you can implement on types
(有关此性状的更多信息,请参见附录 C)。按照帮助文本的
建议中,我们将对 set for 有效的类型限制为仅实现的类型,此示例将编译,因为标准库
在 和 上实施。std::cmp::PartialOrd
largest
T
T
std::cmp::PartialOrd
T
PartialOrd
PartialOrd
i32
char
在结构定义中
我们还可以定义结构体以在一个或多个
字段。示例 10-6 定义了一个结构体来保存和协调任何类型的值。<>
Point<T>
x
y
文件名: 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 }; }
在结构体定义中使用泛型的语法类似于 函数定义。首先,我们在 结构名称后面的尖括号。然后我们使用泛型 type 在 struct 定义中,否则我们将指定具体数据 类型。
请注意,因为我们只使用了一个泛型类型来定义 ,所以这个
definition 表示该结构在某种类型上是泛型的,并且
字段 和 都是相同的类型,无论该类型是什么。如果
我们创建一个具有不同类型值的 a 实例,如
示例 10-7,我们的代码无法编译。Point<T>
Point<T>
T
x
y
Point<T>
文件名: src/main.rs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
在这个例子中,当我们将整数值赋值给 时,我们让
编译器知道泛型类型将是此实例的整数。然后,当我们为 指定 时,我们将其定义为具有
与 相同类型,我们将收到如下所示的类型不匹配错误:5
x
T
Point<T>
4.0
y
x
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
--> src/main.rs:7:38
|
7 | let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integer, found floating-point number
For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
定义一个结构,其中 和 都是泛型,但可以具有
不同的类型,我们可以使用多个泛型类型参数。例如,在
示例 10-8,我们将 的定义更改为 泛型 ,其中 is 为 type 和 is 为 type 。Point
x
y
Point
T
U
x
T
y
U
文件名: 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
在 Enum 定义中
就像我们对结构体所做的那样,我们可以定义枚举来保存其
变种。让我们再看一下标准
library 提供,我们在第 6 章中使用了它:Option<T>
#![allow(unused)] fn main() { enum Option<T> { Some(T), None, } }
现在,此定义对您来说应该更有意义。如你所见,枚举是 type 上的泛型,并且有两个变体:,其中
包含一个 type 为 的值 ,以及一个不包含任何值的 variant。
通过使用枚举,我们可以表达
optional 值,并且因为是 generic 的,所以我们可以使用这个抽象
无论 Optional 值的类型是什么。Option<T>
T
Some
T
None
Option<T>
Option<T>
枚举也可以使用多个泛型类型。我们在第 9 章中使用的枚举定义就是一个例子:Result
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
枚举在两种类型上是泛型的,并且具有两个变体: ,它包含类型为 的值 ,以及 ,它包含类型为值。这个定义使得在任何地方使用 enum 都很方便。
有一个作可能会成功(返回某种类型的值)或失败
(返回某种类型的错误)。事实上,这就是我们用来打开
file 中,其中
文件已成功打开,并在打开文件时填写了类型。Result
T
E
Ok
T
Err
E
Result
T
E
T
std::fs::File
E
std::io::Error
当你识别到代码中具有多个 struct 或 enum 的情况时 定义仅在它们所持有的值的类型上有所不同,您可以 通过使用泛型类型来避免重复。
在方法定义中
我们可以在结构和枚举上实现方法(就像我们在第 5 章中所做的那样)并使用
泛型类型。示例 10-9 显示了我们在示例 10-6 中定义的结构体,上面有一个名为 implemented 的方法。Point<T>
x
文件名: 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()); }
在这里,我们定义了一个名为 on 的方法,它返回一个引用
添加到字段中的数据 。x
Point<T>
x
请注意,我们必须紧接着声明,以便我们可以用来指定
我们正在 .通过声明为
泛型类型后,Rust 可以识别出该类型在
Brackets in 是泛型类型,而不是具体类型。我们可以
为此泛型参数选择的名称与泛型
parameter 的 Parameter 中声明的,但使用相同的名称是
协定的。在声明泛型类型的 an 中编写的方法
将在该类型的任何实例上定义,无论具体类型以何种结尾
up 替换泛型类型。T
impl
T
Point<T>
T
impl
Point
impl
在
类型。例如,我们只能在实例上实现方法
而不是在具有任何泛型类型的实例上。在示例 10-10 中,我们
使用具体类型 ,这意味着我们不会在 .Point<f32>
Point<T>
f32
impl
文件名: src/main.rs
struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } impl Point<f32> { fn distance_from_origin(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() } } fn main() { let p = Point { x: 5, y: 10 }; println!("p.x = {}", p.x()); }
此代码意味着该类型将具有一个方法;where is not 类型的其他实例不会
定义此方法。该方法测量我们的点与
指向坐标 (0.0, 0.0),并使用
仅适用于浮点类型。Point<f32>
distance_from_origin
Point<T>
T
f32
结构体定义中的泛型类型参数并不总是相同的
你在同一结构体中使用 method signatures。示例 10-11 使用泛型
types,以及 struct 和 method
签名以使示例更清晰。该方法使用(类型 )中的值和传入(类型 )中的值创建一个新实例。X1
Y1
Point
X2
Y2
mixup
Point
x
self
Point
X1
y
Point
Y2
文件名: src/main.rs
struct Point<X1, Y1> { x: X1, y: Y1, } impl<X1, Y1> Point<X1, Y1> { 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); println!("p3.x = {}, p3.y = {}", p3.x, p3.y); }
在 中,我们定义了一个 for (值为 )
和一个 for (值为 )。变量是一个结构
它有一个字符串 slice for (with value ) 和一个 for (with value )。用参数调用 on 得到我们 ,
它将具有一个 for because 来自 。变量
将有一个 for 因为来自 。宏
call 将打印 。main
Point
i32
x
5
f64
y
10.4
p2
Point
x
"Hello"
char
y
c
mixup
p1
p2
p3
i32
x
x
p1
p3
char
y
y
p2
println!
p3.x = 5, p3.y = c
此示例的目的是演示一些泛型
参数是使用
定义。在这里,泛型参数 and 是在 之后声明的,因为它们与 struct 定义一起。泛型参数 和 在 之后声明,因为它们仅与
方法。impl
X1
Y1
impl
X2
Y2
fn mixup
使用泛型的代码性能
您可能想知道使用泛型类型时是否有运行时成本 参数。好消息是,使用泛型类型不会使您的程序 运行速度比使用 Concrete 类型慢。
Rust 通过使用 generics 的 generics 进行编译。Monomorphization 是转为泛型 code 转换为特定代码,方法是填写 编译。在这个过程中,编译器执行与我们使用的步骤相反的作 创建示例 10-5 中的泛型函数:编译器会查看所有 调用泛型代码并为具体类型生成代码的位置 泛型代码被调用 with。
让我们通过使用标准库的通用枚举来看看它是如何工作的:Option<T>
#![allow(unused)] fn main() { let integer = Some(5); let float = Some(5.0); }
当 Rust 编译此代码时,它会执行单态化。在此期间
进程中,编译器会读取实例中已使用的值,并识别两种值:一种是 IS 和另一种
是。因此,它将 的通用定义扩展为两个
专门用于 和 的定义,从而替换泛型
定义与特定 Ones 一起使用。Option<T>
Option<T>
i32
f64
Option<T>
i32
f64
代码的单态化版本类似于以下内容( compiler 使用的名称与我们在此处使用的名称不同):
文件名: 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 将泛型代码编译成指定
type 时,我们无需为使用泛型支付运行时成本。当代码
运行,它的性能就像我们通过
手。单态化过程使 Rust 的泛型非常高效
在运行时。Option<T>
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准