Rust 最与众不同的功能
之前学习中,有多种可能类型时必须注明类型;同理,引用的生命周期以一些不同方式相关联时,需要使用泛型生命周期参数来注明关系,这样就能确保运行时实际使用的引用有效
{
// 声明了没有初始值的变量,这些变量存在于外部作用域
// 尝试在给它一个值之前使用这个变量,会出现一个编译错误
// Rust 不允许空值
let r;
{
let x = 5;
r = &x;
// r 引用的值在尝试使用之前就离开了作用域
}
println!("r: {}", r);
}
Rust 编译器有一个 借用检查器(borrow checker)
它比较作用域来确保所有的借用都是有效
下文编译不通过,返回值需要一个泛型生命周期参数
生命周期标注并不改变任何引用的生命周期的长短
生命周期标注描述多个引用生命周期相互的关系,不影响其生命周期
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
// 获取作为引用的字符串 slice,因为不希望 longest 函数获取参数的所有
// 期望该函数接受 String 的 slice(参数 string1 的类型)和字符串字面量
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
// error[E0106]: missing lifetime specifier
// 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
// 返回值需要一个泛型生命周期参数,Rust 并不知道要返回的引用指向 x/y
// 为了修复这个错误,将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行分析
// 位于引用的 & 之后,并有一个空格来将引用类型与生命周期标注分隔开
&i32 // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用
单个生命周期标注本身没有多少意义,生命周期标注告诉 Rust 多个引用的泛型生命周期参数如何相互联系
如果函数有一个生命周期 'a 的 i32 的引用的参数 first,有一个生命周期 'a 的 i32 的引用的参数 second,这两生命周期标注意味着引用 first 和 second 必须与这泛型生命周期存在一样久
当函数签名中指定了泛型类型参数后就可以接受任何类型
当指定了泛型生命周期后函数能接受任何生命周期的引用
【手动标记生命周期的原因】
当函数引用或被函数之外的代码引用时,让 Rust 自身分析出参数或返回值的生命周期几乎是不可能的。这些生命周期在每次函数被调用时都可能不同。这也就是为什么需要手动标记生命周期
// 像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中
// 它告诉 Rust,关于参数中引用和返回值之间的限制是他们都必须拥有相同的生命周期
// longest 函数返回的引用的生命周期与传入该函数的引用的生命周期的较小者一致
// 即当具体引用被传递给 longest 时,被 'a 所替代的具体生命周期是 x 的作用域与 y 的作用域相重叠的那一部分
// 泛型生命周期 'a 的具体生命周期等同于 x 和 y 的生命周期中较小的那一个
// 因为用相同的生命周期参数 'a 标注了返回的引用值,所以返回的引用值就能保证在 x 和 y 中较短的那个生命周期结束之前保持有效
//
// 通过在函数签名中指定生命周期参数时,并没有改变任何传入值或返回值的生命周期
// 指出任何不满足这个约束条件的值都将被借用检查器拒绝
// 在函数中使用生命周期标注时,标注出现在函数签名中,不存在于函数体中任何代码中
// 参数和返回值都是字符串slice
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
// string1 的作用域较长
let string1 = String::from("long string is long");
{
// string2的 作用域仅限于此
let string2 = String::from("xyz");
// result的作用域同string2
let result = longest(string1.as_str(), string2.as_str());
// 正常打印
println!("The longest string is {}", result);
}
// 如下将编译失败
// let result;
// {
// let string2 = String::from("xyz");
// result = longest(string1.as_str(), string2.as_str());
// }
// println!("The longest string is {}", result);
// error[E0597]: `string2` does not live long enough
}
// 打印结果如下:
// The longest string is long string is long
// 如果将 longest 函数的实现修改为总是返回第一个参数而不是最长的字符串 slice
// 就不需要为参数 y 指定一个生命周期
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配
如果返回的引用没有指向任何参数,唯一的可能就是它指向一个函数内部创建的值,它将会是一个悬垂引用,会在函数结束时离开作用域
fn longest<'a>(x: &str, y: &str) -> &'a str {
// 即便为返回值指定了生命周期参数 'a,编译失败
// 因为返回值的生命周期与参数完全没有关联
// error[E0597]: `result` does not live long enough
let result = String::from("really long string");
// result 在 longest 函数的结尾将离开作用域并被清理
// 而尝试从函数返回一个 result 的引用,无法指定生命周期参数来改变悬垂引用
result.as_str()
}
解决方案:返回一个有所有权的数据类型而不是一个引用,由函数调用者清理它
生命周期语法用于将函数的多个参数与其返回值的生命周期进行关联。
一旦他们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为
// 定义包含引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期标注
// 必须在结构体名称后面的尖括号中声明泛型生命周期参数,在结构体定义中使用生命周期参数
struct ImportantExcerpt<'a> {
// 存放一个字符串 slice
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.')
.next()
.expect("Could not find a '.'");
// ImportantExcerpt 的实例i不能比其 part 字段中的引用存在的更久
let i = ImportantExcerpt { part: first_sentence };
}
每一个引用都有一个生命周期
需要为那些使用了引用的函数或结构体指定生命周期
但下文没有指定,但是编译正常,这就是生命周期的省略
【历史原因】
早期版本(pre-1.0)的 Rust ,不能编译
Rust 团队发现特定情况下开发者总是重复地编写一模一样的生命周期标注
这些场景可预测且遵循几个明确的模式。
Rust 团队就把这些模式编码进了 Rust 编译器中,如此借用检查器在这些情况下就能推断出生命周期而不再强制开发者显式的增加标注。
未来只会需要更少的生命周期标注。
如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么,直接编译报错
【概念】
输入生命周期(input lifetimes) :函数或方法的参数的生命周期
输出生命周期(output lifetimes):返回值的生命周期
生命周期省略规则(lifetime elision rules):被编码进 Rust 引用分析的模式。这并不是需要开发者遵守的规则;这些规则是一系列特定的场景,此时编译器会考虑,如果代码符合这些场景,就无需明确指定生命周期。
【生命周期规则】
(1)输入:每一个是引用的参数都有它自己的生命周期参数
(2)输出:只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数
(3)输出:如果方法有多个输入生命周期参数并且其中一个参数是 &self 或 &mut self,说明是个对象的方法(method), 那么所有输出生命周期参数被赋予 self 的生命周期
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[..]
}
它的隐式解析如下:
// 开始时签名中的引用并没有关联任何生命周期
fn first_word(s: &str) -> &str {
// 编译器应用第一条规则:每个引用参数都有其自己的生命周期,假设规则为a:
fn first_word<'a>(s: &'a str) -> &str {
// 编译器应用第二条规则:输入参数的生命周期将被赋予输出生命周期参数:
fn first_word<'a>(s: &'a str) -> &'a str {
编译器可以继续它的分析而无须开发者标记这个函数签名中的生命周期
另一个例子
fn longest(x: &str, y: &str) -> &str {
// 应用规则1后就变成如下,但是规则2不适用,报错
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
为结构体实现方法时,结构体字段的生命周期必须总是在 impl 关键字之后声明,在结构体名称之后被使用
struct ImportantExcerpt<'a> {
// 存放一个字符串 slice
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
// 应用第一条生命周期规则,并不必须标注 self 引用的生命周期
fn level(&self) -> i32 {
3
}
}
规则1、3
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
// 应用第一条生命周期省略规则并给予 &self 和 announcement 各自的生命周期
// 其中一个参数是 &self,返回值类型被赋予了 &self 的生命周期
'static,其生命周期能够存活于整个程序期间
所有的字符串字面量都拥有 'static 生命周期
// 这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的
// 所有的字符串字面量都是 'static
let s: &'static str = "I have a static lifetime.";
将引用指定为 'static 之前,思考一下这个引用是否真的在整个程序的生命周期里都有效
代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,解决这些问题而不是指定一个 'static 的生命周期
use std::fmt::Display;
// ann 的类型是泛型 T,任何实现了Display trait的类型实例都可以放进去
// 生命周期参数 'a 和泛型类型参数 T 都位于函数名后的同一尖括号列表中
fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where T: Display
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}