所有的编程语言都会致力于高效地处理重复概念,Rust 中的泛型(generics)就是这样一种工具。泛型是具体类型或其他属性的抽象替代。比如 Option
、Vec
、Hash
等。
将代码提取为函数以减少重复工作
下面的代码可以用来在数字列表中找到最大值:
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = number_list[0];
for number in number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {}", largest);
}
为了消除重复代码,可以通过定义函数来创建抽象,令该函数可以接收任意整数列表作为参数并进行求值。
fn largest(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list.iter() {
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);
}
假设我们拥有两个不同的函数:一个用于在 i32 切片中搜索最大值;另一个用于在 char 切片中搜索最大值。代码可能是下面这个样子:
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 number_list = vec![34, 50, 25, 100, 65];
let result = largest_i32(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&char_list);
println!("The largest char is {}", result);
}
泛型数据类型
在函数定义中使用
当使用泛型来定义一个函数时,我们需要将泛型放置在函数签名中用于指定参数和返回值类型的地方。
以这种方式编写的代码更加灵活,可以在不引入重复代码的同时向函数调用者提供更多的功能。
上面代码中的 largest_i32
和 largest_char
是两个只在名称和签名上有所区别的函数。largest_i32
作用于 i32 类型的切片,而 largest_char
作用于 char 类型的切片。
这两个函数拥有完全相同的代码,因此可以通过在一个函数中使用泛型来消除重复代码。
在函数签名中使用泛型合并不同的 largest 函数:
fn largest(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
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);
}
其中 largest
部分的 PartialOrd
和 Copy
是为类型 T 指定的两个 trait 约束(后面会提到)。
在结构体定义中使用
同样地,也可以使用 <>
语法来定义在一个或多个字段中使用泛型的结构体。
struct Point {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 1 };
let float = Point { x: 1.0, y: 4.0 };
}
如上面的代码,在结构名后的一对尖括号中声明泛型参数后,就可以在结构体定义中用于指定具体数据类型的位置使用泛型了。
在定义 Point
结构体时仅使用了一个泛型参数,表明该结构体对某个类型 T
是通用的。但无论 T
具体的类型是什么,字段 x
和 y
都同时属于这个类型。即 x
和 y
只能是同一类型。
为了使结构体 Point 中的 x 和 y 能够被实例化为不同的类型,可以使用多个泛型参数。
struct Point {
x: T,
y: U,
}
fn main() {
let both_integer = Point { x: 5, y: 1 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
}
在方法定义中使用
方法也可以在自己的定义中使用泛型:
struct Point {
x: T,
y: T,
}
impl Point {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
上面的代码为结构体 Point
实现了名为 x
的方法,返回一个指向 x 字段中 T 类型值的引用。
紧跟着 impl 关键字声明 T 是必须的。通过在 impl 之后将 T 声明为泛型,Rust 能够识别出 Point
中尖括号内的类型是泛型而不是具体的类型。
实际上,可以单独为 Point
实例而不是所有的 Point
泛型实例来实现特定的方法。
当在 Point<32>
声明中使用了明确的类型 f32,也意味着无需在 impl 之后附带任何类型声明了。
impl Point {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
上面的代码意味着,类型 Point
将会拥有一个名为 distance_from_origin
的方法,而其他的 Point
实例则没有该方法的定义。
结构体定义中的泛型参数并不总是与方法签名中使用的类型参数一致。
struct Point {
x: T,
y: U,
}
impl Point {
fn mixup(self, other: Point) -> Point {
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);
// => p3.x = 5, p3.y = c
}
trait:定义共享行为
trait 用来向 Rust 编译器描述某些特定类型拥有的且能够被其他类型共享的功能,使我们可以以一种抽象的方式来定义共享行为。
trait 与其他语言中的接口(interface)功能类似,但也不尽相同。
类型的行为由该类型本身可供调用的方法组成。当我们可以在不同的类型上调用相同的方法时,就称这些类型共享了相同的行为。
trait 提供了一种将特定方法组合起来的途径,定义了为达成某种目的所必须的方法(行为)集合。
定义 trait
假如我们拥有多个结构体(struct),分别持有不同类型、不同数量的文本字段。其中 NewsArticle 结构体存放新闻故事,Tweet 结构体存放推文。
我们还想要方便地获取存储在 NewsArticle 和 Tweet 实例中的数据摘要。因此需要为每个结构体类型都实现摘要行为,从而可以在这些实例上统一地调用 summarize
方法来请求摘要内容。
可以定义如下形式的 Summary trait:
trait Summary {
fn summarize(&self) -> String;
}
在大括号中声明了用于定义类型行为的方法签名,即 fn summarize(&self) -> String;
。
方法签名后省略了大括号及方法的具体实现。任何想要实现这个 trait 的类型都需要为上述方法提供自定义行为。编译器会确保每一个实现了 Summary trait 的类型都定义了与这个签名完全一致的 summarize 方法。
一个 trait 可以包含多个方法,每个方法签名占据单独一行并以分号结尾。
为类型实现 trait
完整代码:
trait Summary {
fn summarize(&self) -> String;
}
struct NewsArticle {
headline: String,
location: String,
author: String,
content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
struct Tweet {
username: String,
content: String,
reply: bool,
retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
// => 1 new tweet: horse_ebooks: of course, as you probably already know, people
}
其中 impl Summary for NewsArticle
和 impl Summary for Tweet
部分负责为 NewsArticle 和 Tweet 两个结构体类型定义 Summary trait 中指定的 summarize
方法,并为该方法实现具体的行为。
默认实现
某些时候,为 trait 中的某些或所有方法都提供默认行为非常有用,使我们无需为每一个类型的 trait 实现都提供自定义行为。
当我们为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。
如为 Summary trait 中的 summarize
方法指定一个默认的字符串返回值:
trait Summary {
fn summarize(&self) -> String {
String::from("(Read More...)")
}
}
假如需要在 NewsArticle 的实例中使用上述默认实现,而不是自定义实现,可以指定一个空的 impl 代码块:
impl Summary for NewsArticle {}
此时虽然没有直接为 NewsArticle 定义 summarize
方法,依然可以在 NewsArticle 实例上调用 summarize
方法。
fn main() {
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best
hockey team in the NHL.",
),
};
println!("New article available! {}", article.summarize());
// => New article available! (Read More...)
}
可以在默认实现中调用同一 trait 中的其他方法,哪怕这些被调用的方法没有默认实现。例如,可以为 Summary trait 定义一个需要被实现的方法 summarize_author
(即 trait 中没有该方法的默认实现,需要在后续的类型中实现),再通过调用 summarize_author
为 summarize
方法提供一个默认实现:
trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
为了使用这个版本的 Summary,只需要在后续类型实现这一 trait 时定义 summarize_author
方法:
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
定义了 summarize_author
之后,就可以在 Tweet 实例上调用 summarize
了。summarize
的默认实现会进一步调用我们提供的 summarize_author
的定义。
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
// => 1 new tweet: (Read more from @horse_ebooks...)
}
trait 作为参数
前面的代码中为 NewsArticle 和 Tweet 类型实现了 Summary trait,我们还可以定义一个 notify 函数来调用这些类型的 summarize
方法。语法如下:
fn notify(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}
上述代码没有为 item
参数指定具体的类型,而是使用了 impl
关键字及对应的 trait 名称。
这意味着 item
参数可以接收任何实现了指定 trait 的类型。在 notify
函数体内,则可以调用来自 Summary trait 的任何方法。
尝试使用其他类型(如 String
或 i32
)来调用 notify
函数则无法通过编译,因为这些类型没有实现 Summary trait。
上述代码其实只是 trait 约束的一种语法糖,完整形式如下:
fn notify(item: T) {
println!("Breaking news! {}", item.summarize());
通过 + 语法来指定多个 trait 约束
如果 notify 函数需要在调用 summarize 方法的同时显示格式化后的 item,则此处的 item 就必须实现两个不同的 trait:Summary 和 Display。
fn notify(item: impl Summary + Display) {
这一语法在泛型的 trait 约束中同样有效:
fn notify
where 从句简化 trait 约束
因为每个泛型都拥有自己的 trait 约束,定义多个类型参数的函数可能会有大量的 trait 约束信息需要被填写在函数名与参数列表之间。Rust 提供了一种替代语法。
如 fn some_function
可以改写成如下形式:
fn some_function(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
返回实现了 trait 的类型
同样可以在返回值中使用 impl Trait 语法,用于返回某种实现了特定 trait 的类型。
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}
之前在介绍泛型时编写的 largest
函数就通过 trait 约束来限定泛型参数的具体类型。
在 largest
函数中,我们想要使用大于号运算符来比较两个 T 类型的值。这一运算符被定义为标准库 std::cmp::PartialOrd
的一个默认方法,因此需要在 T 的 trait 约束中指定 PartialOrd
,才能够使 largest
函数用于任何可比较类型的切片上。
我们在编写 largest
函数的非泛型版本时,只尝试过搜索 i32
和 char
类型的最大值。这两种都是拥有确定大小并存储在栈上的类型,实现了 Copy trait。
但当我们尝试将 largest
函数泛型化时,list 参数中的类型有可能是没有实现 Copy trait 的。为了确保这个函数只会被那些实现了 Copy trait 的类型所调用,还需要把 Copy 加入到 T 的 trait 约束中。
所以最终的 largest
函数采用如下声明:
fn largest
使用 trait 约束有条件地实现方法
通过在带有泛型参数的 impl 代码块中使用 trait 约束,我们可以单独为实现了指定 trait 的类型编写方法。
use std::fmt::Display;
struct Pair {
x: T,
y: T,
}
impl Pair {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl Pair {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
fn main() {
let pair = Pair::new(3, 4);
pair.cmp_display()
}
上面的代码中,所有的 Pair
类型都会实现 new
方法,但只有在内部类型 T
实现了 PartialOrd
(用于比较)和 Display
(用于打印)这两个 trait 的前提下,才会实现 cmd_display
方法。
总结
借助于 trait 和 trait 约束,我们可以在使用泛型参数消除重复代码的同时,向编译器指明自己希望泛型拥有的功能。而编译器则可以利用这些 trait 约束信息来确保代码中使用的具体类型提供了正确的行为。
在动态语言中,尝试调用类型没有实现的方法会导致在运行时出现错误。Rust 将这些错误出现的时机转移到了编译期,我们无需编写那些用于在运行时检查类型的代码,这一机制在保留泛型灵活性的同时提升了代码性能。
参考资料
The Rust Programming Language