러스트에서 모든 참조자는 라이프타임(lifetime)을 갖는데, 이는 해당 참조자가 유효한 스코프이다. 대부분의 경우에서 타입들이 추론되는 것과 마찬가지로, 대부분의 경우에서 라이프타임 또한 암묵적으로 추론된다. 여러 가지 타입이 가능하기 때문에 우리가 타입을 명시해야 하는 때와 비슷하게, 참조자의 라이프타임이 몇몇 다른 방식으로 연관될 수 잇는 경우들이 있으므로, 러스트는 우리에게 제네릭 라이프타임 파라미터를 이용하여 이 관계를 명시하길 요구하여 런타임에 실제 참조자가 확실히 유효하도록 확신할 수 있도록 한다.
라이프타임의 주목적은 댕글링 참조자(dangling reference)를 방지하는 것인데, 댕글링 참조자는 프로그램이 우리가 참조하기로 의도한 데이터가 아닌 다른 데이터를 참조하는 원인이 된다.
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
초기화되지 않은 변수는 사용할 수 없다
값을 제공하기 전에 변수를 사용하고자 시도한다면, 컴파일 에러가 발생할 것이다.
다음과 같은 에러 발생
error: `x` does not live long enough
|
6 | r = &x;
| - borrow occurs here
7 | }
| ^ `x` dropped here while still borrowed
...
10 | }
| - borrowed value needs to live until here
변수 x가 스코프를 벗어났기 때문에 더이상 유효하지 않고 이 해당 x를 참조하고 있는 r은 x가 존재하는 스코프보다 상위단 스코프에 존재하므로 살아있다. r은 x가 스코프 밖으로 벗어났을 때 할당이 해제되는 메모리를 참조하게 될 것이고, r을 가지고 시도하려 했던 어떤 것이든 정확히 동작하기 않게 될 것이다. 그렇다면 러스트는 이 코드가 허용되어서는 안 된다는 것을 어떻게 결정할 것인가?
빌림 검사기(borrow checker)라고 불리는 컴파일러의 부분이 모든 빌림이 유효한지를 결정하기 위해 스코프를 비교한다.
{
let r; // -------+-- 'a
// |
{ // |
let x = 5; // -+-----+-- 'b
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
// |
// -------+
}
여기서 r의 라이프타임을 'a
라고 명명하였고, x의 라이프타임을 'b
라고 명명하였다. 내부의 'b
블록은 외부의 'a
라이프타임 블록에 비해 훨씬 작다. 컴파일 타임에서, 러스트는 두 라이프타임의 크기를 비교하고 r이 'a
라이프타임을 가지고 있지만, 'b
라이프타임을 가지고 있는 어떤 오브젝트를 참조하고 있음을 보게 된다. 'b
라이프타임이 'a
라이프타임에 비해 작기 때문에 러스트 컴파일러는 이 프로그램을 거부한다: 참조자의 주체가 참조자만큼 오래 살지 못하고 있기 때문이다.
두 스트링 슬라이스 중에서 긴 쪽을 반환하는 함수를 작성해 보자.
이 함수에 두 개의 스트링 슬라이스를 넘겨서 호출할 수 있기를 원하고, 스트링 슬라이스를 반환하기를 원합니다.
fn main() {
let string1 = String::from("abcd");
let String2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
longest
함수가 인자의 소유권을 얻는 것을 원치 않기 때문에 스트링 슬라이스들을 파라미터로서 갖는 함수를 원한다는 점을 주목하라. 우리는 함수가 String의 슬라이스(string1)는 물론 스트링 리터럴(string2) 또한 받아들일 수 있기를 원하고 있다.
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
위 코드는 컴파일되지 않고 에러를 뱉을 것이다.
error[E0106]: missing lifetime specifier
|
1 | fn longest(x: &str, y: &str) -> &str {
| ^ expected 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`
이 도움말은 반환 타입에 대하여 제네릭 라이프타임 파라미터가 필요하다는 것을 말해주고 있다. 왜냐하면 반환되는 참조자가 x를 참조하는지 혹은 y를 참조하는지를 러스트가 알 수 없기 때문이다.
이 함수를 정의하는 시점에서, 이 함수에 넘겨지게 될 구체적인 값을 모르므로, if
케이스가 실행될지 else
케이스가 실행될지 알 수 없다. 또한 함수에 넘겨지게 될 참조자의 구체적인 라이프타임을 알지 못하므로, 우리가 반환하는 참조자가 항상 유효한지를 결정하기 위해서 스코프를 살펴보고 결정할 수도 없다. 빌림 검사기 또한 이를 결정할 수 없는데, 그 이유는 x
와 y
의 라이프타임이 반환 값의 라이프타임과 어떻게 연관되어 있는지 알지 못하기 때문이다. 우리는 참조자들 간의 관계를 정의하는 제네릭 라이프타임 파라미터를 추가하여 빌림 검사기가 분석을 수행할 수 있도록 할 것이다.
라이프타임 명시는 연관된 참조자의 수명을 바꾸지는 않는다. 함수의 시그니처가 제네릭 타입 파라미터를 특정할 때 이 함수가 어떠한 타입이든 허용할 수 있는 것과 같은 방식으로, 함수의 시그니처가 제네릭 라이프타임 파라미터를 특정할 때라면 이 함수는 어떠한 라이프타임을 가진 참조자라도 허용할 수 있다. 라이프타임 명시가 하는 것은 여러 개의 참조자에 대한 라이프타임들을 서로 연관 짓도록 하는 것이다.
라이프타임 명시는 약간 독특한 문법을 갖고 있다: 라이프타임 파라미터의 이름은 어퍼스트로피(Apostrophe)'
로 시작해야 한다. 라이프타임 파라미터의 이름은 보통 모두 소문자이며, 제네릭 타입과 비슷하게 그들의 이름은 보통 매우 짧다. 'a
는 대부분의 사람들이 기본적으로 사용하는 이름이다. 라이프타임 파라미터 명시는 참조자의 &
뒤에 오며, 공백 문자가 라이프타임 명시와 참조자의 타입을 구분해준다.
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
스스로에 대한 하나의 라이프타임 명시는 큰 의미를 가지고 있지 않다: 라이프타임 명시는 러스트에게 여러 개의 참조자에 대한 제네릭 라이프타임 파라미터가 서로 어떻게 연관되는지 말해준다. 만일 라이프타임 'a
를 가지고 있는 i32에 대한 또다른 참조자인 second를 또 다른 파라미터로 가진 함수가 있다면, 이 두개의 같은 이름을 가진 라이프타임 명시는 잠조자 first와 second가 둘다 동일한 제네릭 라이프타임만큼 살아야 한다는 것을 가리킨다.
제네릭 타입 파라미터와 마찬가지로, 제네릭 라이프타임 파라미터도 함수 이름과 파라미터 리스트 사이에 꺾쇠 괄호를 쓰고 그 안에 정의되어야 한다.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
시그니처 내의 모든 참조자들이 동일한 라이프타임 'a
를 가지고 있다. 이 함수는 적어도 라이프타임 'a
만큼 살아있는 스트링 슬라이스를 반환할 것이다.
이 함수는 x
와 y
가 정확히 얼마나 오래 살게 될지 알지 못하지만 (혹은 알 필요 없지만), 다만 이 시그니처를 만족시킬 'a
에 대입될 수 있는 어떤 스코프가 있음을 알아야 할 필요가 있을 뿐이다.
fn main() {
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);
}
}
string1
은 외부 스코프가 끝날 때까지 유효하고 string2
는 내부 스코프가 끝날 때까지 유효하며, result
는 내부 스코프가 끝날 때가지 유효한 무언가를 참조한다. 위의 코드는 에러 없이 잘 돌아가게 된다.
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);
}
위의 코드는 다음과 같은 에러를 얻는다.
error: `string2` does not live long enough
|
6 | result = longest(string1.as_str(), string2.as_str());
| ------- borrow occurs here
7 | }
| ^ `string2` dropped here while still borrowed
8 | println!("The longest string is {}", result);
9 | }
| - borrowed value needs to live until here
이 에러는 result가 println!에서 유효하기 위해서는 string2가 외부 스코프의 끝까지 유효할 필요가 있음을 말해준다. 그 이유는 우리가 함수의 파라미터들과 반환 값에 대해 동일한 라이프타임 파라미터 'a
를 명시했기 때문이다.
longest
함수에 의해 반환되는 참조자의 라이프타임이 인자로 넘겨준 라이프타임들 중 작은 쪽과 동일하기 때문에, 빌림검사기는 잠재적으로 유효하지 않은 참조자를 가질 수 있는 문제로 인해 허용하지 않는다.
'static
라이프타임은 프로그램의 전체 생애주기를 가리킨다. 모든 스트링 리터럴은 'staic
라이프타임을 가지고 있는데, 아래와 같이 명시하는 쪽을 선택할 수 있다.
let s: &'static str = "I have a static lifetime.";
이 스트링의 텍스트는 프로그램의 바이너리 내에 직접 저장되며 프로그램의 바이너리는 항상 이용이 가능하다. 따라서 모든 스트링 리터럴의 라이프타임은 'static
이다.
참조: https://rinthel.github.io/rust-lang-book-ko/ch10-03-lifetime-syntax.html