수명을 사용하여 참조 유효성 검사

완료됨

참조를 사용하면 문제가 발생합니다. 참조에서 참조하는 항목이 해당 참조를 모두 추적하지는 않습니다. 이 동작으로 인해 문제가 발생할 수 있습니다. 항목이 삭제되고 해당 리소스가 해제된 경우 현재 해제되어 유효하지 않은 메모리를 가리키는 참조가 없다는 것을 어떻게 확신할 수 있나요?

C와 C++ 같은 언어에서는 포인터가 이미 해제된 항목을 가리키는 위치에서 문제가 발생하는 경우가 많습니다. 이 문제를 “현수 포인터”라고 합니다. 다행히 Rust에서는 해당 문제가 제거됩니다. Rust는 모든 참조가 항상 유효한 항목을 참조하도록 보장합니다. 하지만 어떻게 그럴 수 있는 것일까요?

질문에 관한 Rust 답변은 수명입니다. 가비지 수집에 따른 성능 비용 없이 Rust의 메모리 안전이 보장됩니다.

값이 범위를 벗어난 참조를 사용하려 하는 다음 코드 조각을 살펴보세요.

fn main() {
    let x;
    {
        let y = 42;
        x = &y; // We store a reference to `y` in `x` but `y` is about to be dropped.
    }
    println!("x: {}", x); // `x` refers to `y` but `y has been dropped!
}

앞선 코드는 다음 오류 메시지와 함께 컴파일에 실패합니다.

    error[E0597]: `y` does not live long enough
     --> src/main.rs:6:17
      |
    6 |             x = &y;
      |                 ^^ borrowed value does not live long enough
    7 |         }
      |         - `y` dropped here while still borrowed
    8 |         println!("x: {}", x);
      |                           - borrow later used here

이 오류는 값이 아직 대여 중인 가운데 삭제되었기 때문에 발생합니다. 이 경우 y는 내부 범위의 끝에서 삭제되지만 xprintln 호출까지 이를 대여합니다. x가 여전히 외부 범위에 대해 유효하므로(범위가 더 크기 때문) “수명이 더 길다”고 할 수 있습니다.

각 변수 수명에 그림이 그려진 동일한 코드 조각입니다. 각 수명에 이름이 제공되었습니다.

  • 'a는 값 x에 대한 수명 주석입니다.
  • 'b는 값 y에 대한 수명 주석입니다.
fn main() {
    let x;                // ---------+-- 'a
    {                     //          |
        let y = 42;       // -+-- 'b  |
        x = &y;           //  |       |
    }                     // -+       |
    println!("x: {}", x); //          |
}

여기에서 내부 'b 수명 블록이 외부 'a 블록보다 짧은 것을 알 수 있습니다.

Rust 컴파일러는 ‘대여 검사기’를 사용하여 대여가 유효한지 확인할 수 있습니다. 대여 검사기는 컴파일 시간에 두 수명을 비교합니다. 이 시나리오에서 x는 수명이 'a이지만 수명이 'b인 값을 참조합니다. 참조 주체(수명이 'by)는 참조(수명이 'ax)보다 시간이 짧으므로 프로그램이 컴파일되지 않습니다.

함수의 수명 주석 달기

형식과 마찬가지로 수명 기간은 Rust 컴파일러에 의해 유추됩니다.

수명이 여러 개 있을 수 있습니다. 이런 경우 수명에 주석을 달면 컴파일러가 런타임에 참조가 유효한지 확인하는 데 사용할 수명을 파악하는 데 도움이 됩니다.

예를 들어 두 문자열을 입력 매개 변수로 가져오고 그 가운데 가장 긴 문자열을 반환하는 함수가 있습니다.

fn main() {
    let magic1 = String::from("abracadabra!");
    let magic2 = String::from("shazam!");

    let result = longest_word(&magic1, &magic2);
    println!("The longest magic word is {}", result);
}

fn longest_word(x: &String, y: &String) -> &String {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

앞선 코드는 다음의 정보 전달용 오류 메시지와 함께 컴파일에 실패합니다.

    error[E0106]: missing lifetime specifier
     --> src/main.rs:9:38
      |
    9 | fn longest_word(x: &String, y: &String) -> &String {
      |                    ----        ----        ^ expected named 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`
    help: consider introducing a named lifetime parameter
      |
    9 | fn longest_word<'a>(x: &'a String, y: &'a String) -> &'a String {
      |                ^^^^    ^^^^^^^        ^^^^^^^        ^^^

도움말 텍스트에서는 Rust가 반환된 참조가 x 또는 y를 참조하는지 여부를 확인할 수 없다고 합니다. 우리도 확인할 수 없습니다. 따라서 참조가 무엇인지 확인하려면 제네릭 매개 변수로 반환 형식에 주석을 달아 수명을 나타냅니다.

수명은 함수가 호출될 때마다 달라질 수 있습니다. longest_word 함수에 전달될 참조의 구체적인 수명을 알 수 없으며, 반환되는 참조가 항상 유효한 것인지 확인할 수 없습니다.

대여 검사기는 참조가 유효한지 여부를 확인할 수 없습니다. 입력 매개 변수의 수명이 반환 값의 수명과 어떻게 관련되는지를 알 수 없습니다. 이러한 모호성 때문에 수동으로 수명에 주석을 달아야 합니다.

다행히 컴파일러가 오류를 수정하는 방법에 대한 힌트를 주었습니다. 함수 시그니처에 일반 수명 매개 변수를 추가할 수 있습니다. 이러한 매개 변수는 참조 사이의 관계를 정의하므로 대여 검사기에서 분석을 완료할 수 있습니다.

fn longest_word<'a>(x: &'a String, y: &'a String) -> &'a String {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Rust 플레이그라운드에서 이 코드를 사용해 볼 수 있습니다.

꺾쇠괄호 안에 제네릭 수명 매개 변수를 선언하고 매개 변수 목록과 함수 이름 사이에 선언을 추가해야 합니다.

참고

서명에서 반환 값과 모든 매개 변수 참조의 수명이 동일해야 합니다. 따라서 동일한 수명 이름(예: 'a)을 사용합니다. 그런 다음, 함수 시그니처의 각 참조에 이름을 추가합니다.

이 경우 이름 'a에는 특별한 것이 없습니다. 'response 또는 'program과 같은 다른 단어를 사용해도 좋습니다. 명심할 사항은 모든 매개 변수와 반환 값이 각각에 연결된 수명 동안은 지속된다는 점입니다.

이 샘플 코드로 실험을 해보고 longest_word 함수로 전달된 참조의 값과 수명 중 일부를 변경하여 어떻게 동작하는지 확인하겠습니다. 컴파일러는 또한 다음 코드 조각을 거부합니다. 이유를 추측할 수 있습니까?

fn main() {
    let magic1 = String::from("abracadabra!");
    let result;
    {
        let magic2 = String::from("shazam!");
        result = longest_word(&magic1, &magic2);
    }
    println!("The longest magic word is {}", result);
}

fn longest_word<'a>(x: &'a String, y: &'a String) -> &'a String {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

이 코드 조각을 Rust 플레이그라운드에서 확인할 수 있습니다.

이 코드가 손상되었다고 추측했다면 정답입니다. 이번에는 다음 오류가 표시됩니다.

    error[E0597]: `magic2` does not live long enough
     --> src/main.rs:6:40
      |
    6 |         result = longest_word(&magic1, &magic2);
      |                                        ^^^^^^^ borrowed value does not live long enough
    7 |     }
      |     - `magic2` dropped here while still borrowed
    8 |     println!("The longest magic word is {}", result);
      |                                              ------ borrow later used here

이 오류는 컴파일러가 magic2의 수명이 반환 값 및 x 입력 인수의 수명과 동일하다고 예상했음을 보여줍니다. Rust에서는 이 동작을 예상하고 있습니다. 동일한 수명 이름('a)을 사용하여 함수 매개 변수와 반환 값의 수명에 주석을 달았기 때문입니다.

사람처럼 코드를 검사한 경우 magic1magic2보다 긴 것을 확인할 수 있습니다. 결과에 유효한 만큼 지속되는 magic1에 대한 참조가 포함된 것을 확인할 수 있습니다. 하지만 Rust는 컴파일 시간에 해당 코드를 실행할 수 없습니다. &magic1&magic2 참조 모두 가능한 반환 값이라 고려하고 이전에 표시되었던 오류를 내보낼 것입니다.

longest_word 함수가 반환하는 참조의 수명은 전달된 참조의 수명 중 더 작은 수명과 일치합니다. 따라서 코드에 잘못된 참조가 포함될 수 있으며 대여 검사기는 해당 참조를 허용하지 않습니다.

형식의 수명 주석 달기

구조체 또는 열거형의 필드 중 하나에 참조를 보유할 때마다 형식 정의에 각 참조의 수명으로 주석을 달아야 합니다.

예를 들어 다음 예시 코드를 살펴보세요. text 문자열(콘텐츠 소유)과 Highlight 튜플 구조체가 있습니다. 구조체에는 문자열 조각을 보유하는 하나의 필드가 있습니다. 조각은 프로그램의 다른 부분에서 대여한 값입니다.

#[derive(Debug)]
struct Highlight<'document>(&'document str);

fn main() {
    let text = String::from("The quick brown fox jumps over the lazy dog.");
    let fox = Highlight(&text[4..19]);
    let dog = Highlight(&text[35..43]);
    println!("{:?}", fox);
    println!("{:?}", dog);
}

위 코드는 Rust 플레이그라운드에서 제공됩니다.

꺾쇠괄호 안의 제네릭 수명 매개 변수 이름을 구조체 이름 다음에 배치합니다. 이렇게 배치하면 구조체 정의 본문에서 수명 매개 변수를 사용할 수 있습니다. 이 Highlight 인스턴스는 선언으로 인해 해당 필드의 참조보다 오래 지속될 수 없습니다.

위 코드에서 구조체에 'document라는 수명으로 주석을 추가했습니다. 이 주석은 Highlight 구조체가 대여한 &str의 원본(제안한 문서)보다 수명이 길 수 없음을 알리는 것입니다.

main 함수는 Highlight 구조체의 인스턴스 2개를 생성합니다. 각 인스턴스에는 변수 text에서 소유하는 String 값의 세그먼트에 대한 참조를 보유합니다.

  • foxtext 문자열의 4번째 문자와 19번째 문자 사이의 세그먼트를 참조합니다.
  • dogtext 문자열의 35번째 문자와 43번째 문자 사이의 세그먼트를 참조합니다.

또한 text가 범위를 벗어나기 전에 Highlight가 범위를 벗어나면 Highlight 인스턴스는 유효합니다.

코드는 콘솔에서 이 메시지를 출력합니다.

Highlight("quick brown fox")
Highlight("lazy dog")

실험 삼아 text에서 보유한 값을 범위 밖으로 이동하려 하고 대여 검사기에 어떤 경고를 보내는지 확인합니다.

#[derive(Debug)]
struct Highlight<'document>(&'document str);

fn erase(_: String) { }

fn main() {
    let text = String::from("The quick brown fox jumps over the lazy dog.");
    let fox = Highlight(&text[4..19]);
    let dog = Highlight(&text[35..43]);

    erase(text);

    println!("{:?}", fox);
    println!("{:?}", dog);
}

이 실패 코드 조각을 Rust 플레이그라운드에서 확인할 수 있습니다.