[0]
프로그래밍 언어에서 null이 도입된 이후에, 이와 관련된 무수한 버그들이 발생하였다. 대표적으로 null pointer dereference가 있는데, 이는 디버깅도 굉장히 어렵다. Rust에서는 이러한 이유로 null을 도입하지 않는다. 대신에 Option<T>라는 enum을 제공한다.
[1]
Option<T>는 std::option에 다음과 같이 정의되어 있다.
enum Option<T> {
Some(T),
None,
}
여기서 Some(T)는 해당 값이 non-null일 경우, None은 null일 경우에 대응한다고 생각하면 된다. 따라서 다음과 같은 방식으로 사용할 수 있다.
fn add(num: Option<i32>) -> Option<i32> {
match num {
None => None,
Some(i) => Some(i + 1),
}
}
add()는 int형의 인자를 하나 받아 1을 더해주고 리턴하는 단순한 함수다. null로 구현되어 있는 언어에서 if문을 통해 num이 null인지 아닌지 확인하는 것처럼, Rust에서도 유사하게 match를 통해 Option<T>가 None인지 Some인지를 확인하고 해당하는 동작을 한다. 여기까지는 다른 언어들이랑 유사하게 보인다. 그렇다면 Rust에서 null대신 Option<T>로 구현한 이유는 무엇일까?
[2]
핵심은 바로 Option<T>와 T가 다른 타입이라는 것이다. null이 구현되어 있는 언어에서는, 오브젝트에 null이 들어가거나, 의미 있는 값이 들어가거나 같은 타입이다. 예를 들어 다음과 같은 c프로그램은 아무 문제없이 컴파일된다.
void * get_addr(){
void * ptr = get_addr_from_somewhere(); //can be null!
return ptr;
}
위의 코드에서, ptr에 의미 있는 주소 값이 저장되든 아니든, return은 같은 void * 타입으로 이루어진다. 따라서 get_addr()을 호출한 함수에서, null체크를 해주지 않는다면 null pointer dereference와 같은 메모리 취약점이 발생할 수 있다. 반면 Rust에서는 Option<T>와 T가 완전히 다른 타입이기 때문에, 타입을 맞추지 않는다면, 컴파일 타임에 에러가 발생하게 된다.
[3]
그럼 Option의 퍼포먼스 오버헤드는 어떨까? 어느 정도 예상 가능하다싶이, Rust 컴파일러가 Option을 포인터로 컴파일 타임에 변환하기 때문에 런타임 오버헤드는 사실상 0이라고 할 수 있다! 되도록 많은 체크와 작업들을 컴파일 타임에 하고, 런타임 오버헤드는 없도록 하는 Rust의 철학이 다시한번 반영되었다고 생각하면 된다.
다만 메모리 오버헤드는 어느정도 발생한다. C의 union과 마찬가지로, 원소 중 가장 큰 type의 사이즈를 갖도록 구현되어 있기 때문이다.(Enum이 어느 타입을 가질지 알 수 없으므로, 원소 중 가장 큰 type의 사이즈에 맞출 수밖에 없다.) 따라서, 포인터의 사이즈(32비트면 4바이트, 64비트면 8바이트)보다 작은 타입의 경우에는 Option<T>에서 그만큼의 메모리 낭비가 발생한다. 예컨대 i32는 4바이트지만 Option<i32>는 8바이트와 같은 식으로. 하지만 null값을 가정하는 타입이 거의 레퍼런스임을 감안하면 크게 부담되는 오버헤드는 아니라고 할 수 있다.
[참고]
https://doc.rust-lang.org/book/
https://stackoverflow.com/questions/16504643/what-is-the-overhead-of-rusts-option-type
'Computer Science > Rust' 카테고리의 다른 글
Rust의 async/await와 Future (0) | 2020.07.21 |
---|---|
Rust의 Copy trait와 Clone trait (2) | 2020.06.30 |
Rust의 스마트 포인터 (0) | 2020.05.20 |
Rust의 lifetime parameter (0) | 2020.05.05 |
Rust의 Trait (1) | 2020.03.10 |