包阅导读总结
1. `Rust`、`Lifetimes`、`Borrow Checker`、`Annotations`、`Explicit`
2. 本文主要介绍了 Rust 中的 Lifetimes 机制,包括其重要性、原理、语法和应用场景,还提及了 `’static` 生命周期及总结,帮助读者更好地理解和运用 Lifetimes。
3.
– Rust 中的 Lifetimes 是确保代码安全的基本机制。
– 对于复杂的 Rust 项目很重要,但理解有难度。
– 帮助借检查器确保借用有效。
– 教程的前提条件
– 至少具备 Rust 初学者水平。
– 熟悉泛型。
– 了解借检查器工作原理有帮助。
– 什么是 Lifetimes
– 用于明确程序中数据的生存周期。
– 变量的生命周期从初始化到销毁。
– 借检查器不能检测时需显式标注。
– 显式生命周期标注的语法
– 如 `’static`、`’a` 等。
– 多个生命周期标注类似泛型。
– 不同场景中的标注
– 函数:返回引用时需要。
– 结构体:字段为引用时需要。
– 方法:与函数和结构体类似。
– 枚举:字段为引用时需要。
– `’static` 生命周期
– 表示数据从初始化到程序结束。
– 可创建运行时变量,不可主动释放。
– 可用 `static` 关键字声明静态变量。
– 总结
– Lifetimes 帮助借检查器确保引用有效,必要时需显式标注。
– 标注用于处理引用的结构、函数和方法。
思维导图:
文章地址:https://www.freecodecamp.org/news/what-are-lifetimes-in-rust-explained-with-code-examples/
文章来源:freecodecamp.org
作者:Oduah Chigozie
发布时间:2024/9/6 20:03
语言:英文
总字数:1790字
预计阅读时间:8分钟
评分:85分
标签:Rust,生命周期,借用检查,内存安全,静态生命
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
Lifetimes are fundamental mechanisms in Rust. There’s a very high chance you’ll need to work with lifetimes in any Rust project that has any sort of complexity.
Even though they are important to Rust projects, lifetimes can be quite tricky to wrap your head around. So I created this guide to provide more clarity on what they are and when you should use them.
Prerequisites for this Tutorial
To get the most out of this tutorial, you’ll need the following:
-
At least beginner-level familiarity with Rust: This tutorial doesn’t help with learning how to code in Rust. It only helps with understanding lifetimes in Rust and how they work
-
Familiarity with generics: Generics in Rust work identically to how they do in popular programming languages. Knowledge of how generics work in any language would be helpful.
-
Knowing how the borrow checker works isn’t as much a requirement as the last two above, but it would be helpful. Knowledge of how lifetimes work also helps in understanding how the borrow checker works.
So, What are Lifetimes in Rust?
For Rust’s borrow checker to ensure safety throughout your code, it needs to know how long all the data in the program will live during its execution. This becomes difficult to do in certain situations, and those situations are where you need to use explicit lifetime annotations.
Lifetimes in Rust are mechanisms for ensuring that all borrows that occur within your code are valid. A variable’s lifetime is how long it lives within the program’s execution, starting from when it’s initialized and ending when it’s destroyed in the program.
The borrow checker can detect the lifetimes of variables in many cases. But in cases where it can’t, you have to assist it with explicit lifetime annotations.
The syntax for explicit lifetime annotations is a single quote followed by a set of characters for identification (for example, 'static
, 'a
) as in:
max<'a>
The lifetime annotation indicates that max
should live at most as long as 'a
.
Using multiple lifetimes follows the same syntax:
max<'a, 'b>
In this case, the lifetime annotations indicate that max
should live at most as long as 'a
and 'b
.
Explicit lifetime annotations are handled similarly to how generics are. Let’s take a look at an example:
fn max<'a>(s1: &'a str, s2: &'a str) -> &'a str { }
In the example, the lifetime annotations indicate that max
should live at most as long as the lifetimes of s1
or s2
. It also indicates that max
returns a reference that lives as long as s1
.
A Rust project has many cases that would require explicit lifetime annotations, and in the next few sections, we’ll go over each of them.
Lifetime Annotations in Functions
A function only needs an explicit lifetime annotation when it returns a reference from any of its arguments. Let’s take an example:
fn max<'a>(s1: &'a str, s2: &'a str) -> &'a str { if s1.len() > s2.len() { s1 } else { s2 }}
If you remove the lifetime annotations, you’ll get an LSP (Language Server Protocol) warning to include the lifetime annotations. If you ignore LSP’s warning message and compile the code, you’ll get the same message as a compiler error. For example:
fn max(s1: &str, s2: &str) -> &str { if s1.len() > s2.len() { s1 } else { s2 }}
On the other hand, a function doesn’t need explicit lifetimes if it isn’t returning a reference in its arguments. For example:
fn print_longest(s1: &str, s2: &str) { if s1.len() > s2.len() { println!("{s1} is longer than {s2}") } else { println!("{s2} is longer than {s1}") }}
A function returning a different value doesn’t need explicit lifetime annotations either:
fn join_strs(s1: &str, s2: &str) -> String { let mut joint_string = String::from(s1); joint_string.push_str(s2); return joint_string;}
You only need to specify lifetimes if a function returns a reference from one of its arguments that is a borrowed reference.
Lifetime Annotations in Structs
Structs require explicit lifetime annotations when any of their fields are references. This allows the borrow checker to ensure that the references in the struct’s fields live longer than the struct. For example:
struct Strs<'a, 'b> { x: &'a str, y: &'b str,}
Without the lifetime annotations, you’ll get a similar LSP and compiler error message to the one in the previous section:
struct OtherStruct { x: &str, y: &str,}
Lifetime Annotations in Methods
Lifetime annotations concerning methods can be done as annotations to standalone methods, impl
blocks, or traits. Let’s look at each of them:
Standalone Methods:
Annotating lifetimes on standalone methods is identical to annotating lifetimes in functions:
impl Struct { fn max<'a>(self: &Self, s1: &'a str, s2: &'a str) -> &'a str { if s1.len() > s2.len() { s1 } else { s2 } }}
impl
Blocks
Writing explicit lifetime annotations for impl
blocks is required if the struct it is associated with has lifetime annotations in its definition. This is the syntax for writing impl
blocks with explicit lifetime annotations:
struct Struct<'a> {}impl<'a> Struct<'a> {}
This allows any method you write in the impl
block to return a reference from Struct
. For example:
struct Strs<'a> { x: &'a str, y: &'a str,}impl<'a> Strs<'a> { fn max(self: &Self) -> &'a str { if self.y.len() > self.x.len() { self.y } else { self.x } }}
Traits
Lifetime annotations in traits are dependent on the methods that the trait defines.
Let’s look at one example. A method inside a trait definition can use explicit lifetime annotations as a standalone method, and the trait definition won’t require explicit lifetime annotations. Like so:
trait Max { fn longest_str<'a>(s1: &'a str, s2: &'a str) -> &'a str;}impl<'a> Max for Struct<'a> { fn longest_str(s1: &'a str, s2: &'a str) { if s1.len() > s2.len() { s1 } else { s2 } }}
If a trait method requires references from the struct its associated with, the trait’s definition would require explicit lifetime annotations. For example:
trait Max<'a> { fn max(self: &Self) -> &'a str;}
Which can be implemented this way:
struct Strs<'a> { x: &'a str, y: &'a str,}trait Max<'a> { fn max(self: &Self) -> &'a str;}impl<'a> Max<'a> for Strs<'a> { fn max(self: &Self) -> &'a str { if self.y.len() > self.x.len() { self.y } else { self.x } }}
Lifetime Annotations in Enums
Similar to structs, enums need explicit lifetime annotations if any of their fields are references. For example:
enum Either<'a> { Str(String), Ref(&'a String),}
The 'static
Lifetime
In many Rust projects, you’ll likely have encountered variables that are 'static
in lifetimes. In this section, we’ll go over a brief overview of what a 'static
lifetime is, how it works, and where it is commonly used.
'static
is a reserved lifetime name in Rust. It signifies that the data that a reference points to lives from where it is initialized to the end of the program. This differs slightly from static variables, which are stored directly in the program’s binary file. However, all static variables have a 'static
lifetime.
Variables with 'static
lifetimes can be created at runtime. But they can’t be dropped, only coerced into shorter lifetimes. For example:
fn max<'a>(s1: &'a str, s2: &'a str) -> &'a str { if s1.len() > s2.len() { s1 } else { s2 }}fn main() { let first = "First string"; { let second = "Second string"; println!("The biggest of {} and {} is {}", first, second, max(first, second)); };}
String literals are examples of values with 'static
lifetimes. They are also stored in the program’s binary file and can be created at runtime.
Rust allows you to declare static variables with the static
keyword, using this syntax:
static IDENTIFIER: &'static str = "value";
Static variables can be declared in any scope, including the global scope. This means that you can use static variables as global variables. For example:
static FIRST_NAME: &'static str = "John";static LAST_NAME: &'static str = "Doe";fn main() { println!("First name: {}", FIRST_NAME); println!("Last name: {}", LAST_NAME);}
Static variables can also be mutable or immutable. But working with mutable static variables is only allowed in unsafe
blocks because they’re unsafe.
static mut FIRST_NAME: &'static str = "John";static LAST_NAME: &'static str = "Doe";fn main() { unsafe { println!("First name: {}", FIRST_NAME); } println!("Last name: {}", LAST_NAME); unsafe { FIRST_NAME = "Jane"; println!("First name changed to: {}", FIRST_NAME); }}
Summary
Lifetimes in Rust help the borrow checker ensure that all borrowed references are valid. The borrow checker can detect the lifetimes of variables in many cases, but in cases where it can’t you have to assist it with explicit lifetime annotations.
Explicit lifetime annotations are those 'a
, 'b
, and 'static
things you see in many Rust projects. You only need to use them in structures (structs, enums, traits, and impls) that deal with references, and in functions or methods that receive and return references.
In this guide, you learned about explicit lifetime annotations and saw some examples of how to use them. I it gave you some clarity on the topic, and helped you understand lifetimes better.
Thanks for reading!