Rust의 lifetimes

손호준·2025년 1월 28일
0

Rust의 lifetimes

아래 코드는 에러가 발생한다. picked_value의 라이프타임이 int2의 라이프타임과 같은데(두 개의 파라미터 중 라이프타임이 짧은 쪽을 반환 값의 라이프타임으로 반환하기 때문), int2의 유효 라이프타임 범위 밖에서 picked_value를 출력하려 했기 때문이다.

fn main() {
    let int1 = 5;
    let picked_value;
    {
        let int2 = 10;
        picked_value = picking_int(&int1, &int2);
    }
    println!("{picked_value}");
}

fn picking_int<'a>(i: &'a i32, j: &'a i32) -> &'a i32 { // 둘 중 더 짧은 라이프타임을 반환 값에 적용한다 (만약 i가 j보다 더 짦은 라이프타임을 갖는다면 반환 값의 라이프타임은 i와 동일하다.)
    if rand::random() {
        i
    } else {
        j
    }
}
Standard Error
   Compiling playground v0.0.1 (/playground)
error[E0597]: `int2` does not live long enough
 --> src/main.rs:6:43
  |
5 |         let int2 = 10;
  |             ---- binding `int2` declared here
6 |         picked_value = picking_int(&int1, &int2);
  |                                           ^^^^^ borrowed value does not live long enough
7 |     }
  |     - `int2` dropped here while still borrowed
8 |     println!("{picked_value}");
  |               -------------- borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `playground` (bin "playground") due to 1 previous error
Standard Output

위의 에러를 수정하려면 main함수 안에 있는 {}를 삭제하거나, 혹은 다음과 같이 picking_int 함수의 반환 값이 i가 되도록 작성할 수 있다.

fn main() {
    let int1 = 5;
    let picked_value;
    {
        let int2 = 10;
        picked_value = picking_int(&int1, &int2);
    }
    println!("{picked_value}");

}
fn picking_int<'a>(i: &'a i32, j: &i32) -> &'a i32 { 
    i
}

static 라이프타임

만약 특정 값을 함수 안에서 정의하여, 해당 값의 참조를 반환하고 싶으면 어떻게 할까? 아래와 같이 코드를 작성하면 에러가 발생한다.

fn picking_int<'a>(i: &'a i32, j: &'a i32) -> &'a i32 {
    let y = 6;           // `y`는 스코프를 벗어나면 사라짐
    &y                  // ERROR: y는 'a 라이프타임을 만족하지 않음
}
error[E0515]: cannot return reference to local variable `y`
  --> src/main.rs:13:5
   |
13 |     &y                  // ERROR: y는 'a 라이프타임을 만족하지 않음
   |     ^^ returns a reference to data owned by the current function

For more information about this error, try `rustc --explain E0515`.
warning: `playground` (bin "playground") generated 2 warnings
error: could not compile `playground` (bin "playground") due to 1 previous error; 2 warnings emitted

함수 내에서 선언한 y의 라이프타임이 함수를 벗어나는 순간 유효하지 않기 때문에 컴파일러는 에러를 발생시키고 있다.

대신 함수 내애서 정의한 값의 참조를 반환하기 위한 방법으로 static 라이프타임을 사용할 수 있다. static 라이프타임은 프로그램 전체에서 유효한 라이프타임이다.

fn picking_int<'a>(i: &'a i32, j: &'a i32) -> &'a i32 { 
    let y: &'static i32 = &6;
    y
}

위와 같이 코드를 작성하고 실행하면 문제없이 컴파일 되지만, 실제 반환 값이 라이프타임a를 반환하지 않았는데, 반환 라이프타임이 a가 되도록 명시하는것이 이상하다. 근데 왜 에러가 아닐까?

Rust는 반환값의 실제 라이프타임인 'static이 입력 라이프타임 'a를 포괄적으로 포함(outlive)하므로, 반환 라이프타임 'a에도 문제가 없다고 판단하기 때문이다. Rust 라이프타임 시스템은 더 긴 라이프타임이 더 짧은 라이프타임을 자동으로 만족시킨다는 원칙을 따른다. 즉, 'static은 'a 라이프타임보다 더 긴 기간 동안 유효하기 때문에, 'static 참조를 'a 라이프타임으로 안전하게 변환(캐스팅)할 수 있다.

하지만 코드의 의도를 명확히 전달하기 위해 함수는 다음과 같이 수정하자.

fn picking_int(i: &i32, j: &i32) -> &'static i32 { 
    let y: &'static i32 = &6;
    y
}

예제

fn main() {
    let mut some_str = String::from("I am String");
    let ref1 = &some_str; // 불변 참조 생성
    println!("{ref1}"); // move this line only
    let ref2 = &mut some_str; // 가변 참조 생성
    ref2.push_str(" additional information");
    println!("{ref2}");
}

원칙대로라면, 위의 코드는 아래의 빌림 규칙 1번에 위배되므로 에러가 발생해야한다.

빌림 규칙 (Borrowing Rules)

  1. 어떤 값에 불변 참조(&T)가 하나 이상 존재하는 동안에는 가변 참조(&mut T)를 생성할 수 없다.
  2. 가변 참조는 한 번에 하나만 존재할 수 있다.

참조가 생성되고 사용되는 스코프는 빌림 체크의 핵심이다. 참조가 더 이상 사용되지 않는 시점에는 빌림이 해제된다(이는 컴파일러가 자동으로 판단).

하지만 위의 예제 코드는 문제없이 컴파일되었다. 왜?
-> 참조(ref1)가 더 이상 사용되지 않는다고 컴파일러가 판단했기 때문.

Rust는 참조가 더 이상 사용되지 않는 경우, 해당 참조의 스코프를 조기에 종료시킨다(이를 Non-Lexical Lifetimes(NLL)라 한다).

만약 코드를 아래와 같이 수정한다면, 에러가 발생한다.

fn main() {
    let mut some_str = String::from("I am String");
    let ref1 = &some_str;
    let ref2 = &mut some_str;
    ref2.push_str(" additional information");
    println!("{ref1}"); // move this line only
    println!("{ref2}");
}

위의 코드에서는 ref1을 ref2이후에 다시 사용하려했다(println()). 이 렇게 되면 ref1의 스코프가 종료되지 않은 상태에서 ref2를 some_str에 대한 가변참조로 정의했기 때문에 명백히 빌림 규칙1에 위배된다.

라이프타임 생략 규칙

rust에서는 라이프타임 생략 규칙 덕분에 대부분의 경우 라이프타임 명시가 필요 없음.

  1. 각 참조 매개변수는 고유한 라이프타임을 갖는다.
  2. 만약 라이프타임 파라미터가 딱 하나일 경우, 그 라이프타임이 모든 출력 파라미터의 라이프타임이 된다.
  3. 여러개의 입력 라이프타임 파라미터가 있을때, 그 중 하나가 &self 혹은 &mut self면 해당 라이프타임이 모든 출력 파라미터의 라이프타임이 된다.

1번 예제

각 참조 매개변수는 고유한 라이프타임을 갖기 때문에 굳이 명시하지 않아도 컴파일러가 내부적으로 알아서 변환한다.

fn foo(x: &i32, y: &i32); 
// 컴파일러가 변환: 
// fn foo<'a, 'b>(x: &'a i32, y: &'b i32);

1번 예외: 여러개의 입력 참조가 있고, 반환값이 참조일 때

fn pick_str(x: &str, y: &str) -> &str;
// 컴파일러가 변환: 
// fn pick_str<'a, 'b>(x: &'a str, y: &'b str) -> &str ; ❌ 컴파일 에러 발생(반환 라이프타임이 뭔지 모름)

위와 같은 상황에서는 반환값의 라이프타임을 추론할 수 없기 때문에 아래와 같이 명시해줘야한다.

fn pick_str<'a>(x: &'a str, y: &'a str) -> &'a str;

2번 예제: 라이프타임 파라미터가 1개

fn get_str(s: &str) -> &str; 
// 컴파일러가 변환:
// fn get_str<'a>(s: &'a str) -> &'a str;

3번 예제: 입력 참조에 &self 포함된 경우

struct Person {
    name: String,
}

impl Person {
    fn get_name(&self) -> &str {
        &self.name
    }
}

// 컴파일러가 변환:
//    fn get_name<'a>(&'a self) -> &'a str {}

예외: 입력이 참조가 아니면서 반환값이 참조인 경우

fn create_str() -> &str;  // ❌ 컴파일 에러 발생!

이 함수에서는 반환값의 라이프타임을 추론할 수 없고, 반환값의 라이프타임이 어디에서 유래했는지 명확하지 않으므로 아래와 같이 명시해야함

fn create_str<'a>() -> &'a str { ... } // 특정 라이프타임 지정
profile
Rustacean🦀

0개의 댓글