비욘드 JS: 러스트 - Lifetimes

dante Yoon·2022년 12월 30일
0

beyond js

목록 보기
11/20
post-thumbnail

글을 시작하며

안녕하세요, 단테입니다. 러스트의 lifetime, 시작합니다.

Prevent Dangling Reference

dangling reference를 방지하기 위해 사용합니다. dangling reference를 허용하면 프로그램이 참조하는 데이터가 의도하지 않은 reference를 참조할 수 있습니다.

아래 코드는 두 개의 스코프가 있습니다. outer scope, inner scope.
inner scope에서는 r에 x의 reference를 바인딩 시키고 있는데요, inner scope가 종료되고 r을 출력하려고 할 때 r이 참조하고 있는 x의 reference는 이미 inner scope에서 벗어난 직후 없어졌기 때문에 에러 메세지가 발생합니다.
fn main() {
let r;

{
    let x = 5;
    r = &x;
}

println!("r: {}", r);

}

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 | 
9 |     println!("r: {}", r);
  |                       - borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error

r이 outer scope, x가 inner scope에 있기 때문에 outer scope가 더 넓은 스코프를 가지고 있다고 말합니다. 이는 다르게 말해서 더 긴 생명주기를 가지고 있다고 말하기도 합니다.

러스트는 앞서 봤던 예제에서 컴파일 에러를 발생시키는 것처럼 코드가 유효한지 살펴보기 위해서 borrow checker를 사용합니다.

Borrow Checker

Ownership 강의에서 한번 다뤘었죠?
러스트 컴파일러의 borrow checker는 모든 borrow들이 유효한지 서로 다른 스코프를 비교해보며 검사합니다.

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

러스트에서 r의 lifetime'r,
x의 lifetime'x 라고 표기합니다.

위 코드의 도식도에서 볼 수 있듯이 'b의 블럭이 'a의 블럭보다 작음으로 컴파일 에러가 발생하는 것입니다.
r의 라이프 타임은 'a'지만 더 작은 라이프 타임인 'b의 메모리를 참조하기 때문에 실패합니다.

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+

정상적으로 작동하는 코드

Generic lifetimes in functions

두 문자열을 합치는 함수를 작성하려고 합니다.

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

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

println! 매크로의 {}에서 "abcd xyz"가 출력되어야 하는 것입니다.

두 문자열을 합치는 함수는 longest 함수입니다.

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

위 함수 작성 시 아래와 같이 에러가 발생합니다.

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | 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
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` due to previous error

longest 함수의 바디 조건문에 따라 x,y 중 한가지 값만 반환하기 때문에 러스트 컴파일러는 x, y 값 중 어떤 값을 반환 타입으로 참조해야 하는지 모르는 상황이기 때문입니다.

longest 함수가 사용되는 곳에서 x,y 파라메터에 정확히 어떤 값이 들어가 x,y 중 어떤 값이 더 클지, 그리고 전달되는 reference의 lifetime이 어떻게 되는지 알기 못하기 때문에 러스트 컴파일러는 함수 내부에서 사용되는 x,y의 스코프에 대해 모릅니다.

위 상황을 해결하기 위해서는
lifetime에 대한 정보를 함수 선언시 전달해주어 borrow checker가 분석할 수 있도록 정보를 제공해주어야 합니다.

Lifetime Annotation Syntax

lifetime annotation은 reference의 lifetime을 변경하는 것이 아닌 여러 reference 간의 관계에 대한 정보를 제공합니다. 제너릭과 동일하게 generic lifetime parameter를 사용함으로 인해 어떤 lifetime의 레퍼런스또한 전달할 수 있습니다.

특이하게 러스트의 lifetime annoation은 apostrophe'를 사용합니다.
사용 예시를 보겠습니다.

&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&a mut i32 // a mutable reference with an explicit lifetime

함수 시그니처에서 사용하기

lifetime parameter 제너릭을 사용하기 위해서는 angle bracket <>을 함수 이름과 파라메터 리스트 사이에 작성해야 합니다. 제너릭 선언과 동일합니다. longest<'a>

심호흡 한번 하고 잘 따라와보세요.

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

함수 시그니처를 봅시다.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
함수가 두 개의 파라메터를 받아들이고 있습니다. 각 타입은 string slice이고 lifetime은 'a'라고 명시되어 있습니다.

그리고 함수에서 반환되는 string slice는 'a의 lifetime을 갖는다고 말하고 있습니다.

generic lifetime parameter가 위 함수에서 가지는 의미는 아래와 같습니다.
longest 함수에서 반환되는 함수 argument에 전달되는 파라메터 중 더 작은 범위의 lifetime과 동일한 lifetime을 갖는다.

' lifetime annotation은 함수 시그니처에만 사용되고 바디에는 사용되지 않습니다.

fn main() {
    let string1 =  String::from("abcd");
    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {}", result);
    }
}

외부 스코프가 끝날 때까지 string1은 유효합니다. 그리고 string2는 내부 스코프가 끝날때까지 유효합니다.

아래 코드는 컴파일 에러가 발생하는데요,
result의 레퍼런스의 lifetime이 두 argument중 작은 lifetime과 같아야 하는 것을 보여줍니다.

fn main() {
    let string1 =  String::from("abcd");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}
$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {}", result);
  |                                          ------ borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error

resultprintln!매크로에서 사용되는 것이 유효하지 않다고 나오는데요, 에러가 없어지기 위해서는 string2는 외부 스코프가 없어질 때까지 유효해야 합니다.
러스트 컴파일러는 이러한 스코프에 대한 사실을 liftetime parameter 'a의 사용 때문에 알 수 있습니다.
우리는 러스트 컴파일러에게 longest 함수에서 반환되는 값은 전달되는 argument 중 작은 lifetime과 동일하다는 사실을 알려주기 때문에 이러한 에러를 컴파일 타임에 잡아낼 수 있습니다.

필요 없을 때

lifetime parameter를 꼭 작성해야 하는지는 우리가 작성하는 함수의 역할에 따라 그럴 수도, 아닐 수도 있습니다. 다음 예제에서 우리는 longest 함수가 이전에 봤었던 예제들과는 다르게 항상 첫번째 파라메터를 반환한다는 내용을 작성했습니다.

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
  x
}

우리는 lifetime parameter 'a를 파라메터 x에 그리고 리턴 타입에 작성했습니다.
함수에서 reference를 반환할 때 반환 타입의 lifetime parameter는 적어도 함수의 파라메터들 중 하나의 lifetime과 일치해야 합니다. 만약 referencer가 함수 파라메터의 lifetime을 참조하지 않는다면 파라메터와 관계 없는 바디 내부에 선언된 파라메터와는 관계 없는 값을 반환타입으로 참조해야 합니다.

하지만, 파라메터의 lifetime을 참조하지 않는다는 것은 이 함수가 반환하는 것이 dangling reference일 수도 있다는 사실을 의미합니다. 왜냐하면 함수가 종료되면서 이 함수 내부에서 작성된 값은 스코프를 벗어나기 때문입니다.

아래 코드에서 반환 타입에 'a를 사용했습니다. 이 코드는 컴파일 에러를 발생시키는데 lifetime의 반환 값이 파라메터의 lifetime과는 연관이 없기 때문입니다.

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}
$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return reference to local variable `result`
  --> src/main.rs:11:5
   |
11 |     result.as_str()
   |     ^^^^^^^^^^^^^^^ returns a reference to data owned by the current function

For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` due to previous error

이게 문제가 되는 이유는 result 가 함수 내부의 스코프를 벗어날 시 longest 함수가 끝나면서 메모리에서 정리되기 때문입니다. 러스트는 이러한 dangling reference가 일어나는 코드를 허용하지 않습니다.

struct에서 Lifetime annotation` 사용하기

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

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

ImportantExcerpt<'a>의 의미는 이 struct의 인스턴스는 필드 part의 lifetime을 초과하는 스코프에서는 사용될 수 없다는 것입니다.

메인 함수를 보면 novel이 가지고 있는 문장의 일부 String을 필드로 가지고 있는 ImportantExcerpt 인스
턴스를 만들었습니다. novel에 있는 데이터가 ImportantExcerpt가 생성될 시점보다 전에 만들어졌기 때문에 유효한 코드입니다.

static lifetime

'static 이라고 하는 특별한 lifetime이 있는데요, reference가 프로그램이 실행되는 동안은 항상 살 유지되는 것을 말합니다.

모든 string literal 타입은 'static lifetime을 가지고 있습니다.

let s: &'static str = "I have a static lifetime.";

글을 마치며

러스트의 제너릭의 일종인 lifetime, 다른 언어에서는 존재하지 않는 개념이기에 유난히 이번 주제가 더 어렵게 느껴졌을 수도 있을 것 같습니다.

공부하시느라 수고 많으셨습니다. 다음은 테스트 코드에 대해 알아보겠습니다.

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글