#14 수명

Pt J·2020년 8월 25일
0

[完] Rust Programming

목록 보기
17/41
post-thumbnail

이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.

모든 참조는 유효한 범위를 가진다.
우리는 그것을 그 참조의 수명이라고 부른다.
수명도 자료형과 마찬가지로 기본적으로는 추론에 의해 결정되지만
그렇게 결정될 수 없는 경우에는 따로 명시해주어야 한다.

수명 Lifetime

어떤 변수 A가 다른 변수 B를 참조할 때
A는 아직 유효 범위 내에 존재하지만 B는 범위를 벗어난다면
A는 의미 없는 값을 참조하게 된다.
이러한 문제 상황을 방지하기 위해 Rust는 수명이라는 개념을 사용한다.
어떤 변수가 유효한 범위가 그것이 수명을 가지는 범위이며
수명이 다한 변수를 참조하고 있다면 그 참조 변수도 수명을 다하게 된다.
이 경우에는 A가 아직 유효 범위 내에 있다고 하더라고 그것은 수명을 다한다.

Rust는 컴파일 시점에 대여한 값의 유효성을 검사하는 대여 검사기를 사용한다.
대여 검사기는 각각의 변수의 수명을 확인하여
어떤 변수가 다른 변수를 참조하다가 그것의 수명이 다한 후에 사용하려고 하면
이미 죽은 참조에 접근하려고 하는 것을 인식하고 컴파일을 거부한다.

수명 애노테이션 Lifetime Annotation

수명에 대한 개념을 더 잘 이해하기 위한 예제를 하나 작성해보도록 하겠다.
두 개의 문자열을 인자로 받아 그 중 길이가 더 긴 녀석을 반환하는 함수를 작성할 것이다.
이 때, 함수는 문자열의 소유권을 가져가지 않고 문자열 슬라이스의 참조 형태로 인자를 받는다.

peter@hp-laptop:~/rust-practice$ cd chapter10/
peter@hp-laptop:~/rust-practice/chapter10$ cargo new longest_string
     Created binary (application) `longest_string` package
peter@hp-laptop:~/rust-practice/chapter10$ cd longest_string/
peter@hp-laptop:~/rust-practice/chapter10/longest_string$ vi src/main.rs

src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    println!("The longest string is {}", longest(string1.as_str(), string2));
}

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

그런데 이렇게 작성하고 실행하려고 하면 컴파일이 되지 않는 것을 확인할 수 있다.

peter@hp-laptop:~/rust-practice/chapter10/longest_string$ cargo run
   Compiling longest_string v0.1.0 (/home/peter/rust-practice/chapter10/longest_string)
error[E0106]: missing lifetime specifier
 --> src/main.rs:8:33
  |
8 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ 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
  |
8 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ^^^^    ^^^^^^^     ^^^^^^^     ^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0106`.
error: could not compile `longest_string`.

To learn more, run the command again with --verbose.
peter@hp-laptop:~/rust-practice/chapter10/longest_string$

xy 중 무엇이 반환될지 알 수 없으므로
대여 검사기를 사용할 수 있도록 제네릭 수명을 명시해주어야 한다는 것이다.
어떻게 수정해야 하는지는 오류 메시지에 예시가 나와 있다.
이런 식으로 명시해주는 제네릭 수명을 수명 애노테이션이라고 한다.

수명 애노테이션은 제네릭과 마찬가지로 짧게 한 글자로 쓰는 것이 일반적이지만
제네릭과 달리 소문자를 사용한다.
그리고 수명 애노테이션은 항상 작은 따옴표'로 시작해야 한다.

이에 따라 코드를 수정해보자.

peter@hp-laptop:~/rust-practice/chapter10/longest_string$ vi src/main.rs

src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    println!("The longest string is {}", longest(string1.as_str(), string2));
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
peter@hp-laptop:~/rust-practice/chapter10/longest_string$ cargo run
   Compiling longest_string v0.1.0 (/home/peter/rust-practice/chapter10/longest_string)
    Finished dev [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/longest_string`
The longest string is abcd
peter@hp-laptop:~/rust-practice/chapter10/longest_string$ 

이제 문제 없이 실행되는 것을 확인할 수 있다.
xy가 수명 애노테이션 'a을 공유하여 'a가 나타내는 수명은
x의 수명과 y의 수명의 교집합에 해당하는 범위로 결정된다.
따라서 함수 longest의 반환값은 x 또는 y 둘 중 하나라도 수명을 다하면 수명이 끝난다.
심지어 xy보다 수명이 긴 상황에서 x가 반환되어도
y가 수명을 잃었을 때 반환값도 수명을 잃게 된다.
함수가 참조를 반환할 경우 그것의 수명은
인자로 전달된 녀석들 중 누군가의 수명하고는 일치해야 한다는 것은 여담.

수명 애노테이션이 포함된 구조체

우리는 지금까지 구조체가 스스로 소유하는 값만을 필드로 갖는 것을 봤는데
수명 애노테이션을 명시해줄 경우 참조 필드도 가질 수 있다.

문자열 슬라이스를 저장하는 구조체를 사용하는 예제를 작성해보자.

peter@hp-laptop:~/rust-practice/chapter10/longest_string$ cd ..
peter@hp-laptop:~/rust-practice/chapter10$ cargo new important_excerpt
     Created binary (application) `important_excerpt` package
peter@hp-laptop:~/rust-practice/chapter10$ cd important_excerpt/
peter@hp-laptop:~/rust-practice/chapter10/important_excerpt$ vi src/main.rs

src/main.rs

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ismael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };

    println!("{}", i.part);
}
peter@hp-laptop:~/rust-practice/chapter10/important_excerpt$ cargo run
   Compiling important_excerpt v0.1.0 (/home/peter/rust-practice/chapter10/important_excerpt)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/important_excerpt`
Call me Ismael
peter@hp-laptop:~/rust-practice/chapter10/important_excerpt$ 

수명 애노테이션을 명시한 녀석의 수명은 그들의 범위의 교집합만큼의 수명을 가진다.
즉, 위 예제에서는 part가 수명을 잃으면 구조체 인스턴스 자체가 수명을 잃는다.
이렇게 수명 애노테이션을 사용하면 안전하게 참조 필드를 가진 구조체를 생성할 수 있다.

수명 생략 규칙 Lifetime Elision Rules

함수나 메서드에서 참조를 사용하지만 수명 애노테이션을 생략할 수 있는 경우도 있다.
이는 수명 애노테이션을 명시하지 않아도 Rust 컴파일러가 그것을 유추할 수 있는 경우다.
이 때 수명 유추에 사용되는 것이 수명 생략 규칙이다.
Rust 컴파일러는 수명 생략 규칙을 적용했을 때 적절한 수명을 판단할 수 없다면
수명 애노테이션을 명시할 것을 요구하며 컴파일을 거부한다.
수명 생략 규칙에서 매개변수에 적용되는 수명을 입력 수명,
반환값에 적용되는 수명을 출력 수명이라고 한다.

[규칙1] 각 참조 매개변수는 각자의 수명 매개변수를 가진다.
The first rule is that each parameter that is a reference gets its own lifetime parameter.

이 규칙이 적용되면 fn foo(x: &i32)와 같이 하나의 매개변수를 가진 함수는
fn foo<'a>(x: &'a i32)와 같이 하나의 수명 매개변수를 가지며
fn foo(x: &i32, y: &i32)와 같이 두 개의 매개변수를 가진 함수는
fn foo<'a, 'b>(x: &'a i32, y: &'b i32)와 같이 하나의 수명 매개변수를 가지게 된다.

[규칙2] 입력 수명 매개변수가 하나 존재한다면 그것이 모든 출력 수명 매개변수에 적용된다.
The second rule is if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters

이 규칙이 적용되면 fn foo<'a>(x: &'a i32) -> &i32와 같이
하나의 수명 애노테이션이 명시되어 있는 함수는
fn foo<'a>(x: &'a i32) -> &'a i32와 같이 그것이 출력 수명에 적용된다.

[규칙3; 메서드에만 적용] 여러 개의 입력 수명 매개변수가 존재하며 그 중 하나가 &self 또는 &mut self라면 self의 수명이 출력 수명 매개변수에 적용된다.
The third rule is if there are multiple input lifetime parameters, but one of them is &self or &mut self because this is a method, the lifetime of self is assigned to all output lifetime parameters.

이 규칙은 메서드에 대한 가독성을 높여준다.

메서드에서 수명 애너테이션을 명시할 땐 제네릭과 마찬가지로 impl<'a>로 선언하며
수명 애노테이션은 구조체 이름에 포함되므로 구조체 이름 다음에도 명시해주어야 한다.
impl 블럭 안의 메서드는 'a 애노테이션을 사용할 수도 있고 그렇지 않을 수도 있다.

앞서 작성한 구조체 예제에 메서드를 구현해보도록 하자.

peter@hp-laptop:~/rust-practice/chapter10/important_excerpt$ vi src/main.rs

src/main.rs

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ismael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };

    println!("{}", i.part);

    let return_value = i.announce_and_return_part("test");
    println!("{}", return_value);
}
peter@hp-laptop:~/rust-practice/chapter10/important_excerpt$ cargo run
   Compiling important_excerpt v0.1.0 (/home/peter/rust-practice/chapter10/important_excerpt)
    Finished dev [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/important_excerpt`
Call me Ismael
Attention please: test
Call me Ismael
peter@hp-laptop:~/rust-practice/chapter10/important_excerpt$ 

메서드에는 수명 애노테이션이 명시되지 않았지만 &self가 매개변수로 존재하므로
구조체 인스턴스 자체의 수명이 반환값의 수명에 적용되었다.
수명 생략 규칙의 세 번째 규칙에 해당하는 상황이다.

정적 수명 Static Lifetime

조금 특별한 수명으로 정적 수명이라는 녀석이 있다.
모든 문자열 리터럴은 정적 수명을 가지며 'static이라는 수명 애노테이션으로 명시할 수 있다.
정적 수명을 가진 녀석은 프로그램 전 범위에서 사용 가능하다.

이 포스트의 내용은 공식문서의 10장 3절 Validating References with Lifetimes에 해당합니다.

profile
Peter J Online Space - since July 2020

0개의 댓글