Rust에서 lifetimes는 참조의 유효 범위를 정의하여 컴파일러가 포인터가 공중에 떠 있지 않도록(즉, 유효하지 않은 참조가 없도록) 보장합니다. 이는 가비지 컬렉터 없이도 컴파일 시 메모리 안전성을 보장합니다.
참조 작업을 할 때 Rust는 컴파일러가 이를 추론할 수 없을 때 명시적으로 생애주기를 지정하도록 요구합니다. 일반적으로는 'a 구문을 사용하여 이루어집니다:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
여기서 두 매개변수와 반환값은 같은 lifetime을 가지며, 이는 반환된 참조가 어떤 인자보다 더 오래 살지 않도록 보장합니다.
Lifetimes는 데이터의 생명 주기를 변경하지 않고 컴파일러에게 그 생명 주기를 설명합니다.
함수 내부의 로컬 변수에 대한 참조를 반환할 수 있는가?
아니요, 불가능합니다: 해당 변수가 함수 종료 시 파괴되기 때문입니다. 예:
fn foo() -> &String { // 컴파일 오류! let s = String::from("hello"); &s } // s에 대한 참조가 유효하지 않게 됩니다
컴파일러는 그런 코드를 컴파일하는 것을 허용하지 않으며, 파괴된 데이터에 대한 참조 사용을 방지합니다.
이야기
팀에서 많은 메모리 누수가 발생했는데, 함수가 무심코 로컬 버퍼에 대한 참조를 반환했기 때문입니다. 이것은 작동하지 않았고, lifetimes에 대해 신랄하게 반응하기 시작한 컴파일러 덕분에 구사할 수 있었습니다. 이러한 오류가 자주 발생하여 복잡한 구조체와 중첩 참조를 사용하는 함수의 경우 명시적으로 lifetime을 지정하는 규칙이 채택되었습니다.
이야기
프로젝트에서 데이터 캐싱을 위한 제네릭 코드를 작성했는데, lifespan generic 파라미터의 잘못된 설계로 "cannot infer lifetime" 유형 오류가 발생하여 캐시에 저장된 데이터의 생명을 추론할 수 없게 되었습니다. 이는 캐시된 데이터와 비캐시된 데이터를 서로 다른 구조체로 분리하기로 결정할 때까지 시행착오를 통해 lifetime 주석을 설정하는 결과를 초래했습니다.
이야기
한 동료가 연결 객체에 대한 참조를 사용하여 연결 풀을 구현하려고 했지만, 연결의 lifetimes가 풀의 생명 주기와 일치하지 않는다는 점을 간과했습니다. 결국 연결이 해제된 후 유효하지 않은 참조가 발생했고, 이는 종합 테스트 단계에서만 확인되었습니다. 이후 프로젝트는 안전한 래퍼(Arc<Mutex<T>>)로 전환되었습니다.