10 KiB
수명
러스트는 이런 규칙들을 수명을 통해서 강제합니다. 수명은 레퍼런스가 유효해야 하는, 이름이 지어진 코드의 지역입니다. 이 지역들은 꽤나 복잡할 수 있는데, 프로그램의 실행 분기에 대응하기 때문입니다. 또한 이런 실행 분기에는 구멍까지도 있을 수 있는데, 레퍼런스가 다시 쓰이기 전에 재초기화된다면, 이 레퍼런스를 무효화할 수 있기 때문입니다. 레퍼런스를 포함하는 (또는 포함하는 척하는) 타입도 수명을 붙여서 러스트가 그것을 무효화하는 것을 막게 할 수 있습니다.
우리의 예제 대부분, 수명은 코드 구역과 대응할 것입니다. 이것은 우리의 예제가 간단하기 때문입니다. 그렇게 대응되지 않는 복잡한 경우는 밑에 서술하겠습니다.
함수 본문 안에서 러스트는 보통 관련된 수명을 명시적으로 쓰게 하지 않습니다. 이것은 지역적인 문맥에서 수명에 대해 말하는 것은 대부분 별로 필요없기 때문입니다: 러스트는 모든 정보를 가지고 있고, 모든 것들이 가능한 한 최적으로 동작하도록 할 수 있습니다. 컴파일러가 아니라면 일일히 작성해야 하는 많은 무명 구역과 경계들을 컴파일러가 대신해 주어서 당신의 코드가 "그냥 작동하게" 해 줍니다.
그러나 일단 함수 경계를 건너고 나면 수명에 대해서 이야기해야만 합니다. 수명은 보통 작은 따옴표로 표시됩니다: 'a
, 'static
처럼요. 수명이라는 주제에 발가락을 담그기 위해서, 우리는 코드 구역을 수명으로 수식할 수 있는 척을 하며, 이 챕터의 시작부터 있는 예제들의 문법적 설탕을 해독해 보겠습니다.
원래 우리의 예제들은 코드 구역과 수명에 대해서 공격적인 문법적 설탕-- 고당 옥수수콘 시럽 같은 --을 이용했는데, 모든 것들을 명시적으로 적는 것은 굉장히 요란하기 때문입니다. 모든 러스트 코드는 이런 식의 공격적인 추론과 "뻔한" 것들을 생략하는 것에 의존합니다.
문법적 설탕 중 하나의 특별한 조각은 모든 let
문장이 암시적으로 코드 구역을 시작한다는 점입니다. 대부분의 경우에는 이것이 문제가 되지는 않습니다. 그러나 서로 참조하는 변수들에게는 문제가 됩니다.
간단한 예제로, 이런 간단한 러스트 코드 조각을 해독해 봅시다:
let x = 0;
let y = &x;
let z = &y;
대여 검사기는 항상 수명의 길이를 최소화하려고 하기 때문에, 아마 이런 식으로 해독할 것입니다:
// 주의: `'a: {` 나 `&'b x` 는 유효한 문법이 아닙니다!
'a: {
let x: i32 = 0;
'b: {
let y: &'b i32 = &'b x;
'c: {
let z: &'c &'b i32 = &'c y; // "i32의 레퍼런스의 레퍼런스" (수명이 표시되어 있음)
}
}
}
와. 이건... 별로네요. 러스트가 이런 것들을 간단하게 만들어 준다는 것을 감사하는 우리가 됩시다.
Actually passing references to outer scopes will cause Rust to infer a larger lifetime:
let x = 0;
let z;
let y = &x;
z = y;
'a: {
let x: i32 = 0;
'b: {
let z: &'b i32;
'c: {
// Must use 'b here because the reference to x is
// being passed to the scope 'b.
let y: &'b i32 = &'b x;
z = y;
}
}
}
Example: references that outlive referents
Alright, let's look at some of those examples from before:
fn as_str(data: &u32) -> &str {
let s = format!("{}", data);
&s
}
desugars to:
fn as_str<'a>(data: &'a u32) -> &'a str {
'b: {
let s = format!("{}", data);
return &'a s;
}
}
This signature of as_str
takes a reference to a u32 with some lifetime, and
promises that it can produce a reference to a str that can live just as long.
Already we can see why this signature might be trouble. That basically implies
that we're going to find a str somewhere in the scope the reference
to the u32 originated in, or somewhere even earlier. That's a bit of a tall
order.
We then proceed to compute the string s
, and return a reference to it. Since
the contract of our function says the reference must outlive 'a
, that's the
lifetime we infer for the reference. Unfortunately, s
was defined in the
scope 'b
, so the only way this is sound is if 'b
contains 'a
-- which is
clearly false since 'a
must contain the function call itself. We have therefore
created a reference whose lifetime outlives its referent, which is literally
the first thing we said that references can't do. The compiler rightfully blows
up in our face.
To make this more clear, we can expand the example:
fn as_str<'a>(data: &'a u32) -> &'a str {
'b: {
let s = format!("{}", data);
return &'a s
}
}
fn main() {
'c: {
let x: u32 = 0;
'd: {
// An anonymous scope is introduced because the borrow does not
// need to last for the whole scope x is valid for. The return
// of as_str must find a str somewhere before this function
// call. Obviously not happening.
println!("{}", as_str::<'d>(&'d x));
}
}
}
Shoot!
Of course, the right way to write this function is as follows:
fn to_string(data: &u32) -> String {
format!("{}", data)
}
We must produce an owned value inside the function to return it! The only way
we could have returned an &'a str
would have been if it was in a field of the
&'a u32
, which is obviously not the case.
(Actually we could have also just returned a string literal, which as a global can be considered to reside at the bottom of the stack; though this limits our implementation just a bit.)
Example: aliasing a mutable reference
How about the other example:
let mut data = vec![1, 2, 3];
let x = &data[0];
data.push(4);
println!("{}", x);
'a: {
let mut data: Vec<i32> = vec![1, 2, 3];
'b: {
// 'b is as big as we need this borrow to be
// (just need to get to `println!`)
let x: &'b i32 = Index::index::<'b>(&'b data, 0);
'c: {
// Temporary scope because we don't need the
// &mut to last any longer.
Vec::push(&'c mut data, 4);
}
println!("{}", x);
}
}
The problem here is a bit more subtle and interesting. We want Rust to
reject this program for the following reason: We have a live shared reference x
to a descendant of data
when we try to take a mutable reference to data
to push
. This would create an aliased mutable reference, which would
violate the second rule of references.
However this is not at all how Rust reasons that this program is bad. Rust
doesn't understand that x
is a reference to a subpath of data
. It doesn't
understand Vec
at all. What it does see is that x
has to live for 'b
in
order to be printed. The signature of Index::index
subsequently demands that
the reference we take to data
has to survive for 'b
. When we try to call
push
, it then sees us try to make an &'c mut data
. Rust knows that 'c
is
contained within 'b
, and rejects our program because the &'b data
must still
be alive!
Here we see that the lifetime system is much more coarse than the reference semantics we're actually interested in preserving. For the most part, that's totally ok, because it keeps us from spending all day explaining our program to the compiler. However it does mean that several programs that are totally correct with respect to Rust's true semantics are rejected because lifetimes are too dumb.
The area covered by a lifetime
A reference (sometimes called a borrow) is alive from the place it is created to its last use. The borrowed value needs to outlive only borrows that are alive. This looks simple, but there are a few subtleties.
The following snippet compiles, because after printing x
, it is no longer
needed, so it doesn't matter if it is dangling or aliased (even though the
variable x
technically exists to the very end of the scope).
let mut data = vec![1, 2, 3];
let x = &data[0];
println!("{}", x);
// This is OK, x is no longer needed
data.push(4);
However, if the value has a destructor, the destructor is run at the end of the scope. And running the destructor is considered a use ‒ obviously the last one. So, this will not compile.
#[derive(Debug)]
struct X<'a>(&'a i32);
impl Drop for X<'_> {
fn drop(&mut self) {}
}
let mut data = vec![1, 2, 3];
let x = X(&data[0]);
println!("{:?}", x);
data.push(4);
// Here, the destructor is run and therefore this'll fail to compile.
One way to convince the compiler that x
is no longer valid is by using drop(x)
before data.push(4)
.
Furthermore, there might be multiple possible last uses of the borrow, for example in each branch of a condition.
# fn some_condition() -> bool { true }
let mut data = vec![1, 2, 3];
let x = &data[0];
if some_condition() {
println!("{}", x); // This is the last use of `x` in this branch
data.push(4); // So we can push here
} else {
// There's no use of `x` in here, so effectively the last use is the
// creation of x at the top of the example.
data.push(5);
}
And a lifetime can have a pause in it. Or you might look at it as two distinct borrows just being tied to the same local variable. This often happens around loops (writing a new value of a variable at the end of the loop and using it for the last time at the top of the next iteration).
let mut data = vec![1, 2, 3];
// This mut allows us to change where the reference points to
let mut x = &data[0];
println!("{}", x); // Last use of this borrow
data.push(4);
x = &data[3]; // We start a new borrow here
println!("{}", x);
Historically, Rust kept the borrow alive until the end of scope, so these examples might fail to compile with older compilers. Also, there are still some corner cases where Rust fails to properly shorten the live part of the borrow and fails to compile even when it looks like it should. These'll be solved over time.