&str, String과 lifetime

이정후·2023년 7월 11일
0

Rust

목록 보기
3/13
post-thumbnail

&str, String?

&str"string slice"라고 부르며, 메모리 상의 문자열 데이터의 문자에 대한 불변성을 제공한다. 즉 &str은 데이터를 소유하지 않고 해당 데이터에 대한 참조만을 갖는다.

String은 문자열 데이터의 소유권을 갖는 타입이다. String&str과 다르게 힙에 할당된 문자열 데이터를 갖고 있으며 필요에 따라 크기를 동적으로 조정할 수 있다. 문자열 생성, 수정 및 소유할 수 있는 기능을 제공한다.

&str은 불변하고 고정된 문자열을 참조하는데 사용하며, String은 가변적이고 동적인 문자열을 소유하는데 사용한다.

Rust에서 여러가지 문자열 타입이 있는 이유는 각각의 상황에 맞는 최적의 퍼포먼스와 유연성을 제공하기 위함이다. &str은 작고 불변인 문자열에 적합하며, String은 동적이고 가변적인 문자열에 적합하다. 이는 Rust에서 메모리 안정성과 성능을 보장하는데 도움이 된다.

LifeTime

Rust에서 모든 참조자는 lifetime을 갖는데, 이는 해당 참조자가 유효한 스코프를 얘기한다. 대부분의 경우에서 타입들이 추론되는 것과 마찬가지로, 대부분의 경우에서 lifetime 또한 암묵적으로 추론된다. 비슷하게 우리가 몇몇 상황에서 타입을 직접 명시해줘야 하는 것과 동일하게, 직접 lifetime을 작성 해줘야 하는 경우도 있다.

dangling reference

dangling reference는 적절한 타입의 유효한 객체를 가리키고 있지 않는 포인터를 말한다. 이미 메모리에서 해제된 값 처럼 빈 공간, 유효하지 않은 목적지에 대한 참조를 말한다.
우리 프로그램은 할당 해제된 메모리를 다른 프로세스에게 재할당 하겠지만, 기존 프로그램이 댕글링 레퍼런스를 참조하게 된다면, 메모리는 전혀 다른 데이터를 가지고 있을 것이므로 예측할 수 없는 결과나 값을 받게 될 것이다.

Rust는 컴파일 타임에 추적하여 스택에 할당되는 객체와 변수의 생성과 소멸 시기를 모두 결정하고 잡아내며, 검증하여 이 dangling pointer를 방지한다.

생명주기 - lifetime

기본적으로 변수의 lifetime은 생성될 때 시작되고 소멸될 때 끝난다.

fn main() {
    {
        let r;

        {
            let x = 5;
            r = &x;
        }
        println!("r is {}", r);
    }
}

외부 스코프에서 변수 r은 선언되었으나 할당은 이루어지지 않았다. 내부 스코프에서 변수 x에 5의 값이 할당되었고, r은 내부 스코프의 x에 대한 참조라를 할당하였다. r의 값을 출력해보자.

error[E0597]: `x` does not live long enough     
  --> main.rs:8:17
   |
7  |             let x = 5;
   |                 - binding `x` declared here
8  |             r = &x;
   |                 ^^ borrowed value does not live long enough
9  |         }
   |         - `x` dropped here while still borrowed
10 |
11 |         println!("r is {}", r);
   |                             - borrow later used here

error: aborting due to previous error

실행하면 컴파일러는 다음과 같은 결과를 보여준다.
'x' does not live long enough

x가 충분히 오래 살지 못한다.

변수 x가 죽는 시점(dropped)은 내부 스코프가 닫히는 9번 라인일 것이다. 그러나 그 값을 담고있는 r은 외부 스코프에 존재하며 아직 살아있는 상태이다. 이 코드가 만약 정상적으로 실행되게 된다면, 할당이 해제된 메모리를 참조하게 되는 것이고 문제가 생길 것이다.

fn main() {

    {
        let r;

        {
            let x = 5;
            r = &x;
            println!("r is {}", r);
        }
    }
}

출력문을 내부 스코프 안으로 보내게 된다면 이는 유효한 코드이다. 데이터가 참조하는 값 보다 더 긴 lifetime을 가지기 때문이다.

Explict annotation

borrow checker는 명시적 수명 주석을 사용하여 참조가 유효한지를 판별한다. Rust에서는 수명을 결정하기 위해서 또는 필요한 경우 명시적으로 수명을 작성해야 한다. 추가 하기 위해서는 '를 사용한다.

struct School<'a> {}

&'a i32
&'a mut i32

수명 파라미터라고 하며, 참조자의 수명을 검증하기 위한 일종의 제네릭이다. 참조자를 인자로 받는 함수나 참조자를 가지는 구조체 및 튜플에 정의하며, 아직 결정되지 않은 피참조 객체의 수명에 묶이게 된다.

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

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

    let result = longest(string1.as_str(), string2);
}

이렇게 두개의 문자를 비교하여 긴 쪽을 반환하는 longest 함수가 있는데 컴파일 되지 않는다.

error[E0106]: missing lifetime specifier  
 --> tempCodeRunnerFile.rs:1:33
  |
1 | 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
  |
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

error: aborting due to previous error

함수의 리턴값이 borrowed value 즉 빌린 값(차용 값)이 포함되어 있는데, x에서 참조를 하는지, y에서 참조를 하는지 알 수 없다.

그리고 컴파일러는 친절하게 해결책을 제시해주는데 함수, 매개변수, 반환 값에 라이프 타임을 명시하면 된다. 이는 이 값들이 모두 동일한 lifetime을 가지고 있어야 한다는 뜻이다.

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

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

    let result = longest(string1.as_str(), string2);

    println!("{}", result);
}

어떤 lifetime 'a 에 대하여 라고 읽고, 이 함수의 파라미터 a와 b는 적어도 lifetime 'a 만큼 살아있는 &str임을 명시한다. 그리고 lifetime 'a 만큼 살아있는 &str을 반환한다.

lifetime의 생략

어떤 코드들, 혹은 대부분의 코드들은 lifetime을 위와같이 명시하지 않아도 제대로 동작한다. 그럼 컴파일러는 어떤 기준으로 이를 판단하게 될까?

  1. 참조자인 각각의 파라미터는 고유한 lifetime 파라미터를 가진다.
  2. 만일 하나의 lifetime 파라미터만 가지고 있다면, 그 lifetime 파라미터가 모든 출력의 lifetime 파라미터에 대입된다.
  3. 만일 여러 개의 lifetime 파라미터가 있는데, 그 중 하나가 메소드라서 &self, 혹은 &mut self 라고 한다면 self 의 lifetime이 모든 lifetime 파라미터에 대입된다.

Static lifetime

Rust에서 lifetime은 제네릭과 흡사하다고 했다. lifetime 파라미터는 사실 사용자의 입맛에 맞게 그 이름을 바꿀 수 있는데, 여기서 static은 이미 예약된 lifetime 파라미터이다.

'static lifetime은 프로그램 전체의 lifetime을 얘기한다. 모든 스트링 리터럴은 'static lifetime을 가지고 있다. 명시를 한다면 아래와 같다.

let my_str: &'static str = "static lifetime";
profile
꾸준하게

0개의 댓글