Rust - chapter 10-3 Validate Refrerences with Lifetimes
Rust 공식문서(링크) 와 Rust 비공식 번역 문서(링크) 챕터 10-3 라이프타임을 이용한 참조자 유효화를 읽고 정리한 글이다.
러스트에서 모든 참조자는 라이프타임을 갖는데, 이는 해당 참조자가 유효한 스코프입니다. 대부분의 경우에서 타입들이 추론되는 것과 마찬가지로, 대부분의 경우에서 라이프타임 또한 암묵적이며 추론된다. 다만 추론할 수 없는 경우가 있으므로, 러스트는 제너릭 파이프타임 파라미터를 이용하여 라이프타임을 명시하길 요구하며, 런타임에 실제 참조자가 확실히 유효하도록 확신할 수 있도록 합니다.
흔치 않은 개념이며, 러스트의 가장 독특한 기능이며, 이 장에서 전체를 다룰 수 없는 큰 주제이며 19장에서 추가적으로 설명한다고 한다. 🪡
라이프타임은 댕글링 참조자를 방지
라이프타임의 주목적은 댕글링 참조자(dangling reference)
를 방지하는 것인데, 댕글링 참조자는 프로그램이 우리가 참조하기로 의도한 데이터가 아닌 데른 데이터를 참조하는 원인이 되기 때문이다.
{
let r; // -------+-- a
{ // |
let x = 5; // -+-----+-- b
r = &x; // | |
} // 1 // -+ |
println("r: {}", r); // |
} // -------+
💿 초기화 하지 않은 변수는 사용할 수 없다.
러스트는 null 값을 가지지 않는다는 개념과 위의 r 변수를 선언만 한 것이 이상할지 모르지만 러스트는 값을 초기화 하기 전에 사용하려 하면 컴파일 에러가 발생한다.
위 예제를 컴파일하면 error: 'x' does not live long enough
에러가 발생한다. x 는 1 에서 내부스코프가 종료되면서 스코프에서 없어지나 더 오래 살 수 있고 외부 스코프에서 유효한 r 이 x의 참조자를 가지고 있는 상태에서 외부 스코프에서 사용될 수 있기 때문에 발생하게 된다.
빌림 검사기(Borrow checker)
컴파일러의 한 부분인 빌림 검사기는 모든 빌림이 유효한지 결정하기 위해서 스코프를 비교한다.
앞선 예제에서 r 의 라이프타임을 a, x의 라이프타임을 b 라고 할 경우 b 는 a 에 비해 작은 범위를 지니고 있다. 러스트는 두 라이프타임의 크기를 비교하여 r 이 a 의 라이프타임을 가지며, b 의 라이프타임을 갖는 객체를 참조하고 있음을 파악하여 b 의 라이프타임이 a 에 비해 작기 때문에 위 예제는 컴파일이 되지 않는다.
{
let x = 5; // -------+-- a
let r = &x; // -+-- b |
println("r: {}", r); // -+-----+
}
데이터(x)가 참조자(r)에 비해 더 긴 라이프타임을 가지고 있기 때문에 위는 정상동작하는 코드이다.
함수에서의 제네릭 라이프타임
먼제 예제를 살펴보자
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(stirng1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest(x: &str, y: &str) -> &str{
if x.len() > y.len() {
x
} else {
y
}
}
위 예제에서는 스트링 슬라이스들을 함수의 파라미터로 갖고 이는 longest 함수가 인자의 소유권을 갖지 않게끔 하기 위해서이고 위 예제의 컴파일은 다음과 같은 에러를 출력하며 실패할 것이다. error[E0106]: missing lifetime specifier
추가적인 도움말을 확인해보면 반환타입에 제네릭 라이프타임 파라미터가 필요하는 것을 표기하고 있는데 반환되는 참조자가 x, y인지 알 수가 없기 때문이다.
이 함수를 정의하는 시점에서는 x, y에 어떠한 값이 들어올지 알 수가 없다. 따라서 빌림 검사기도 이를 결정할 수 없는데 명시적으로 참조자들 간의 관계를 정의하는 제네릭 라이프타임 파리미터를 추가하여 빌림 검사기가 분석을 수행할 수 있도록 할 수 있다.
라이프트임 명시 문법
라이프타임 명시는 연관된 참조자의 유효범위를 바꾸지 않으며, 여러 개의 참조자에 대한 라이프타임들을 서로 연관 짓도록 한다.
라이프타임 파라미터는 '
어퍼스트로피로 시작해야하며 관례적으로 a 와 같이 짧은 이름을 사용합니다. 참조자의 &의 뒤에 오며 공백 문자가 라이프타임 명시와 참조자의 타입을 구분한다.
&i32
&'a i32
&'a mut i32
위의 예제와 같이 사용할 수 있으며 스스로에 대한 하나의 라이프타임의 명세는 큰 의미를 가지지 않으며 라이프타임의 명시는 러스트에게 여러 개의 참조자에 대한 제네릭 라이프타임 파라미터가 서로 어떻게 연관되는지를 말해준다.
함수 시그니쳐 내의 라이프타임 명시
위 예제를 컴파일 되게 하려면 아래와 같이 longest 함수에 라이프타임을 명시해야한다.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
위와 같이 제네릭 타입 파라미터와 똑같이 제네릭 라이프타임 파라미터를 작성하고 각 참조자에 'a
의 동일한 라이프타임을 추가하면 컴파일이 되며,
이러한 작업은 'a
의 라이프타임을 같는 매개변수가 있고 해당하는 라이프타임 만큼 살아있는 값을 반환함을 러스트에게 알려준다.
이 함수 시그니처는 이제 어떤 라이프타임 'a
에 대해 두 개의 파라미터를 갖게 될 것이고, 반환 까지 모두 적어도 라이프타임 'a
만큼 살아있는 스트링 슬라이스임을 알 수 있습니다.
함수 안에 라이프타임을 명시할 때 이 명시는 함수 시그니처에 붙어있으며, 함수의 본체 내에의 어떠한 코드에도 붙어있지 않다. 함수가 함수 밖의 코드에서 참조자를 가지고 있을 때, 인자 또는 반환 값들의 라이프타임이 하뭇가 호출될 때마다 달라질 수 있기 때문이며, 러스트가 발견하기에는 비용이 크고 또는 불가능할 것이다. 이러한 경우 우리는 라이프타임을 명시할 필요가 있다.
또한 위의 예제에서는 반환되는 값의 라이프타임이 인자들의 라이프타임보다 작아야 합니다. 반환되는 값의 라이프타임이 아래와 같이 더 클 경우 컴파일이 되지 않는다.
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
라이프타임의 측면에서 생각하기
라이프타임 파라미터를 특정하는 방법은 함수가 어떤 일을 하고 있는가에 따라 달라지는 문제이다. 앞선 예제 longest 함수를 무조건 첫번째 인자만 반환하도록 할 경우 y의 인자의 라이프타임은 특정할 필요가 없다.
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
함수로부터 참조자를 반환할 때, 반한 타입에 대한 라이프타임 파라미터는 인자 중 하나의 라이프타임 파라미터와 일치할 필요가 있다.
라이프타임 문법은 함수들의 다양한 인자들과 반환 값 사이를 연결하는 것에 대한 문법이고, 이들이 연결되면, 러스트는 메모리에 안전한 연산들을 허용하고 댕글링 포인터를 생성하거나 메모리 안전에 위배되는 연산들을 배제하기에 충분한 정보를 가지게 된다.
구조체 정의 상에서의 라이프타임 명시
구조체 정의 내의 모든 참조자들에 대하여 라이프타임을 표시할 필요가 있습니다.
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let firest_sentence = novel.split('.').next().unwrap();
let i = ImportantExcerpt { part: firest_sentence };
}
위 예제에서 구조체는 스트링 슬라이스를 담을 수 있는 part 라는 하나의 참조자 필드를 가지고 있다. main 함수는 변수 novel 이 소유하고 있는 String의 첫 문장에 대한 참조자를 들고 있는 ImportantExcerpt 구조체 인스턴스를 생성한다.
라이프타임 생략
우리는 앞선 장들에서는 라이프타임을 명시하지 않고 예제들을 사용해왔고 그런 예제들은 컴파일이 되었다. 1.0 이전 시절의 러스트에서는 모든 참조자에 명시적인 라이프타임이 필요하였지만 이후 반복되는 패턴들 속에서 타입을 추론할 수 있음 알게 되고 빌림 검사기가 라이프타임을 추론할 수 있도록 되었다.
생략 규칙들은 모든 추론을 제공하지는 않지만, 러스트가 이 규칙들을 적용한 이유에도 참조자들의 라이프타임이 모호하다면 직접 라이프타임을 명시하여야 한다.
- 라이프타임 생략 규칙, 1번은 입력 라이프타임, 2~3번은 출력 라이프타임에 적용된다. 모든 규칙에서 타입이 추론되지 않는다면 컴파일러는 에러를 발생시킨다.
- 참조자인 각각의 파라미터는 고유한 라이프타임 파라미터를 갖는다.
- 정확히 딱 하나의 라이프타임 파라미터만 있다면, 그 라이프타입이 모든 출력 라이프타임 파라미터들에 대입된다.
- 여러 개의 입력 라이프타임 파라미터가 있고, 메소드라서 그 중 하나가
&self
혹은&mut self
인 경우, self의 라이프타임이 모든 출력 파라미터에 대입된다.
아래 예제는 적용 예시이다.
// 생략할 수 있는 예
fn first_word(s: &str) -> &str {}
// 1번 규칙 적용
fn first_word<'a>(s: &'a str) -> &str {}
// 2번 규칙 적용
fn first_word<'a>(s: &'a str) -> &'a str {}
// 생략할 수 없는 예
fn longest(x: &str, y: &str) -> &str {}
// 1번 규칙 적용
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {}
// 하나 이상의 파라미터를 가지고 있으므로 2번 불가 메서드가 아니므로 3번 불가
// 반환 타입 라이프타임을 추론하지 못하였으므로 컴파일시 에러 발생
메서드 정의 내에서의 라이프타임 명시
구조체 필드를 위한 라이프타임은 언제나 impl
뒤에 선언되어야 하며, 그 후 구조체의 이름 뒤에 사용하며, 라이프타임들은 구조체 타입의 일부이기 때문이다.
impl
블록 안에 있는 메소드 시그니처에서, 참조자들이 구조체 필드에 있는 참조자들의 라이프타이모가 묶일 수도 있고, 혹은 독립적일 수도 있다. 또한 라이프타임 생략 규칙이 종종 적용되어 라이프타임 명시를 할 필요가 없습니다.
// 1번 규칙으로 인해 명시할 필요가 없다.
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {}
}
// 3번 규칙으로 인해 명시할 필요가 없다.
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
정적 라이프타임
프로그램의 전체 생애주기를 나타내는 static 라이프타임이 있다. 모든 스트링 리터럴은 static 라이프타임을 가지고 있으며 명시할 수도 있다.
let s: &'static str = "I have a static lifetime.";
에러 메시지 도움말에서 'static
라이프타임을 이용하라는 제안을 받았을 수 있지만 잘 생각해보길 바란다. 정말 전체 기간동안 사용되는 건인지 필요한 부분에서만 사용하고 비우는 걸 추천한다.
10장 함께 사용하기
10장… 길었다… 회사일이며 게으름이 폭발하여 안 했다 이제 보니 10장만 한 달 넘게 붙잡고 있었구나.. ㄸ💦
10장의 문법이 모두 사용된 예제로 마무리한다.
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
}
}
개인이 참고하고자 작성한 글이며, 잘못된 정보가 있을 수 있습니다. 잘못된 정보는 메일로 보내주시면 감사하겠습니다. 🙏