hyunsu blog

홈강좌Rust 입문·중급 47편 중 30편째

Rust panic! 과 Result — 멈출 실패와 다룰 실패를 나눈다

목차 · 4
  1. 01panic! — 여기서 멈춘다
  2. 02Result — 성공이면 Ok, 실패면 Err
  3. 03Result 는 값 그대로 쓸 수 없다
  4. 04실습 — match 로 다루고, unwrap 은 조심

Rust 는 실패를 둘로 나눈다. 고칠 수 없는 실패는 panic! 으로 프로그램을 멈추고, 다룰 수 있는 실패는 Ok(값) 이나 Err(오류) 인 Result 로 돌려준다. 문자열을 숫자로 바꾸는 parse 가 Result 를 돌려주는 대표적인 예다. Result 를 값 자리에 그대로 쓰면 E0308 이 난다.

01 panic! — 여기서 멈춘다

지금까지 범위 밖 번호(28편)나 None 의 unwrap(26편)에서 패닉을 봤다. 직접 멈출 수도 있다. 재고보다 많이 꺼내려 하면 멈추는 함수다.

rust
fn take(stock: u32, n: u32) -> u32 {
    if n > stock {
        panic!("not enough stock: have {stock}, asked {n}");
    }
    stock - n
}

fn main() {
    println!("{}", take(5, 3));
    println!("{}", take(5, 9));
}
text
   Compiling stock v0.1.0 (~/projects/stock)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/stock`
2

thread 'main' (213128086) panicked at src/main.rs:3:9:
not enough stock: have 5, asked 9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

첫 take(5, 3) 은 2 를 찍었고, 두 번째에서 panic! 이 프로그램을 멈췄다. 둘째 줄이 멈춘 자리(3번째 줄 9번째 글자), 셋째 줄이 panic! 에 넣은 문장이다. panic! 의 괄호는 println! 처럼 {stock} 자리를 채운다.

panic! 은 "이 일이 일어났다면 코드가 틀린 것이다" 일 때 쓴다. 사용자가 잘못 입력한 것처럼 일어날 수 있는 실패에 쓰면, 입력 하나 틀렸다고 프로그램 전체가 죽는다. 그런 실패는 Result 로 돌려준다.

02 Result — 성공이면 Ok, 실패면 Err

Result 는 26편의 Option 과 닮은 열거형이다. Option 이 "있다(Some)·없다(None)" 라면 Result 는 "성공(Ok)·실패(Err)" 이고, 실패에는 이유가 붙는다. 문자열을 숫자로 바꾸는 parse 가 Result 를 돌려준다.

rust
fn main() {
    let good: Result<u8, _> = "30".parse();
    let bad: Result<u8, _> = "thirty".parse();
    let big: Result<u8, _> = "300".parse();

    println!("{good:?}");
    println!("{bad:?}");
    println!("{big:?}");
}
text
   Compiling age v0.1.0 (~/projects/age)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/age`
Ok(30)
Err(ParseIntError { kind: InvalidDigit })
Err(ParseIntError { kind: PosOverflow })
  • "30" 은 u8 로 바뀌어 Ok(30) 이다.
  • "thirty" 는 숫자가 아니라서 Err. 이유는 InvalidDigit(숫자가 아닌 글자)다.
  • "300" 은 숫자지만 u8(0~255, 5편)을 넘어서 Err. 이유는 PosOverflow(너무 크다)다.

Result<u8, _> 는 "성공하면 u8, 실패하면 어떤 오류" 다. _ 는 오류 타입을 컴파일러가 알아서 채우라는 뜻이다. parse 는 무엇으로 바꿀지 모르니, 이렇게 타입을 적어 알려 준다.

03 Result 는 값 그대로 쓸 수 없다

Option 과 마찬가지로 Result 는 안의 값이 아니다.

rust
fn main() {
    let age: u8 = "30".parse();
    println!("{age}");
}
text
error[E0308]: mismatched types
 --> src/main.rs:2:19
  |
2 |     let age: u8 = "30".parse();
  |              --   ^^^^^^^^^^^^ expected `u8`, found `Result<_, _>`
  |              |
  |              expected due to this
  |
  = note: expected type `u8`
             found enum `Result<_, _>`
help: consider using `Result::expect` to unwrap the `Result<_, _>` value, panicking if the value is a `Result::Err`
  |
2 |     let age: u8 = "30".parse().expect("REASON");
  1. 무엇 — 타입이 안 맞는다. u8 이 와야 하는데 Result<_, _> 가 왔다.
  2. 어디 — 2번째 줄의 "30".parse().
  3. 고치는 법 — help: 가 .expect("REASON") 으로 꺼내라고 한다. Err 면 패닉한다.

실패할 수 있다는 것을 무시하고 지나갈 수 없게 만드는 장치다. 다른 언어에서 예외를 잡지 않고 지나치는 일이 Rust 에서는 컴파일 오류가 된다.

04 실습 — match 로 다루고, unwrap 은 조심

Result 도 열거형이니 25편의 match 로 나눈다.

rust
fn main() {
    for input in ["30", "thirty"] {
        let parsed: Result<u8, _> = input.parse();
        match parsed {
            Ok(age) => println!("{input}: age {age}"),
            Err(e) => println!("{input}: error: {e}"),
        }
    }
}
text
   Compiling age v0.1.0 (~/projects/age)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/age`
30: age 30
thirty: error: invalid digit found in string

Ok(age) 갈래는 값을 꺼내 쓰고, Err(e) 갈래는 오류를 꺼내 사람이 읽는 문장으로 찍는다. {e} 처럼 :? 없이 찍으면 invalid digit found in string 같은 설명이 나온다. 프로그램은 멈추지 않고 다음 입력으로 넘어갔다.

unwrap() 으로 꺼내면 짧지만, Err 면 멈춘다.

rust
fn main() {
    let age: u8 = "thirty".parse().unwrap();
    println!("{age}");
}
text
   Compiling age v0.1.0 (~/projects/age)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/age`

thread 'main' (213128635) panicked at src/main.rs:2:36:
called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

26편의 Option::unwrap 과 같다. 셋째 줄에 Err 의 이유까지 찍힌다. 실패하지 않는다고 확신할 때만 쓴다. 함수 안에서 Result 를 하나하나 match 하면 길어지는데, 이것을 한 글자로 줄이는 것이 다음 편의 ? 다.