검사받는 초기화되지 않은 메모리

C와 같이, 러스트에서 모든 스택 변수는 값이 배정되기 전까지 초기화되지 않은 상태입니다. C와 다르게, 러스트는 값을 배정하기 전에 그 변수들을 읽는 것을 컴파일 때 방지합니다:

fn main() {
    let x: i32;
    println!("{}", x);
}
  |
3 |     println!("{}", x);
  |                    ^ use of possibly uninitialized `x`

이것은 기본적인 경우 검사에 기반한 것입니다: x가 처음 쓰이기 전에 모든 경우에서 x에 값을 할당해야 합니다. 짧게 말하자면, 우리는 "x가 초기화됐다" 혹은 "x가 미초기화됐다"고도 말합니다.

흥미롭게도, 만약 늦은 초기화를 할 때 모든 경우에서 변수에 값을 정확히 한 번씩 할당한다면, 러스트는 변수가 가변이어야 한다는 제약을 걸지 않습니다. 하지만 이 분석은 상수 분석 같은 것을 이용하지 못합니다. 따라서 이 코드는 컴파일되지만:

fn main() {
    let x: i32;

    if true {
        x = 1;
    } else {
        x = 2;
    }

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

이건 아닙니다:

fn main() {
    let x: i32;
    if true {
        x = 1;
    }
    println!("{}", x);
}
  |
6 |     println!("{}", x);
  |                    ^ use of possibly uninitialized `x`

이 코드는 컴파일됩니다:

fn main() {
    let x: i32;
    if true {
        x = 1;
        println!("{}", x);
    }
    // 초기화되지 않는 경우가 있지만 신경쓰지 않습니다
    // 그 경우에서는 그 변수를 사용하지 않기 때문이죠
}

당연하게도, 이 분석이 진짜 값을 신경쓰는 건 아니지만, 프로그램 구조와 의존성에 대한 비교적 높은 수준의 이해를 하고 있습니다. 예를 들어, 다음의 코드는 작동합니다:

#![allow(unused)]
fn main() {
let x: i32;

loop {
    // 러스트는 이 경우가 조건 없이 실행될 거라는 것을 알지 못합니다
    // 이 경우는 실제 값에 의존하기 때문이죠
    if true {
        // 하지만 러스트는 이 경우가 정확히 한 번 실행될 거라는 것을 압니다
        // 우리가 조건 없이 이 경우를 `break`하기 때문이죠.
        // 따라서 `x`는 가변일 필요가 없습니다.
        x = 0;
        break;
    }
}
// 또한 러스트는 `break`에 닿지 않고서는 여기에 도달할 수 없다는 것을 압니다.
// 그리고 따라서 여기의 `x`는 반드시 초기화된 상태라는 것도요!
println!("{}", x);
}

만약 어떤 변수에서 값이 이동한다면, 그 타입이 Copy가 아닐 경우 그 변수는 논리적으로 미초기화된 상태가 됩니다. 즉:

fn main() {
    let x = 0;
    let y = Box::new(0);
    let z1 = x; // `i32`는 `Copy`이므로 `x`는 여전히 유효합니다
    let z2 = y; // `Box`는 `Copy`가 아니므로 `y`는 이제 논리적으로 미초기화 되었습니다
}

하지만 이 예제에서 y에 값을 다시 할당하려면 y가 가변이어야 할 겁니다, 안전한 러스트에서 프로그램이 y의 값이 변했다는 것을 볼 수 있을 테니까요:

fn main() {
    let mut y = Box::new(0);
    let z = y; // `Box`는 `Copy`가 아니므로 `y`는 이제 논리적으로 미초기화 되었습니다
    y = Box::new(1); // `y`를 재초기화 합니다
}

그게 아니라면 마치 y가 새로운 변수처럼 보일 테니까요.