다른 프로그래밍 언어(C/C++, Python, Java, Ruby, Lisp 등)를 사용해 본 적이 있다면, null이나 nil을 접해 봤을 거예요.
null이나 nil은 이런 언어들이 특정 변수에 값이 없다는 것을 나타내는 방식이에요.
하지만 덕분에 null을 가리키는 변수를 실수로 쓰는 일이 아주 쉬운(그리고 흔한) 실수가 돼요.
짐작하겠지만, 없는 함수를 호출하거나 없는 값에 접근하려고 하면 온갖 버그와 크래시로 이어질 수 있어요.
null을 만든 사람은 그것을 두고 자신의 '10억 달러짜리 실수'.라고 부르기까지 했어요.
Option 타입이런 문제를 피하기 위해 Rust는 널 참조를 사용하지 않아요.
그렇지만 특정 변수에 값이 없다는 것을 나타낼 안전한 방법은 여전히 필요해요.
이때 등장하는 것이 Option이에요.
값이 없는 변수를 두는 대신, Rust의 변수는 Option 열거형을 사용할 수 있어요.
이 열거형에는 두 가지 배리언트가 있어요: Rust에서 null에 해당하는 None과, T가 임의의 타입의 값인 Some(T)예요.
다음과 같이 생겼어요:
enum Option<T> {
None,
Some(T),
}
Option은 널 참조가 일으킬 수 있는 문제들로부터 나를 지켜 주는 안전 장치라고 생각하면 돼요. 그러면서도 널 참조가 지닌 개념적 유용성은 그대로 유지해 줘요.
Option 사용하기변수를 None으로 설정하는 것은 꽤 간단해요:
let nothing: Option<u32> = None; // Variable nothing is set to None
하지만 Option 타입이 값을 담게 하고 싶다면, 그 값을 직접 대입할 수는 없어요.
Option 타입 변수와, 이를테면 i32 타입 변수는 서로 같지 않아요.
Some을 사용해야 해요:
let wrong_way: Option<i32> = -4; // This will not work
let right_way: Option<i32> = Some(-4); // This will work
let another_right_way = Some(-4); // Compiler infers that this is Option<i32>
같은 이유로 다음 코드도 작동하지 않아요:
let number = 47;
let option_number = Some(15);
let compile_error = number + option_number; // Cannot add an i32 and an Option<i32> - they are of different types
Some이 담고 있는 값을 얻고 싶다면, 먼저 그 값이 존재하는지 확인해야 해요:
let mut some_words = Some("choose something to say"); // some_words set to something
match some_words {
Some(str) => println!("Here, we will {}", str),
None => println!("I've got nothing to say"),
} // Prints "Here, we will choose something to say"
some_words = None; // some_words now set to None
// exactly the same match block as above
match some_words {
Some(str) => println!("Here, we will {}", str),
None => println!("I've got nothing to say"),
} // Prints "I've got nothing to say"
match 외에도 Rust에는 Option에 담긴 값을 확인하고 접근할 수 있는 다른 도구들이 있어요. 하지만 match는 이제 익숙할 거예요.
덧붙여, 이 예제는 Rust가 왜 널 참조 대신 Option을 사용하는지 보여 주는 예라고 생각해 봐요.
핵심은 Option 변수가 Some인지(이 경우에는 안에 담긴 값을 꺼내서 사용하면 돼요), 아니면 None인지 반드시 확인해야 한다는 점이에요.
그 외의 경우에는 프로그램이 컴파일되지 않아요. 컴파일러가 null로부터 나를 안전하게 지켜 주는 거예요.
롤플레잉 게임을 구현하는 작업을 하고 있어요. 플레이어의 캐릭터는 다음과 같이 표현돼요:
pub struct Player {
health: u32,
mana: Option<u32>,
level: u32,
}
이 게임의 플레이어는 레벨 10에 도달해야 마나 풀을 열 수 있고, 그때부터 주문을 시전할 수 있어요. 그 전까지 Player의 마나는 None이에요.
이 게임에서 두 가지 기능, 부활 기능과 주문 시전 기능을 작업할 거예요.
revive 메서드는 Player가 실제로 죽었는지(체력이 0이 되었는지) 확인하고, 죽었다면 체력이 100인 새로운 Player 인스턴스를 반환해야 해요.
Player의 레벨이 10 이상이면 마나도 100으로 함께 부활해야 해요.
Player의 레벨이 10 미만이면 마나는 None이어야 해요. revive 메서드는 Player의 레벨을 그대로 유지해야 해요.
let dead_player = Player { health: 0, mana: None, level: 2 };
dead_player.revive()
// Returns Player { health: 100, mana: None, level: 2 }
체력이 1 이상인 Player에 대해 revive 메서드를 호출하면 None을 반환해야 해요.
let alive_player = Player { health: 1, mana: Some(15), level: 11 };
alive_player.revive()
// Returns None
cast_spell 메서드는 Player에 대한 가변 참조와, 주문에 드는 마나의 양을 나타내는 mana_cost 매개변수를 받아요. 이 메서드는 시전한 주문이 입히는 피해량을 반환하는데, 주문 시전에 성공하면 항상 주문의 마나 비용의 두 배예요.
플레이어가 마나 풀에 접근할 수 없다면, 주문을 시전하려고 할 때 체력이 주문의 마나 비용만큼 감소해야 해요. 반환되는 피해량은 0이어야 해요.
let not_a_wizard_yet = Player { health: 79, mana: None, level: 9 };
assert_eq!(not_a_wizard_yet.cast_spell(5), 0)
assert_eq!(not_a_wizard_yet.health, 74);
assert_eq!(not_a_wizard_yet.mana, None);
플레이어에게 마나 풀이 있지만 마나가 부족하면, 메서드는 마나 풀에 영향을 주지 않고 대신 0을 반환해야 해요.
let low_mana_wizard = Player { health: 93, mana: Some(3), level: 12 };
assert_eq!(low_mana_wizard.cast_spell(10), 0);
assert_eq!(low_mana_wizard.health, 93);
assert_eq!(low_mana_wizard.mana, Some(3));
그 외의 경우에는 Player의 마나 풀에서 mana_cost를 차감하고, 알맞은 피해량을 반환해야 해요.
let wizard = Player { health: 123, mana: Some(30), level: 18 };
assert_eq!(wizard.cast_spell(10), 20);
assert_eq!(wizard.health, 123);
assert_eq!(wizard.mana, Some(20));
즐겁게 해봐요!