Unwrapping in Rust

Unwrapping Data from Rust's Option and Result Types

Unwrapping in Rust

개요

현대적 프로그래밍 언어에서 컨테이너형 타입에서 데이터를 안전하게 꺼내는 방법이 많이 사용되고 있습니다. 특히 러스트 프로그래밍 언어는 다양한 방법을 제공해는데 자주 사용하는 Option과 Result 타입에서 안전하게 데이터를 꺼내는(unwrapping) 하는 방법에 대해 알아봅시다.

대상 데이터 타입

여러 가지 데이터 타입에 대해서도 데이터를 꺼내는 방법을 알아볼 수 있지만 많이 사용하는 Option<T> 타입과 Rersult<T, E> 타입을 예를 들어서 설명합니다.

Option<T> 타입

pub enum Option<T> {
    None,
    Some(T),
}

Option<T> 데이터 타입은 하스켈의 Maybe 데이터 타입과 비슷합니다.

data Maybe a = Just a
             | Nothing
             deriving (Eq, Ord)

Result<T, E> 타입

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Result<T> 데이터 타입은 하스켈의 Either 데이터 타입과 비슷합니다.

data Either a b = Left a
                | Right b
                deriving (Eq, Ord, Read, Show)

데이터를 꺼내는(unwrapping) 방법

하스켈(Haskell)은

하스켈에서 데이터를 꺼내는 전체적인 메커니즘은 패턴 매칭을 바탕으로 하고 있습니다. 그리고 case-of 표현식이나 guard 표현식을 사용할 수도 있습니다.

main :: IO ()
main = do
    print $ getValue $ Just 1
    print $ getValue' $ Just 1
    print $ getValue'' $ Just 1

-- 1. Pattern matching
getValue :: Num a => Maybe a -> a
getValue (Just x) = x
getValue Nothing = 0

-- 2. Case-of expression
getValue' :: Num a => Maybe a -> a
getValue' value = case value of
    Just x -> x
    Nothing -> 0

-- 3. Guard expression
getValue'' :: Num a => Maybe a -> a
getValue'' value | Just x <- value = x
                 | otherwise     a = 0

러스트(Rust)는

러스트에서는 하스켈의 case-of 표현식과 비슷한 match 표현식으로 데이터를 꺼낼 수 있습니다. 뿐만아니라 unwrap(), expect(), ? 연산자, if-let 표현식을 이용하는 방법도 있습니다.

match 표현식

패턴 매칭을 할 수 있는 곳이라면 어디든 사용할 수 있습니다. 타입 Option 이나 Result 에도 패턴 매칭해서 사용할 수 있습니다.

fn main() {
    let mut value: Option<i32> = Some(1);
    match value {
        Some(x) => println!("{}", x),
        None => println!("None"),
    }
    // 1

    value = None;
    match value {
        Some(x) => println!("{}", x),
        None => println!("None"),
    }
    // None
}

unwrap() 메소드

  • 타입 Option 에서 값이 Some 값이거나, Result 에서 값이 Ok 값이면 안에 있는 값을 꺼 내 다음 단계를 진행합니다.

  • 타입 Option 에서 값이 None 값이거나, Result 에서 값이 Err 값이면 프로그램 패닉을 일으킵니다.

fn main() {
    let mut value: Option<i32> = Some(1);
    println!("{}", value.unwrap());
    // 1

    value = None;
    println!("{}", value.unwrap());
    // thread 'main' panicked at 'called `Option::unwrap()` on a `None` value
}

match 패턴 패칭 표현식을 사용하면 다음과 같습니다.

fn main() {
    let mut value: Option<i32> = Some(1);
    match value {
        Some(x) => println!("{}", x),
        None => panic!(),
    }
    // 1

    value = None;
    match value {
        Some(x) => println!("{}", x),
        None => panic!(),
    }
    // thread 'main' panicked at 'explicit panic', ...
}

unwrap_or() 메소드

  • 타입 Option 에서 값이 Some 값이거나, Result 에서 값이 Ok 값이면 안에 있는 값을 꺼 내 다음 단계를 진행합니다.

  • 타입 Option 에서 값이 None 값이거나, Result 에서 값이 Err 값이면 대체할 값으로 정한 값을 리턴합니다.

fn main() {
    let alternative: i32 = 1_000;

    let mut value: Option<i32> = Some(1);
    println!("{}", value.unwrap_or(alternative));
    // 1

    value = None;
    println!("{}", value.unwrap_or(alternative));
    // 1000


    let mut value2: Result<i32, &str> = Ok(1);
    println!("{}", value2.unwrap_or(alternative));
    // 1

    value2 = Err("Error");
    println!("{}", value2.unwrap_or(alternative));
    // 1000
}

unwrap_or_else() 메소드

unwrap_or() 과 비슷합니다. 차이점은 값을 넘겨주는 것 대신 클로저(closure)를 넘겨줍니다.

  • 타입 Option 에서 값이 Some 값이거나, Result 에서 값이 Ok 값이면 안에 있는 값을 꺼 내 다음 단계를 진행합니다.

  • 타입 Option 에서 값이 None 값이거나, Result 에서 값이 Err 값이면 대체할 값을 생성하는 클로저를 넘겨줍니다.

fn main() {
    let mut value: Option<i32> = Some(1);
    println!("{}", value.unwrap_or_else(|| 1_000));
    // 1

    value = None;
    println!("{}", value.unwrap_or(|| 1_000));
    // 1000

    let mut value2: Result<i32, &str> = Ok(1);
    println!("{}", value2.unwrap_or_else(|_| 1_000);
    // 1

    value2 = Err("Error");
    println!("{}", value2.unwrap_or_else(|_| 1_000);
    // 1000
}

unwrap_or_default() 메소드

  • 타입 Option 에서 값이 Some 값이거나, Result 에서 값이 Ok 값이면 안에 있는 값을 꺼 내 다음 단계를 진행합니다.

  • 타입 Option 에서 값이 None 값이거나, Result 에서 값이 Err 값이면 해당 데이터 타입의 기본값을 리턴합니다. (예: i32면 0, char면 '', bool이면 false)

fn main() {
    let mut value: Option<i32> = Some(1);
    println!("{}", value.unwrap_or_default());
    // 1

    value = None;
    println!("{}", value.unwrap_or_default());
    // 0

    let mut value2: Result<bool, &str> = Ok(true);
    println!("{}", value2.unwrap_or_default());
    // true

    value2 = Err("Error");
    println!("{}", value2.unwrap_or_default());
    // false
}

unwrap_err() 메소드

Result<T, E> 타입에만 해당합니다. 그리고 unwrap() 과는 반대로 처리합니다.

  • 타입 Result 에서 값이 Err 값이면 안에 있는 값을 꺼 내 다음 단계를 진행합니다.

  • 타입 Result 에서 값이 Ok 값이면 프로그램 패닉을 일으킵니다.

fn main() {
    let mut value: Result<i32, &str> = Err("Error should be occurred.");
    println!("{}", value.unwrap_err());
    // Error should be occurred.

    value = Ok(1);
    println!("{}", value.unwrap_err());
    // thread 'main' panicked at 'called `Result::unwrap_err()` on an `Ok` value: 1', ...
}

expect()메소드

  • 타입 Option 에서 값이 Some 값이거나, Result 에서 값이 Ok 값이면 안에 있는 값을 꺼 내 다음 단계를 진행합니다.

  • 타입 Option 에서 값이 None 값이거나, Result 에서 값이 Err 값이면 에러 메시지와 함께 프로그램 패닉을 일으킵니다.

fn main() {
    let mut value: Option<i32> = Some(1);
    println!("{}", value.expect("Value should be Some value");
    // 1

    value = None;
    println!("{}", value.expect("Value should be Some value");
    // thread 'main' panicked at 'Value should be Some value', ...
}

match 표현식을 사용하면 다음과 같습니다.

fn main() {
    let mut value: Option<i32> = Some(1);
    match value {
        Some(x) => println!("{}", x),
        None => panic!("Value should be Some value"),
    }
    // 1

    value = None;
    match value {
        Some(x) => println!("{}", x),
        None => panic!("Value should be Some value"),
    }
    // thread 'main' panicked at 'Value should be Some value', ...
}

expect_err() 메소드

Result<T, E> 타입에만 해당합니다. 그리고 expect() 과는 반대로 처리합니다.

  • 타입 Result 에서 값이 Err 값이면 안에 있는 값을 꺼 내 다음 단계를 진행합니다.

  • 타입 Result 에서 값이 Ok 값이면 에러 메시지와 함게 프로그램 패닉을 일으킵니다.

fn main() {
    let mut value: Result<i32, &str> = Err("Error should be occurred.");
    println!("{}", value.expect_err("Should NOT get Ok value"));
    // Error should be occurred.

    value = Ok(1);
    println!("{}", value.expec_err("Should NOT get Ok value"));
    // threa 'main' panicked at 'Should NOT get Ok value 1', ...
}

? 연산자

? 연산자는 Rust 버전 1.13에 try!() 매크로를 대체할 목적으로 추가되었습니다. 그러므로 try!() 매크로를 사용하지 마세요.

  • 타입 Option 에서 값이 Some 값이거나, Result 에서 값이 Ok 값이면 안에 있는 값을 꺼 내 다음 단계를 진행합니다.

  • 타입 Option 에서 값이 None 값이거나, Result 에서 값이 Err 값이면 상위 호출한 구문으로 그 값을 즉시 그대로 반환합니다. 이 때 상위 호출자도 같은 반환 타입을 가져야 합니다.

fn main() {
    println!("{:?}", callee());
    // None

    println!("{:?}", callee2());
    // Error("Error")
}

fn callee() -> Option<i32> {
    let value: Option<i32> = None;
    println!("{}", value?); // return here immediately
    return Some(1);
}

fn callee2() -> Result<i32, &'static str> {
    let value: Result<i32, &'static str> = Err("Error");
    println!("{}", value?); // return here immediately
    return Ok(1);
}

if-let 표현식

  • 타입 Option 에서 값이 Some 값이 없거나(()), Result 에서 값이 Ok 값이 없을 때(()) 사용합니다.

  • 타입 Option 에서 값이 None 값이거나, Result 에서 값이 Err 값이면 패턴 매칭하여 let 에 선언된 바인딩 변수에 넣습니다.

fn main() {
    if let Err(e) = callee() {
        println!("Error: {:?}", e);
    }
}

fn callee() -> Result<(), &'static str> {
    Err("This is an error.")
}

main() 함수에서 에러 전파

Rust 버전 1.26 전에는 타입 Option 이나 Resultmain() 함수의 반환 타입으로 사용할 수 없었습니다. 하지만 그 다음 버전부터 타입 Resultmain 함수의 반환 타입으로 전달할 수 있습니다. 그러면 Err를 디버그 용도로 출력할 수 있습니다.

use std::fs::File;

fn main() -> std::io::Result<()> {
    let _ = File::open("some-file.txt")?;
    // Error: Os {code 2, kind: NotFound, message: "No such file or directory" }

    Ok(())
}

마무리

러스트는 컨테이너형 타입에서 데이터를 꺼내는 다양한 방법을 지원하고 있습니다. 여러 방법 가운데 어느 것이 더 좋다 나쁘다라고 말 할 수는 없겠습니다. 하지만 같은 상황에 같은 방법을 일관되게 적용하면 코드를 이해하기 쉽고 문제 파악도 빨라질 것입니다.