[TOC]
泛型程序设计是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。泛型编程的中心思想是从携带类型信息的具体的算法中抽象出来,得到一种可以与不同的数据类型表示相结合的算法,从而生成各种有用的软件。泛型编程是一种软件工程中的解耦方法,很多时候,我们的算法并不依赖某种特定的具体类型,通过这种方法,我们就可以将“类型”从算法和数据结构的具体示例中抽象出来。
考虑以下问题:编写一个函数,这个函数接收两个数字,然后返回较大的那个数字。
fn largest(a: u32, b: u32) -> u32 {
if a > b {
a
} else {
b
}
}
这个函数能工作,但它只能比较两个 u32 类型数字的大小。现在除了想比较两个 u32 外,还想比较两个 f32。有一种可以行的办法,我们可以定义多个 largest 函数,让它们分别叫做 largest_u32
,largest_f32
… 这能正常工作,但不太美观。我们可以使用泛型语法对上述代码进行修改:
fn largest<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
if a > b {
a
} else {
b
}
}
fn main() {
println!("{}", largest::<u32>(1, 2));
println!("{}", largest::<f32>(1.0, 2.1));
}
其中,std::cmp::PartialOrd
被称作泛型绑定,在之后的课程中我们会对此进行解释。
我们还可以使用泛型语法定义结构体,结构体中的字段可以使用泛型类型参数。下面的代码展示了使用 Point<T>
结构来保存任何类型的 x 和 y 坐标值。
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 都是同一类型的 Point 结构体,但同时一个结构体中也可以包含多个不同的泛型参数:
struct Point<T, U> {
x: T,
y: T,
z: U,
}
fn main() {
let integer = Point { x: 5, y: 10, z: 15.0 };
let float = Point { x: 1.0, y: 4.0, z: 8 };
}
但是要注意,虽然一个结构体中可以包含任意多的泛型参数,但我仍然建议拆分结构体以使得一个结构体中只使用一个泛型参数。过多的泛型参数会使得阅读代码的人难以阅读。
我们可以在带泛型的结构体上实现方法,它的语法与普通结构体方法相差不大,只是要注意在它们的定义中加上泛型类型:
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<f32>
有效。
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
某一类数据可能含有一些共同的行为:例如它们能被显示在屏幕上,或者能相互之间比较大小。我们将这种共同的行为称作 Traits。我们使用标准库 std::fmt::Display
这个 traits 举例,这个 traits 实现了在 Formatter
中使用空白格式 {}
的功能。
pub trait Display {
pub fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}
use std::fmt;
struct Point {
x: i32,
y: i32,
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
let origin = Point { x: 0, y: 0 };
assert_eq!(format!("The origin is: {}", origin), "The origin is: (0, 0)");
在知道如何定义和实现 Traits 后,我们就可以探索如何使用 Traits 来定义接受许多不同类型的函数。这一切都与 Java 中的接口概念类似,也就是所谓的鸭子类型。事实上它们的使用场景也基本上是类似的。
我们定义一个 display
函数,它接收一个实现了 Display Traits 的参数 item。
pub fn display(item: &impl std::fmt::Display) {
println!("My display item is {}", item);
}
item 的参数类型是 impl std::fmt::Display
而不是某个具体的类型(例如 Point),这样,任何实现了 Display Traits 的数据类型都可以作为参数传入该函数。
Rust 编译器可以自动为我们的结构体实现一些 Traits,这种自动化技术被称作派生。例如,在编写代码的过程中最常见的一个需求就是将结构体输出的屏幕上,除了使用上节课提到的手工实现的 Display,也可以采用自动派生技术让 Rust 编译器自动帮你添加代码。
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("{:?}", p);
}
Debug
Trait 允许将数据结构使用 {:?}
格式进行格式化。
自动派生有一个前提是,该结构体中全部字段都实现了指定的 Trait,例如,上面例子中的 i32 和 i64 就已经实现了 Debug Trait。
现在,我们来为 Point 实现另一个 Trait:PartialEq
。该特征允许两个数据使用 ==
进行比较。
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 1, y: 2 };
println!("{}", p1 == p2);
}