Option 형식을 사용하여 부재 처리

완료됨

Rust 표준 라이브러리는 값이 없을 가능성이 있을 때 사용되는 Option<T> 열거형을 제공합니다. Option<T>은 Rust 코드에서 널리 사용되며, 존재하거나 비어 있을 수 있는 값을 사용할 때 유용합니다.

다른 많은 언어에서 값이 없는 경우 null 또는 nil을 사용하여 모델링되지만 Rust는 다른 언어와 상호 운용되는 코드 외부의 null을 사용하지 않습니다. Rust는 값이 선택적인 경우에 대해 명시적입니다. 대부분의 언어에서 String을 사용하는 함수는 실제로 String 또는 null을 사용할 수 있으며 Rust에서 같은 함수는 실제로는 String만 사용할 수 있습니다. Rust에서 선택적 문자열을 모델링하려면 Option 형식 Option<String>으로 명시적이게 래핑해야 합니다.

Option<T>는 다음 두 변형 중 하나로 자체를 나타냅니다.

enum Option<T> {
    None,     // The value doesn't exist
    Some(T),  // The value exists
}

Option<T> 열거형 선언의 <T> 부분은 T 형식이 제네릭이며 Option 열거형의 Some 변형과 연결된다고 명시합니다.

이전 섹션에서 설명한 것처럼 NoneSome은 형식이 아니지만 Option<T> 형식의 변형입니다. 즉, 무엇보다도 함수는 Some 또는 None을 인수로 사용할 수 없지만 Option<T>으로만 사용할 수 있습니다.

이전 단원에서 벡터의 존재하지 않는 인덱스에 액세스하려고 시도하면 프로그램이 panic하게 된다고 설명했지만, 패닉 대신 Option 형식을 반환하는 Vec::get 메서드를 사용하면 이를 방지할 수 있습니다. 지정된 인덱스에 값이 있으면 Option::Some(value) 변형으로 래핑됩니다. 인덱스가 범위를 벗어나면 Option::None 값을 대신 반환합니다.

사용해 보겠습니다. 다음 코드를 로컬로 또는 Rust 플레이그라운드에서 실행할 수 있습니다.

let fruits = vec!["banana", "apple", "coconut", "orange", "strawberry"];

// pick the first item:
let first = fruits.get(0);
println!("{:?}", first);

// pick the third item:
let third = fruits.get(2);
println!("{:?}", third);

// pick the 99th item, which is non-existent:
let non_existent = fruits.get(99);
println!("{:?}", non_existent);

출력은 다음과 같습니다.

Some("banana")
Some("coconut")
None

출력된 메시지는 fruits 배열의 기존 인덱스에 액세스하려는 처음 두 번의 시도는 Some("banana")Some("coconut")으로 이어졌지만, 99번째 요소를 가져오려는 시도는 패닉 대신 데이터와 연결되지 않은 None 값을 반환했다는 것을 알려줍니다.

실제로 프로그램이 얻게 되는 열거형 변형에 따라 프로그램의 동작 방식을 결정해야 합니다. 하지만 Some(data) 변형 내부의 데이터에 액세스하려면 어떻게 해야 할까요?

패턴 일치

Rust에는 match라는 강력한 연산자가 있습니다. match는 패턴을 제공하여 프로그램의 흐름을 제어하는 용도로 사용할 수 있습니다. match가 일치하는 패턴을 찾으면 사용자가 해당 패턴과 함께 제공한 코드를 실행합니다.

let fruits = vec!["banana", "apple", "coconut", "orange", "strawberry"];
for &index in [0, 2, 99].iter() {
    match fruits.get(index) {
        Some(fruit_name) => println!("It's a delicious {}!", fruit_name),
        None => println!("There is no fruit! :("),
    }
}

Rust 플레이그라운드에서 이 예제를 실행해 볼 수 있습니다.

출력은 다음과 같습니다.

It's a delicious banana!
It's a delicious coconut!
There is no fruit! :(

위의 코드에서는 이전 예제(0, 2, 99)와 동일한 인덱스를 반복한 다음, 각 인덱스를 사용하여 fruits.get(index) 식을 통해 fruits 벡터에서 값을 검색합니다.

fruits 벡터는 &str 요소를 포함하기 때문에 이 식의 결과가 Option<&str> 형식이라는 것을 알 수 있습니다. 그런 다음, Option 값에 대해 match 식을 사용하고 각 변형에 대한 작업 과정을 정의합니다. Rust는 이러한 분기를 match arm으로 참조하고, 각 arm은 매칭된 값에 대해 가능한 결과 하나를 처리할 수 있습니다.

첫 번째 arm에서는 새 변수인 fruit_name이 도입됩니다. 이 변수는 Some 값 안의 임의의 값과 일치합니다. fruit_name의 범위는 match 식으로 제한되므로 match에서 fruit_name을 도입하기 전에 fruit_name을 선언하는 것은 적절하지 않습니다.

Some 변형 내의 값에 따라 다르게 작동하도록 match 식을 더욱 구체화할 수 있습니다. 예를 들어 다음 코드를 실행하여 코코넛이 매우 맛있다고 강조할 수 있습니다.

let fruits = vec!["banana", "apple", "coconut", "orange", "strawberry"];
for &index in [0, 2, 99].iter() {
    match fruits.get(index) {
        Some(&"coconut") => println!("Coconuts are awesome!!!"),
        Some(fruit_name) => println!("It's a delicious {}!", fruit_name),
        None => println!("There is no fruit! :("),
    }
}

참고

일치 항목의 첫 번째 패턴은 Some(&"coconut")(문자열 리터럴 앞의 & 참조)입니다. 이는 fruits.get(index)이 문자열 조각에 대한 참조의 Option<&&str> 또는 옵션을 반환하기 때문입니다. 패턴에서 &를 제거하는 것은 Option<&str>(문자열 조각에 대한 선택적 참조가 아닌 선택적 문자열 조각)를 기준으로 일치시키려는 것을 의미합니다. 참조를 다루지 않았으므로 현재로는 제대로 의미가 통하지 않을 수 있습니다. 일단은 &가 형식이 제대로 정렬되어 있는지 확인한다는 것만 명심하세요.

Rust 플레이그라운드에서 이 예제를 실행해 볼 수 있습니다.

출력은 다음과 같습니다.

It's a delicious banana!
Coconuts are awesome!!!
There is no fruit! :(

문자열 값이 "coconut"이면 첫 번째 arm이 매칭된 다음, 실행 흐름을 결정하는 데 사용됩니다.

match 식을 사용할 때마다 다음 규칙을 염두에 두어야 합니다.

  • match arm은 위에서 아래로 평가됩니다. 특정 사례는 일반 사례보다 먼저 정의해야 합니다. 그러지 않으면 특정 사례가 일치 및 평가되지 않습니다.
  • match arm은 입력 형식으로 가능한 모든 값을 포함해야 합니다. 완전하지 않은 패턴 목록에 대해 매칭하려고 시도하면 컴파일러 오류가 발생합니다.

if let

Rust는 값이 단일 패턴을 따르는지 여부를 테스트하는 편리한 방법을 제공합니다.

아래 예제에서 match의 입력은 Option<u8> 값입니다. match 식은 입력값이 7인 경우에만 코드를 실행해야 합니다.

let a_number: Option<u8> = Some(7);
match a_number {
    Some(7) => println!("That's my lucky number!"),
    _ => {},
}

여기서는 Some(7)과 일치하지 않는 모든 Some<u8> 값과 None variant를 무시하려고 합니다. 이러한 상황에서는 와일드카드 패턴이 유용합니다. 그 외의 모든 것을 매칭하도록 다른 모든 패턴 뒤에 _(밑줄) 와일드카드 패턴을 추가한 다음, 매우 까다로운 일치 항목에 대한 컴파일러 요구를 충족시키는 데 사용할 수 있습니다.

이 코드를 간결하게 만들려면 if let 식을 사용할 수 있습니다.

let a_number: Option<u8> = Some(7);
if let Some(7) = a_number {
    println!("That's my lucky number!");
}

if let 연산자는 패턴을 식과 비교합니다. 식이 패턴과 일치하면 if 블록이 실행됩니다. if let 식의 장점은 매칭할 단일 패턴에 관심이 있을 때 match 식의 상용구 코드 중 일부만 필요하다는 점입니다.

unwrapexpect 사용

unwrap 메서드를 사용하여 Option 형식의 내부 값에 직접 액세스를 시도할 수 있습니다. 그러나 변형이 None이면 이 메서드가 패닉 상태가 되기 때문에 주의해야 합니다.

예를 들면 다음과 같습니다.

let gift = Some("candy");
assert_eq!(gift.unwrap(), "candy");

let empty_gift: Option<&str> = None;
assert_eq!(empty_gift.unwrap(), "candy"); // This will panic!

이 예제에서는 코드가 다음 출력과 함께 패닉합니다.

    thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:6:27

expect 메서드는 unwrap과 동일한 작업을 수행하지만, 두 번째 인수를 통해 제공되는 사용자 지정 패닉 메시지를 제공합니다.

let a = Some("value");
assert_eq!(a.expect("fruits are healthy"), "value");

let b: Option<&str> = None;
b.expect("fruits are healthy"); // panics with `fruits are healthy`

출력은 다음과 같습니다.

    thread 'main' panicked at 'fruits are healthy', src/main.rs:6:7

해당 함수는 패닉할 가능성이 있으므로 사용하지 않는 것이 좋습니다. 그 대신 다음 방법 중 하나를 사용할 수 있습니다.

  • 패턴 매칭을 사용하고 None 사례를 명시적으로 처리합니다.
  • unwrap_or처럼 변형이 None이면 기본값을 반환하고 변형이 Some(value)이면 내부 값을 반환하는 유사한 비 패닉 메서드를 호출합니다.
assert_eq!(Some("dog").unwrap_or("cat"), "dog");
assert_eq!(None.unwrap_or("cat"), "cat");