结构体,枚举,函数,方法中使用泛型数据类型
泛型
主要目的是为程序员提供了编程的便利,减少代码的臃肿,同时极大丰富了语言本身的表达能力。我们可以使用泛型
为像函数签名或结构体这样的项创建定义,这样它们就可以用于多种不同的具体数据类型。让我们看看如何使用泛型
定义函数、结构体、枚举和方法,然后我们将讨论泛型
如何影响代码性能。
当使用泛型
定义函数时,本来在函数签名中指定参数和返回值的类型的地方,会改用泛型
来表示。采用这种技术,使得代码适应性更强,从而为函数的调用者提供更多的功能,同时也避免了代码的重复。
看示例展示了两个函数,它们的功能都是寻找 slice
中最大值。接着我们使用泛型将其合并为一个函数:
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![11, 232, 63, 74, 51, 96];
let result = largest_i32(&number_list);
println!("最大的数字是:{}", result);
let char_list = vec!['a', 't', 'k', 's', 'x', 'l'];
let result = largest_char(&char_list);
println!("最大的字符是:{}", result);
}
largest_i32
它用来寻找slice
中最大的 i32
。largest_char
函数寻找 slice
中最大的 char
。因为两者函数体的代码是一样的,我们可以定义一个函数,再引进泛型参数来消除这种重复。
如果要在函数体中使用参数,就必须在函数签名中声明它的名字,好让编译器知道这个名字指代的是什么。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。为了定义泛型版本的largest
函数,类型参数声明位于函数名称与参数列表中间的尖括号<>
中,像这样:
fn largest<T>(list: &[T]) -> &T {
可以这样理解这个定义:函数 largest
有泛型类型 T
。它有个参数 list
,其类型是元素为T
的 slice
。largest
函数会返回一个与 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![11, 232, 63, 74, 51, 96];
let result = largest(&number_list);
println!("最大的数字是:{}", result);
let char_list = vec!['a', 't', 'k', 's', 'x', 'l'];
let result = largest(&char_list);
println!("最大的字符是:{}", result);
}
largest
函数在它的签名中使用了泛型,统一了两个实现。该示例也展示了如何调用 largest
函数,把i32
值的 slice
或char
值的 slice
传给它。请注意这些代码还不能编译,不过稍后在本章会解决这个问题:
$ cargo run
Compiling generics v0.1.0 (/rust/projects/generics)
error[E0369]: binary operation `>` cannot be applied to type `&T`
--> src/main.rs:4:17
|
4 | 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 `generics` due to previous error
帮助说明中提到了std::cmp::PartialOrd
,这是一个 trait
。下一部分会讲到 trait
。不过简单来说,这个错误表明 largest
的函数体不能适用于T
的所有可能的类型。因为在函数体需要比较 T
类型的值,不过它只能用于我们知道如何排序的类型。为了开启比较功能,标准库中定义的 std::cmp::PartialOrd`` trait
可以实现类型的比较功能。依照帮助说明中的建议,我们限制T
只对实现了 PartialOrd
的类型有效后代码就可以编译了,因为标准库为i32
和 char
实现了 PartialOrd
。
fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
同样也可以用<>
语法来定义结构体,它包含一个或多个泛型参数类型字段。定义一个可以存放任何类型的x
和 y
坐标值的结构体 Point
:
struct Point<T> {
x: T,
y: T,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 1.1, y: 2.2 };
}
其语法类似于函数定义中使用泛型。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。
注意 Point<T>
的定义中只使用了一个泛型类型,这个定义表明结构体 Point<T>
对于一些类型 T
是泛型的,而且字段x
和 y
都是 相同类型的,无论它具体是何类型。如果尝试创建一个有不同类型值的Point<T>
的实例,不能编译:
struct Point<T> {
x: T,
y: T,
}
fn main() {
let p1 = Point { x: 1, y: 2.1 };
}
在这个例子中,当把整型值1
赋值给 x
时,就告诉了编译器这个 Point<T>
实例中的泛型 T
是整型的。接着指定y
为 2.1
,它被定义为与x
不相同类型,就会得到一个像这样的类型不匹配错误:
$ cargo run
Compiling generics v0.1.0 (/rust/projects/generics)
error[E0308]: mismatched types
--> src/main.rs:7:31
|
7 | let p1 = Point { x: 1, y: 2.1 };
| ^^^ expected integer, found floating-point number
For more information about this error, try `rustc --explain E0308`.
error: could not compile `generics` due to previous error
如果想要定义一个 x
和 y
可以有不同类型且仍然是泛型的 Point
结构体,我们可以使用多个泛型类型参数。我们修改 Point
的定义为拥有两个泛型类型T
和U
。其中字段 x
是T
类型的,而字段 y
是 U
类型的:
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let p1 = Point { x: 1, y: 2.1 };
}
现在所有这些 Point
实例都合法了!你可以在定义中使用任意多的泛型类型参数,不过太多的话,代码将难以阅读和理解。当你发现代码中需要很多泛型时,这可能表明你的代码需要重构分解成更小的结构。
和结构体类似,枚举也可以在成员中存放泛型数据类型。我们很熟悉的Option<T>
枚举,这里再回顾一下:
enum Option<T> {
Some(T),
None,
}
现在这个定义应该更容易理解了。如你所见Option<T>
是一个拥有泛型 T
的枚举,它有两个成员:Some
,它存放了一个类型 T
的值,和不存在任何值的None
。通过 Option<T>
枚举可以表达有一个可能的值的抽象概念,同时因为 Option<T>
是泛型的,无论这个可能的值是什么类型都可以使用这个抽象。
枚举也可以拥有多个泛型类型。错误处理使用过的Result
枚举定义就是一个这样的例子:
enum Result<T, E> {
Ok(T),
Err(E),
}
Result
枚举有两个泛型类型,T
和 E
。Result
有两个成员:Ok
,它存放一个类型 T
的值,而Err
则存放一个类型 E
的值。这个定义使得Result
枚举能很方便的表达任何可能成功(返回 T
类型的值)也可能失败(返回 E
类型的值)的操作。实际上,当成功打开文件的时候,T
对应的是 std::fs::File
类型;而当打开文件出现问题时,E
的值则是 std::io::Error
类型。
在为结构体和枚举实现方法时,一样也可以用泛型。结构体 Point<T>
,和在其上实现的名为 x
的方法:
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn get_x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("p.x: {:?}", p.get_x());
}
这里在Point<T>
上定义了一个叫做get_x
的方法来返回字段x
中数据的引用。
注意必须在 impl
后面声明T
,这样就可以在 Point<T>
上实现的方法中使用 T
了。通过在 impl
之后声明泛型T
,Rust
就知道Point
的尖括号中的类型是泛型而不是具体类型。我们可以为泛型参数选择一个与结构体定义中声明的泛型参数所不同的名称。impl
中编写的方法声明了泛型类型可以定位为任何类型的实例,不管最终替换泛型类型的是何具体类型。
定义方法时也可以为泛型指定限制(constraint)
。例如,可以选择为Point<f32>
实例实现方法,而不是为泛型 Point
实例。一个没有在 impl
之后(的尖括号)声明泛型的例子,这里使用了一个具体类型,f32
:
struct Point<T> {
x: T,
y: T,
}
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) * self.y.powi(2)).sqrt()
}
}
fn main() {
let p: Point<f32> = Point { x: 1.2, y: 2.2 };
println!("p.x: {:?}", p.distance_from_origin());
}
这段代码意味着 Point<f32>
类型会有一个方法 distance_from_origin
,而其他T
不是 f32
类型的 Point<T>
实例则没有定义此方法。这个方法计算点实例与坐标 (0.0, 0.0)
之间的距离,并使用了只能用于浮点型的数学运算符。
结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型:
#[derive(Debug)]
struct Point<X1, Y1> {
x: X1,
y: Y1,
}
impl<X1, Y1> Point<X1, Y1> {
fn creat_point<X2, Y2>(self, other: Point<X2, Y2>) -> Point<Y1, X2> {
Point {
x: self.y,
y: other.x,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 2.0 };
let p2 = Point { x: "hello", y: 'c' };
let p3 = p1.creat_point(p2);
println!("p3 {:#?}", p3);
}
示例为 Point
结构体使用了泛型类型X1
和Y1
,为 creat_point
方法签名使用了 X2
和 Y2
来使得示例更加清楚。这个方法用 self
的 Point
类型的 y
值(类型 Y1
)和参数的Point
类型的x
值(类型 X2
)来创建一个新 Point
类型的实例。
在 main
函数中,定义了一个有 i32
类型的 x
(其值为 1
)和 f64
的 y
(其值为 2.0
)的 Point
。p2
则是一个有着字符串 slice
类型的x
(其值为 "hello"
)和char
类型的y
(其值为c
)的 Point
。在 p1
上以 p2
作为参数调用 creat_point
会返回一个 p3
,它会有一个 f64
类型的 y
,因为 y
来自 p1
,并拥有一个字符串 slice
类型的 x
,因为 x
来自p2
。println! 会打印出:
p3 Point {
x: 2.0,
y: "hello",
}
这个例子的目的是展示一些泛型通过 impl
声明而另一些通过方法定义声明的情况。这里泛型参数X1
和Y1
声明于impl
之后,因为他们与结构体定义相对应。而泛型参数 X2
和 Y2
声明于 fn creat_point
之后,因为他们只是相对于方法本身的。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!