검사받는 초기화되지 않은 메모리
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
가 새로운 변수처럼 보일 테니까요.