Rust 泛型
泛型 Generic Data Type
Generic data type——泛型,泛型可以让我们在同一段代码中使用不同的数据类型,大大减少了一些情况下重复的代码量。
定义
在函数中使用
我们可以在函数中使用泛型,我们需要在函数名后声明需要使用的泛型,然后我们就可以在函数的形参和返回值中使用刚才定义的泛型参数。在Rust中,通常使用大写字母T
或U
来定义泛型,T
是单词type的第一个字母。使用单个字母可以让泛型的定义更加简洁高效。
- #Rust 代码公约#:为泛型的名字使用简短的大写字母,通常只有一个字母,如
T
或U
。
fn largest<T>(list: &[T]) -> &T {
以上是一个使用了泛型的函数。在函数名largest
后,使用<>
包裹需要声明的泛型。我们可以这样读出这个定义:一个拥有类型T
的函数largest
。这个函数拥有一个形参list
,它的类型是T
的切片。
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}");
}
在结构体中使用
我们同样可以在结构体中使用泛型,其语法与在函数中使用泛型类似:在结构体名称后,使用<>
包裹需要声明的泛型参数,然后我们就可以在函数体中使用它们。
// 在Point结构体中,我们定义了T类型
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 };
}
以上是一个使用了泛型的结构体,结构体中的x
和y
可以同时拥有任意类型的数据。需要注意的是,此处我们只使用了一个泛型T
,在结构体中,x
和y
为相同的类型,所以以下代码无法通过编译。
// 这段代码无法通过编译
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
如果我们想让x
和y
使用不同的类型,我们可以在结构体的定义中,添加另一个泛型参数U
的声明,然后在结构体中使用它。
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>
枚举,它的定义如下。
enum Option<T> {
Some(T),
None,
}
Option<T>
枚举是一个拥有泛型T
和两个变量的枚举。Some
用来保存一个T
类型的值,而None
不保存任何值。通过使用Option<T>
枚举类型,我们可以表达可选的值这个抽象的概念。并且,因为Option<T>
是泛化的,在值为任何类型的情况下都可以使用。
与结构体一样,枚举类型也可以使用多个泛型参数,就像标准库中Result<T, E>
的定义一样。
enum Result<T, E> {
Ok(T),
Err(E),
}
Result<T, E>
是一个拥有两个泛型T
和E
,并且拥有两个变量的枚举类型。ok
用于保存一个T
类型的数据,而Err
用于保存一个E
类型的数据。这样的定义让我们在处理一个可能失败的操作时非常方便,如果该操作成功了,那么返回值会被ok
所保存;如果该操作失败了,那么错误的值会被Err
所保存。
在方法的定义中使用
我们可以为结构体和枚举实现方法,并且在方法的定义中使用泛型。
struct Point<T> {
x: T,
y: T,
}
// 在这里,我们定义了一个名为`x`的方法,它会返回结构体内数据的引用。
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
,声明后,我们才能够在方法的实现中使用它。通过在impl
后声明T
是一个泛型,Rust 才能够确定尖括号内的T
不是一个确定的类型,而是一个泛型参数。尽管我们可以在方法的实现中为泛型取T
以外的名字,但与结构体中使用相同的名称是 Rust 的习惯。
- #Rust 代码公约#:在结构体的方法中,使用与结构体定义中同名的泛型。
我们还可以为特定的类型实现方法。
// 注意:在impl后没有类型声明
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
在上方的例子中,我们为f32
类型实现了名为distance_from_origin
的方法。在这个方法中,powi()
方法只能用于浮点类型上,如果我们需要为整型也实现相同的功能,就需要单独为i32
等类型实现特定的方法。
在结构体的方法中使用的泛型参数,并总与在该方法所对应的结构体中的泛型参数相同。在下面的这个例子中,我们在Point
结构体的定义中使用了X1
和X2
作为泛型参数,在mixup
方法中使用了X2
和Y2
作为泛型参数,以此使得我们的代码更加简单易懂。
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);
}
在实际应用中,我们不仅可以在impl
后定义泛型参数,我们也可以在方法中定义泛型参数。
泛型的性能
在 Rust 中,泛型零成本抽象。Rust 编译器在编译阶段,会对所有使用了泛型的代码进行 Monomorphization,即在编译期为所有使用了泛型的代码,填充具体类型。 例如下面这段代码,它声明了两个含有不同数据类型的Some
枚举。
let integer = Some(5);
let float = Some(5.0);
经过 monomorphization 的代码看起来和下面的代码相似(编译器为不同的类型使用不同的名字,并将它们的具体实现全部展开)。
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);
}