20人参与 • 2025-02-26 • rust
悬垂引用是指引用指向的数据已经被释放,从而导致引用变得无效。rust 通过生命周期和借用检查器在编译时就捕获此类问题,从而避免运行时错误。
考虑下面这个示例(listing 10-16),该代码尝试将外部变量 r
设置为引用内层变量 x
的值,但内层变量在作用域结束后便被清理,从而使引用 r
指向已释放的数据:
fn main() { let r; // r 的作用域延伸到整个 main 函数 { let x = 5; r = &x; // 将 r 设为 x 的引用,此时 x 的生命周期仅在此块内 } // 此处 x 已经超出作用域,r 将成为悬垂引用 println!("r: {}", r); }
编译器会报错,提示 x does not live long enough
,这是因为 x
的生命周期比 r
短,无法保证 r
引用的内存始终有效。
rust 的借用检查器负责比较变量的作用域(生命周期),确保所有引用在使用时都是有效的。我们可以通过在代码中手动注解生命周期,来明确告诉编译器各引用的有效范围。
例如,在下面(listing 10-17)我们用 'a
和 'b
分别标注了 r
和 x
的生命周期:
fn main() { // 'a: r 的生命周期,延伸到整个 main let r: &'a i32; { // 'b: x 的生命周期,只在此块内 let x = 5; r = &x; } // 此时 r 引用的 x 的生命周期 'b 已结束,编译器将报错 println!("r: {}", r); }
在这段代码中,借用检查器比较了生命周期 'a
和 'b
,发现 r
的生命周期(外层)比它所引用的 x
的生命周期(内层)长,因此拒绝编译,从而防止了悬垂引用问题。
为修复这种错误,我们需要确保引用的生命周期不超过数据本身的生命周期。
例如,可以将 x
的声明移动到 r
的作用域内,确保它在整个使用期间有效:
fn main() { let x = 5; // x 的生命周期延伸到 main let r = &x; // r 引用 x,生命周期与 x 保持一致 println!("r: {}", r); }
这样编译器就能确认 r
引用的内存始终有效。
考虑一个返回较长字符串切片的函数 longest
。由于函数参数是引用,为了确保返回值引用有效,必须为引用指定生命周期。最初可能写成如下代码,但编译时会报错:
fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } }
编译器不知道返回的引用是属于 x
还是 y
的生命周期,因此报错。解决办法是在函数签名中为引用添加相同的生命周期参数,如下所示(listing 10-21):
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
这表示对于某个生命周期 'a
,x
和 y
的引用至少活跃 'a
时间,而返回的引用也保证至少活跃 'a
。当我们调用 longest
时,编译器会选择 x
和 y
中较短的那段生命周期作为返回引用的实际生命周期。
例如,在下面的代码中,string1
的生命周期长于 string2
的生命周期,所以返回的引用的生命周期为较短的 string2
的作用域范围(listing 10-22):
fn main() { let string1 = string::from("long string is long"); let result; { let string2 = string::from("xyz"); result = longest(string1.as_str(), string2.as_str()); println!("the longest string is {}", result); // 在此处,result 引用 string2 } // 如果在此处使用 result,就会出错,因为 string2 已经超出作用域 }
这样确保了引用的安全性:编译器拒绝在数据无效时使用引用。
如果结构体持有引用,则需要在结构体定义中为引用字段指定生命周期参数。
例如,定义一个保存字符串切片的结构体 importantexcerpt
(listing 10-24):
struct importantexcerpt<'a> { 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 '.'"); let excerpt = importantexcerpt { part: first_sentence }; println!("excerpt: {}", excerpt.part); }
这里,我们在结构体名后面声明了生命周期参数 'a
,并将其用在字段 part
的引用类型上。这意味着 importantexcerpt
的实例不能比它所持有的引用活得更久。
rust 设计了一套生命周期省略规则,让在大部分情况下无需显式标注生命周期。比如函数 first_word
(listing 10-25)在没有显式注解的情况下仍能编译:
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[..] }
编译器根据规则自动推断出所有引用应当共享相同的生命周期。但如果函数涉及多个引用且关系复杂,可能就需要手动添加生命周期注解了。
'static
是一个特殊的生命周期,表示引用可以在整个程序执行期间都有效。所有字符串字面值都具有 'static
生命周期:
let s: &'static str = "i have a static lifetime.";
通常我们不需要显式使用 'static
,除非遇到编译器建议或特殊需求。在大多数场景下,正确使用生命周期参数即可满足内存安全需求。
由于生命周期也是一种泛型,因此它们可以与类型泛型和特质约束一同使用。
例如,下面是一个扩展版的 longest
函数,它不仅返回较长的字符串切片,还接受一个额外的参数 ann
,要求该参数实现 display
特质(listing 10-11 综合示例):
use std::fmt::display; 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 } } fn main() { let string1 = string::from("abcd"); let string2 = "xyz"; let result = longest_with_an_announcement(string1.as_str(), string2, "comparing strings"); println!("the longest string is {}", result); }
在这个例子中,我们在函数名后面的尖括号中同时声明了生命周期参数 'a
和泛型类型参数 t
。t
通过 where
子句被约束为必须实现 display
,保证我们可以使用 {}
格式化输出 ann
。同时,返回的引用的生命周期为 'a
,确保它与输入参数中的较短生命周期一致。
本文介绍了 rust 中如何通过生命周期注解来验证引用的有效性,并确保引用不会出现悬垂问题。我们讨论了:
'a
),使得函数能够安全返回引用,例子中展示了 longest
函数的正确写法。'static
生命周期以及如何将生命周期与泛型和特质约束相结合来编写更灵活的代码。通过这些机制,rust 将所有内存安全问题提前到了编译时检查,从而使得程序在运行时既高效又安全。希望这篇博客能帮助你理解并掌握 rust 中生命周期的使用,在实际开发中编写出既灵活又安全的代码!也希望大家多多支持代码网。
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论