티스토리 뷰

Rust-Language

life time, 라이프 타임

kmj24 2021. 9. 12. 01:32

 러스트에서 모든 참조자는 life time(이하 라이프타임)을 갖는다. 여기서 라이프타임은 참조자가 유효한 scope을 의미한다.

대부분 경우에서 타입이 추론되는것과 마찬가지로, 대부분 경우 라이프타임 또한 암묵적이며, 추론된다.

여러가지 타입이 가능하기 때문에 우리가 타입을 명시해야 하는 때와 비슷하게 참조자의 라이프타임이 몇몇 다른 방식으로 연관될 수 있는 경우들이 있으므로, rust는 우리에게 제네릭 라이프타임 파라미터를 이용하여 이 관계들을 명시하길 요구하며 런타임에 실제 참조자가 확실히 유효하도록 한다.

라이프타임은 타 언어와는 다른 독특한 기능이다.

 

댕글링 참조자 방지

라이프타임의 주목적은 댕글링 참조자(dangling reference)를 방지하는 것이다. 댕글링 참조자는 프로그램이 참조하기로 의도한 데이터가 아닌 다른 데이터를 참조하는 원인이 된다. 

댕글링 포인터 : 어떤 메모리를 가리키는 포인터를 보존하는 동안, 그 메모리를 해제함으로써
다른 개체에게 사용하도록 줘버렸을지도 모를 메모리를 참조하고있는 포인터.

외부 스코프와 내부 스코프를 가진 프로그램이 있다고 생각해보자.

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

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

외부 스코프의 r은 초기값 없이 선언하였고, 내부 스코프 x는 5로 초기화 했다.

내부 스코프에서 x의 참조자를 r에 대입하도록 시도했다. 그 후 내부 스코프는 끝났고, r의 값을 출력하려고 한다.

error 'x' does not live long enough 가 출력된다.

x는 내부 scope를 벗어나는 순간 borrow 규칙에 의하여 메모리가 해제된다.

r은 x를 참조하고 있는데 내부 스코프가 끝난 후 x는 해제되고 r은 참조할 수 없는것을 참조하려고 하고 있기 때문이다.

 

빌림 검사기 (Borrow checker)

 rust 컴파일러에는 빌림 검사기라는 것이 있다.

모든 빌림이 유효한지 결정하기 위해 scope를 비교한다.

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

r의 라이프타임을 'a라고 명명했고, x의 라이프타임을 'b라고 명명했다.

'b 블록은 외부의 'a 라이프타임 블록에 비하여 훨씬 작다. 컴파일 타임에 rust는 두 라이프 타임의 크기를 비교하고 r이 'a 라이프타임을 가지고 있지만 'b 라이프타임을 가지고 있는 어떤 Object를 참조하고 있음을 보게 된다.

'a 라이프타임에서 'b 라이프타임을 참조하고 있고, 'b 라이프타임은 'a 라이프타임이 종료되기 전에 끝나기 때문에 rust 컴파일러는 참조자의 주체가 참조자만큼 오래 살아있지 않음을 확인하고 프로그램을 실행시키지 않는다.

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

이 코드는 댕글링 참조자를 만들지 않는 코드이다.

r의 라이프타임을 'a, x의 라이프타임을 'b라고 했을 때, 'a 라이프타임에서 'b 라이프타임의 변수를 참조하고 있으며, 'a 라이프타임과 'b 라이프타임은 동일하므로(같은시점에서 끝, 같은 스코프) rust 컴파일러는 r의 참조자가 x가 유효한 동한 언제나 유효할 것이라는 것을 알 수 있다.

 

함수에서의 제네릭 라이프타임

두개의 String slice 중 길이가 더 긴 String을 반환하는 함수이다.

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

pub fn run() {
    let str1 = String::from("abcd");
    let str2 = "efghd";

    let result = longest(str1.as_str(), str2);
    println!("{}", result);
}

 

 

위 코드를 실행시키려 했을 때 다음과 같은 오류를 얻게 된다.

반환 타입에 대하여 제네릭 라이프타임 파라미터가 필요하다고 한다. 

함수에 넘겨질 인자에 대한 구체적인  값을 모르기 때문에, 반환되는 참조자가 str1을 참조하는지 str2를 참조하는지 판단할 수 없다는 것이다. 

반환하는 참조자가 항상 유효한지를 결정하기 위해 스코프를 살펴볼 수도 없다. 

빌림 검사기 또한 이를 결정할 수 없다.

str1과 str2의 라이프타임이 반환 값의 라이프타임과 어떻게 연관되어 있는지 알지 못하기 때문이다.

참조자들 간의 관계를 정의하는 제네릭 라이프타임과 parameter를 추가하여 빌림검사기가 분석할 수 있도록 할것이다.

 

라이프타임 명시 문법

연관된 참조자의 라이프타임을 명시한다.

라이프타임 parameter는 apostrophe( ' )로 시작하며, lowercase 이다. 라이프타임 parameter명시는 참조자 키워드 '&' 다음에 위치하며, 공백 문자가 라이프타임 명시와 참조자의 타입을 구분해준다.

&i32        // 라이프타임 param이 없는 i32에 대한 참조자
&'a i32     // 'a라고 명명된 라이프타임 param을 가진 i32 참조자
&'a mut i32 // 'a를 갖고 있고 i32에 대한 가변 참조자

하나의 라이프타임 명시는 큰 의미를 가지고 있지 않는다. 라이프타임 명시는 rust에게 여러개의 참조자에 대한 제네릭 라이프타임 파라미터가 서로 어떻게 연관되는지를 알려준다.

만약 라이프타임 'a를 가지고 있는 i32에 대한 참조자인 first와 second를 parameter로 가진 함수가 있다면, 이 2개의 참조자가 같은 라이프타임을 가지고 있고, 동일한 제네릭 라이프타임만큼 살아야 한다는 것을 가리킨다.

 

함수 시그니처 내의 라이프타임 명시

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

시그니처 내의 모든 참조자들이 동일한 라이프타임 'a를 가지고 있어야 함을 특정한 longest함수이다.

이 함수는 라이프타임이 'a인 2개의 parameter를 가지게 되고, 두개 모두 적어도 'a만큼 살아 있는 String slice를 반환할 것이다.

 

함수 내 라이프타임을 명시할 때, 함수 시그니처에 작성 하며, 함수 본체 내의 코드에는 작성하지 않는다. 이는 rust컴파일러가 다른 도움없이 함수 내의 코드를 분석할 수 있지만, 함수가 그 함수 밖의 코드에서 참조자를 가지고 있을 때, 인자 혹은 반환값 들의 라이프타임이 함수가 호출될 때 마다 달라질 가능성이 있기 때문이다.

이를 rust 컴파일러가 찾기에는 너무 비용이 크고, 종종 불가능한 상황이 일어나므로, 코드 작성자가 스스로 라이프타임을 명시할 필요가 있다.

아래의 코드를 보자.

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

pub fn run() {
    let str1 = String::from("abcd");
    {
        let str2 = String::from("efghd");
        let result = longest(str1.as_str(), str2.as_str());
        println!("{}", result);
    }
}

이 코드에서 'a가 가리키는 라이프타임은 str2가 속한 스코프가 된다.

그리고 다음의 코드를 보자.

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

pub fn run() {
    let str1 = String::from("abcd");
    let result;
    {
        let str2 = String::from("efghd");
        result = longest(str1.as_str(), str2.as_str());
    }
    println!("{}", result);
}

이 str2가 스코프 밖으로 벗어난 후 result를 사용하려고 하는 시도는 컴파일 되지 않는다.

이는 result가 println!에서 유효하기 위해, str2가 외부 스코프의 끝까지 유효할 필요가 있음을 말해준다.

 

라이프타임 parameter를 특정하는 정확한 방법은 함수가 어떤일을 하고 있는가에 따라 달린 문제이다.

아래의 코드를 실행해보자.

fn sample<'a>(str1: &str, str2: &str) -> &'a str {
    let result = String::from("long long string");
    result.as_str()
}

 

반환 타입에 'a를 특정했을지라도, 컴파일 오류가 발생한다.

이는 반환되는 값의 라이프타임이 라이프타임 'a과 아무 관련이 없기 때문이다.

 result가 sample 함수가 끝나는 지점에서 scope 밖으로 벗어나기 때문에 메모리 해제가 된다. 그때 이 함수로부터 result의 참조자를 반환하려는 시도를 하므로 문제가 생긴다.

 

구조체의 라이프타임 명시

구조체에서 참조자들에 대한 라이프 타임을 표시하는 방법

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

pub fn run() {
    let novel = String::from("Lionel Andres Messi Cuccittini is GOAT...");
    let first_sentence = novel.split('.').next().expect("Banbak Bulga");
    let i = ImportantExcerpt {
        part: first_sentence
    };

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

novel이 소유하는 String의 첫 문장에 대한 참조자를 들고있는 ImportantExcerpt 구조체의 인스턴스를 생성한다.

 

라이프타임 생략

모든 참조자가 라이프타임을 가지고 있으며, 참조자를 사용하는 함수나 구조체에 대하여 라이프타임 parameter를 작성해야 된다고 배웠지만, 라이프타임 명시 없이 컴파일이 가능한 경우가 있다.

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for(i, &item) in bytes.iter().enumerate() {
        if item == b' '{
            return &s[0..i];
        }
    }
    &s[..]
}

parameter와 반환값이 참조자임에도 불구하고 라이프타임 명시 없이 컴파일이 되는 함수이다.

이러한 함수는 rust 1.0 이전에는 컴파일되지 않았었다. 모든 참조자들은 명시적인 라이프타임이 필요 했다.

하지만 특정 상황에서 똑같은 라이프탕미 명시를 계속 타이핑하고 있다는 점을 발견하고, 이 상황들은 예측 가능하며 몇가지 결정론적인 패턴을 따르고 있었다. 그래서 rust 개발팀은 컴파일러가 이 패턴들에 대하여 명시적으로 추가하도록 강제하지 않고 빌림 검사기가 라이프타임을 추론할 수 있도록 헀다.

 

참조자에 대한 rust의 분석 기능 내 프로그래밍된 패턴들을 일컬어 라이프타임 생략 규칙(lifetime elision rules)이라고 한다. 이 규칙은 컴파일러가 고려할 특정한 경우에 대한 내용이고, 만약 개발자가 이러한 경우에 들어맞을 경우 명시적으로 라이프타임을 작성할 필요 없이 생략 가능하다.

 

생략 규칙들은 모든 추론을 제공하지는 않는다.

만약 rust가 참조자들이 어떤 라이프타임을 가지고 있는지에 대하여 모호하다면, 해당하는 남은 참조자들의 라이프타임이 어떻게 되는지 추측하지 않고, 컴파일러는 오류를 출력하게 된다.

 

함수나 메서드의 parameter에 대한 라이프타임을 입력 라이프타임(Input lifetime)이라고 하며, 반환 값에 대한 라이프타임을 출력 라이프타임(Output lifetime)이라고 한다.

명시적인 라이프타임이 없을 때 참조자가 어떤 라이프타임을 가져야 하는지 알아내기위해 컴파일러가 사용하는 규칙이다.

라이프타임 규칙

1. 참조자 각각의 parameter는 고유한 라이프타임 parameter를 갖는다.
   하나의 parameter를 갖는 함수는 하나의 라이프타임 parameter를 갖고(fn foo<'a>(x: &'a i32), 
   두개의 parameter를 갖는 함수는 두개의 라이프타임 parameter를 따로 갖는다.(fn foo<'a, 'b>(x: &'a i32, y: &'b i32))
2. 만약 하나의 라이프타임 parameter만 있다면, 그 라이프타임이 모든 출력 라이프타임 parameter들에 
   대입된다.fn foo<'a>(x: &'a i32) -> &'a i32
   fn foo<'a>(x: &'a i32, y: &i32) -> &'a i32는 error가 출력된다.
3. 만약 여러개의 입력 라이프타임 parameter가 있는데, 메서드라서 그 중 하나가 &self 혹은 &mut self라고 
   한다면, self의 라이프타임이 모든 출력 라이프타임 parameter에 대입된다.

첫번째 규칙은 입력 라이프타임에 적용되고, 두번째, 세번째 규칙은 출력 라이프타임에 적용된다.

만약 컴파일러가 이 세가지 규칙의 끝에 도달하고 여전히 라이프타임을 알아낼 수 없는 참조자가 있다면 컴파일러는 에러와 함께 멈춘다.

 

여러가지 예를 살펴보자

EX1

fn foo(x: &str) -> &str {}

첫번째 규칙을 적용한다.

각각의 parameter가 고유의 라이프타임을 갖는다.

 

EX2

fn foo<'a>(x: &'a str) -> &str {}

두번째 규칙을 적용한다.

하나의 입력 라이프타임만 존재한다. 

두번째 규칙에 의하여 하나의 입력 파라미터에 대한 라이프타임이 출력 라이프타임에 대입된다.

그럴 경우 해당 예시는 아래의 코드와 동일하다.

fn foo<'a>(x: &'a str) -> &'a str {}

함수 시그니처의 모든 참조자들이 라이프타임을 갖게 되었고, 컴파일러는 개발자에게 이 함수 시그니처 내의 라이프타임을 명시하도록 요구하지 않고도 분석을 계속 할 수 있게 되었다.

 

EX3

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

첫번째 규칙에 의하여 각각의 parameter에는 고유의 라이프 타임을 가진다.

두번째 규칙은 하나 이상의 입력 라이프타임이 있으므로 적용되지 않는다.

세번째 규칙은 메소드가 아니라 함수이므로 어떠한 상황에서도 self가 아니므로 적용되지 않는다.

모든 규칙을 확인했는데 반환타입의 라이프타임을 알수가 없다.

따라서 위 코드는 오류를 출력한다.

메소드 정의 내에서의 라이프타임 명시

라이프타임을 가진 구조체에 대한 메소드를 구현할 때 제네릭과 비슷하다.

라이프타임 parameter가 선언되고 사용되는 곳은 라이프타임 parameter가 구조체의 필드들 또는 메서드 인자와 반환 값과 연관이 있는지 없는지에 따라 다르다.

구조체 필드를 위한 라이프타임은 항상 impl 뒤에 명시되어야 하고, 구조체의 이름 뒤에 사용되어야 하는데, 이 라이프타임들은 구조체 타입의 일부이기 때문이다.

 

impl 블록 내 메서드 시그니처에서, 참조자들이 구조체 필드에 있는 참조자들의 라이프타임과 묶일수도 있고, 혹은 서로 독립적일 수 있다.

여기에 더해, 라이프타임 생략 규칙이 종종 적용되어 메서드 시그니처 내 라이프타임을 명시할 필요가 없을 수 있다.

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

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl뒤의 라이프타임 파라미터 선언부와 타입 이름 뒤에서 이를 사용해야 되지만, 첫 번째 생략규칙 때문에 self로의 참조자 라이프타임을 명시할 필요가 없다.

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

위 코드는 3번째 규칙이 적용된다.

2개의 입력 라이프타임이 있으므로 첫번째 규칙을 적용하여 모든 입력 라이프타임에 대하여 &self와 announcement에 대하여 고유한 라이프타임이 적용된다. 그 다음 파라미터 중 하나가 self이므로, 반환타입은 &self의 라이프타임을 얻고 모든 라이프타임이 추론되었다.

 

정적 라이프타임

정적 라이프타임은 프로그램의 전체 생애주기를 가리키는 특별한 라이프타임이다.

'static로 표기된다.

모든 string 리터럴은 'static라이프타임을 가지고 있다.

//같은 내용이다.
let s: &str = "test";
let s: &'static str = "test";

 

제네릭 타입 파라미터, 트레잇 바운드, 라이프타임을 함께 써보기

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str where T: Display {
    println!("Announcement {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

위에서의 longest함수와 동일한 기능인 2개의 string slice 중 긴쪽을 반환한다.

ann이라는 이름의 추가인자를 가지고 있고 제네릭 타입이다.

where절을 가지고 특정한바와 같이 Display 트레잇을 구현한 어떤 타입으로도 채워질 수 있다.

println!()으로 ann을 출력해야 되기 때문에 Display trait바운드가 필요하다.

Display trait바운드가 없다면 오류가 출력된다.

 

 

 

https://rinthel.github.io/rust-lang-book-ko/ch10-03-lifetime-syntax.html

 

라이프타임을 이용한 참조자 유효화 - The Rust Programming Language

4장에서 참조자에 대한 이야기를 할 때, 중요한 디테일을 한 가지 남겨두었습니다: 러스트에서 모든 참조자는 라이프타임(lifetime) 을 갖는데, 이는 해당 참조자가 유효한 스코프입니다. 대부분의

rinthel.github.io

 

'Rust-Language' 카테고리의 다른 글

매크로  (0) 2021.12.02
trait 다른 타입 간 허용  (0) 2021.09.15
trait: 공유 동작 정의  (0) 2021.09.08
반복자, iterator  (0) 2021.09.03
Closure, 클로저 함수  (0) 2021.08.05
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함