泛型
泛型(generics)可以使得代码适应性更强,避免编写重复代码。
函数定义中的泛型
我们可以为 i32
类型编写以下代码:
fn largest_i32(list: &[i32]) -> Option<i32> {
let mut largest = list.get(0)?;
for item in list {
if item > largest {
= item;
largest }
}
return Some(*largest);
}
这工作得很好,但假设也需要为 char
找到最大值呢?当然可以原封不动地抄过来,但这不太优雅。此时我们可以使用泛型改写。
fn largest<T: PartialOrd>(list: &[T]) -> Option<&T> {
if list.is_empty() {
return None;
}
let mut largest = list.get(0)?;
for i in list {
if i > largest {
= i;
largest }
}
return Some(largest);
}
其中 PartialOrd
一个 trait
,T
为一个类型。该函数拥有泛型 T
;有一个参数
list
,其类型为 T
的切片;返回值为类型为
&T
的 Option
。
PartialOrd
表示该类型具有比较功能,所以可以找到最大值。
可以注意到,改写后的函数签名有细微的不同。返回的是
Option<&T>
,而不是
Option<T>
。这是因为我们并未要求 T
实现
Copy
trait。现在的函数既可以用于 Copy
也可以用于 Drop
trait。
如果想仅用于实现了 Copy
trait 的类型,可以这样写:
fn largest_copy<T: PartialOrd + Copy>(list: &[T]) -> Option<T> {
if list.is_empty() {
return None;
}
let mut largest = list.get(0)?;
for i in list {
if i > largest {
= i;
largest }
}
return Some(*largest);
}
其中 +
表示并,即需要 T
同时实现了
PartialOrd
trait 和 Copy
trait。
还可以用 where
子句改写函数签名,这在类型要求较多时比较很有用:
fn largest_copy<T>(list: &[T]) -> Option<T>
where
: PartialOrd + Copy T
结构体泛型
结构体也可以使用泛型:
struct Point<T> {
: T,
x: T,
y}
fn main() {
let p1 = Point { x: 5, y: 5 }; // ok
let p2 = Point { x: 5, y: 4.0 }; // compile-error
}
上述例子限制了 x
和 y
必须是相同类型。
也可以有多个泛型参数:
struct Point<T, U> {
: T,
x: U,
y}
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
和
Result
枚举就是极好的例子:
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
方法定义中的泛型
在为结构体和枚举实现方法时,可以使用泛型:
struct Point<T> {
: T,
x: T,
y}
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
,这样就可以在
Point<T>
上实现的方法中使用它。
也可以声明为某个具体类型的实现:
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
self.x.powi(2) + self.y.powi(2)).sqrt()
(}
}
这意味着,只有 Point<i32>
会有
distance_from_origin
方法,而非 f32
的
T
则没有。
结构体的泛型和方法的泛型不必一致,可以不同。以下实例清晰地解释了这一点:
struct Point<X1, Y1> {
: X1,
x: Y1,
y}
impl<X1, Y1> Point<X1, Y1> {
fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
{
Point : self.x,
x: other.y,
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);
}
零成本抽象
Rust 是零成本抽象的,相对于不使用泛型的代码,泛型方法没有任何运行时速度损失。
Rust 使用单态化(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);
}
trait
trait 类似其他语言中的接口(interface),但略有不同。trait 可以用一种抽象的方式定义共享的行为,使用 trait bounds,可以指定泛型所需的 trait。
定义
例如,我们有 NewsArticle
用于存放新闻,而结构体
Tweet
可以存放最多 280
个字符,以及像转推和回复这样的元数据。我们想要创建一个聚合器
aggregator
,用来显示可能存储在 NewsArticle
或
Tweet
实例中的数据摘要。
可以定义一个 Summary
trait 来表示这个行为:
pub trait Summary {
fn summarize(&self) -> String;
}
类似 impl
的声明,不过我们不提供具体实现。而是列举一系列必须的方法签名。
实现
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
与普通的 impl
类似,但需要在后面加上 for T
表示为谁实现 trait。
调用时,只需像调用普通方法一般调用:
fn main() {
let tweet = Tweet {
: String::from("horse_ebooks"),
username: String::from(
content"of course, as you probably already know, people",
,
): false,
reply: false,
retweet};
println!("1 new tweet: {}", tweet.summarize());
}
请注意,只有满足孤儿规则(orphan rule)的类型(结构体或枚举)才能实现对应的 trait,即:
- 被实现的类型
- 或要实现的 trait
其中之一,在本 crate。
也就是说,你可以为在标准库中的 Option<T>
实现
Summary
,也可以为 Tweet
实现在标准库中的
Display
。但不能为标准库中的 Option<T>
实现
Display
。这为了保证程序的相关性(coherence)。这条规则确保了代码不会被互相破坏。反过来说,如果没有这条跪在,两个
crate 可以同时为一个类型实现相同的 trait,而 Rust
无从得知使用哪一个实现。
默认实现
可以在 trait
中提供默认实现,而不是要求每个实现都自定义行为。我们可以为
summarize
方法指定一个默认的字符串值:
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
使用时留空就会使用默认实现:
impl Summary for NewsArticle {}
在有些时候,我们可以要求具体类型实现一小部分代码,而其他的逻辑可以使用默认值:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
请注意,Rust 不允许从重载中调用默认方法。
作为参数
这点在之前讲解泛型时,已经涉及了一点。我们可以使用 impl
关键字简写:
pub fn notify(item: &impl Summary) {
println!("BREAKING NEWS! {}", item.summarize);
}
// trait bound 写法
pub fn notify<T: Summary>(item: T)
// 多个参数
pub fn notify<T: Summary>(item: T, item: T)
// where
pub fn notify<T>(item: T)
where
: Summary T
通过 +
我们还可以指定多个 trait bound
:
pub fn notify(item: &(impl Summary + Display))
pub fn notify<T: Summary + Display>(item: &T)
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
: Clone + Debug U
也可以在返回值中使用:
fn returns_summarizable() -> impl Summary {
{
Tweet : String::from("horse_ebooks"),
username: String::from(
content"of course, as you probably already know, people",
,
): false,
reply: false,
retweet}
}
我们返回 trait,而不是具体类型,在类型较长的场景非常有用。
但 Rust 不允许返回 impl
的不同类型,比如这样,会造成编译错误:
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
{
NewsArticle : String::from(
headline"Penguins win the Stanley Cup Championship!",
,
): String::from("Pittsburgh, PA, USA"),
location: String::from("Iceburgh"),
author: String::from(
content"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
,
)}
} else {
{
Tweet : String::from("horse_ebooks"),
username: String::from(
content"of course, as you probably already know, people",
,
): false,
reply: false,
retweet}
}
}
因为 Rust 的泛型默认使用单态化来完成,以避免运行时性能损失,所以至多有一个实际类型。若要使得能够返回多种类型,需要使用动态分发,这会降低性能,但会提高灵活性。此问题的解决方案将在后面具体说明。
有条件地实现
如下面的代码块。我们可以限制 T
为特定的类型,并为这些类型实现对应的方法。
use std::fmt::Display;
struct Pair<T> {
: T,
x: T,
y}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
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);
}
}
}
在 Rust 标准库中不乏这样的例子:
impl<T: Display> ToString for T {
// ...
}
任何实现了 Display
trait 的类型,都同时拥有了
ToString
trait。
所以,可以直接对任何实现了 Display
trait 的类型,调用
to_string
方法:
let s = 3.to_string();
生命周期
Rust 中每个引用都有其生命周期(lifetime),也就是引用保持有效的作用域。大部分时候,生命周期可以被推断,正如类型也可以被推断一样。而有时候,我们需要手动标注生命周期,来保障运行时引用绝对有效。
避免悬垂引用
避免悬垂引用是生命周期的主要目的,悬垂引用会导致程序读取异常数据。不妨查看下面的例子:
{
let r;
{
let x = 5;
= &x;
r }
println!("r: {}", r);
}
此处的
r
先声明后赋值不代表 Rust 允许空值。运行时永远不会存在类似 JavaScriptundefined
之类的东西。尝试在赋值前使用r
,你会发现 Rust 将给出编译时错误。
外部作用域先声明了一个值。而内部作用域尝试将 x
赋给外部的 r
,并在外部作用域使用。但实际上 x
会在内部作用域结束时被消毁,所以 Rust
给出了错误提示。`x` does not live long enough
借用检查器
Rust 一大安全机制就是借用检查器(borrow checker),用来比较作用域,确保所有的借用都是有效的。查看下例:
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
= &x; // | |
r } // -+ |
// |
println!("r: {}", r); // |
} // ---------+
我们将 r
的生命周期标记为 'a
,将
x
的生命周期标记为 'b
。显然的,'b
比 'a
小得多——被引用的对象比引用者存在的时间更短,所以这是无效引用。
再来看一个没有错误的例子:
{
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+
这里 'b
比 'a
大,就表明 r
可以引用
x
。被引用对象比引用者存在的时间长,引用是有效的。
函数中的泛型生命周期
有一个无法通过编译的函数:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x} else {
y}
}
|
1 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
提示文本告诉我们,返回值需要一个泛型生命周期参数,因为 Rust
不知道返回的引用是指向 x
或
y
。我们也不知道传入引用的具体生命周期。所以我们需要标注生命周期泛型。
生命周期注解
生命周期必须以 '
开头,通常全小写。'a
是首选名称。位于引用的 &
后,并用一个空格将类型和生命周期分开。比如:
&i32 // 引用
&'a i32 // 显式生命周期引用
&'a mut i32 // 显式生命周期可变引用
单个生命周期注解没有多少意义,因为生命周期注解是为了告诉编译器,引用间的互相联系。若两个参数使用一个声明周期
'a
,则表明这两个参数需要和 'a
存在得一样久。
函数签名中的生命周期注解
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x} else {
y}
}
这段代码现在能编译了。并取得了预期的结果。
生命周期注解仅仅存在于函数签名,而不是具体的逻辑。这是函数约定的一部分。能够帮助编译器更准确得指出问题,保证运行时安全。
当具体的引用传递给 longest
时,'a
的实际生命周期,将是 x
和 y
的并集,也就是重叠的部分,x
和 y
中较小的那一个作用域。因为我们也用 'a
标注了返回的引用值,所以能够保证返回值在 'a
中有效。
以下代码能够通过编译:
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
}
而以下代码不行:
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
= longest(string1.as_str(), string2.as_str());
result }
println!("The longest string is {}", result);
因为后者在 string2
作用域之外使用了
result
,result
又有可能是 string2
的引用,会导致错误。所以 Rust 在编译时提前检查了这一问题。
从人的角度上来说,上述代码是正确的,因为 result
永远都会返回 string1
的引用。但逻辑复杂起来时,这种考虑需要深入到代码实现,仔细思考是否存在无效引用,将是极大的心智负担。所以应当使用统一的方法,避免手动检查,保证运行时安全。
结构体的生命周期
之前我们说过,结构体可以存放引用,但需要标注生命周期:
struct ImportantExcerpt<'a> {
: &'a str,
part}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
: first_sentence,
part};
}
和函数的生命周期类似,这可以保证,在 ImportantExcerpt
实例存在时,part
一定有效。
生命周期省略
生命周期省略(lifetime elision)让我们减少手动标注的次数和精力。之前我们写过一个函数,没有标生命周期也没有报错:
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
早期 Rust 必须对每个引用,都标注声明周期,这太麻烦了。所以开发者们尝试寻找了一些规则,让编译器自动标注。
函数或方法参数的生命周期被称为输入生命周期(input lifetimes),而返回值的生命周期则叫输出生命周期(output lifetimes)。
编译器具有 3 条规则,来推导默认的生命周期:
- 每个引用的参数,都是自身的生命周期参数。换句话说:
- 1 参数对 1
生命周期:
fn foo<'a>(x: &'a i32)
- 2 参数对 2
生命周期:
fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
- 1 参数对 1
生命周期:
- 若只有一个输入生命周期参数,那么输出生命周期参数将和其保持一致:
fn foo<'a>(x: &'a i32) -> &'a i32
- 若有多个生命周期参数,且其中一个参数是
&self
或&mut self
,则说明这是对象的方法。所有输出生命周期参数都赋予self
的生命周期。
看看下面几个例子:
- 原始函数:
fn first_word(s: &str) -> &str
- 应用第一条规则:
fn first_word<'a>(s: &'a str) -> &str
- 应用第二条:
fn first_word<'a>(s: &'a str) -> &'a str
- 第三条规则不适用
- 应用第一条规则:
- 原始函数:
fn longest(x: &str, y: &str) -> &str {
- 应用第一条规则:
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str
- 第二条和第三条都不适用
- 应用第一条规则:
方法定义中的生命周期
当我们需要写 impl
块时,语法和函数的类似。我们继续使用之前 ImportantExcerpt
的例子:
struct ImportantExcerpt<'a> {
: &'a str,
part}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
impl
块后面的泛型参数是不可省略的。
对于 level
方法,我们不需要标注,因为只有一个
&self
引用。
对于 announce_and_return_part
方法,Rust
应用第一条规则和第三条规则。相当于:
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'a str {
println!("Attention please: {}", announcement);
self.part
}
static
有一种特殊的生命周期,'static
,即对应的引用在整个程序的存活期间都有效。
默认情况下,字符串字面量就是 'static
的,因为这些值直接存储在二进制文件中,而且总是可用。也可以显式标注出来:
let s: &'static str = "I'm long live!"
在标注一个引用为 'static
之前,应该首先考虑这么做是否合理,即该值是否应该在整个程序运行时都可用。不应该使用
'static
解决悬垂引用和生命周期不匹配问题。
将本文内容总结成一个函数:
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(
: &'a str,
x: &'a str,
y: T,
ann-> &'a str
) where
: Display,
T{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x} else {
y}
}
以上就是和泛型、生命周期有关的内容了。