hyunsu blog

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

Rust ? 연산자 — 실패하면 그 자리에서 Err 를 돌려준다

목차 · 4
  1. 01match 로 오류를 넘기면 길다
  2. 02? 한 글자로 줄인다
  3. 03? 는 Result 를 돌려주는 함수에서만
  4. 04실습 — main 이 Result 를 돌려주면

Rust 의 ? 는 Result 뒤에 붙여, Ok 면 안의 값을 꺼내고 Err 면 그 오류를 들고 함수를 바로 끝낸다. match 다섯 줄이 한 글자가 된다. ? 는 Result 를 돌려주는 함수 안에서만 쓸 수 있어서, 보통의 main 에서 쓰면 E0277 이 난다. main 이 Result 를 돌려주게 하면 된다.

01 match 로 오류를 넘기면 길다

문자열 숫자 둘을 받아 더하는 함수다. 둘 중 하나라도 숫자가 아니면 부른 쪽에 그 오류를 그대로 넘기고 싶다. 30편의 match 로 쓰면 이렇다.

rust
use std::num::ParseIntError;

fn add(a: &str, b: &str) -> Result<i32, ParseIntError> {
    let x: i32 = match a.parse() {
        Ok(n) => n,
        Err(e) => return Err(e),
    };
    let y: i32 = match b.parse() {
        Ok(n) => n,
        Err(e) => return Err(e),
    };
    Ok(x + y)
}

fn main() {
    println!("{:?}", add("3", "4"));
    println!("{:?}", add("3", "four"));
}
text
   Compiling total v0.1.0 (~/projects/total)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/total`
Ok(7)
Err(ParseIntError { kind: InvalidDigit })
  • 반환 타입 Result<i32, ParseIntError> 는 "성공하면 i32, 실패하면 ParseIntError" 다. ParseIntError 는 정수 parse 가 내는 오류 타입이고, 맨 위 use 로 불러왔다(29편).
  • parse 마다 match 를 써서, Ok 면 값을 꺼내고 Err 면 return Err(e) 로 함수를 끝낸다.
  • 둘 다 성공하면 마지막 줄 Ok(x + y) 로 성공을 돌려준다.

동작은 맞지만, 실제로 하고 싶은 일(두 수 더하기)보다 오류를 넘기는 줄이 훨씬 길다.

02 ? 한 글자로 줄인다

match 다섯 줄을 ? 로 바꾼다.

rust
use std::num::ParseIntError;

fn add(a: &str, b: &str) -> Result<i32, ParseIntError> {
    let x: i32 = a.parse()?;
    let y: i32 = b.parse()?;
    Ok(x + y)
}

fn main() {
    println!("{:?}", add("3", "4"));
    println!("{:?}", add("3", "four"));
}
text
   Compiling total v0.1.0 (~/projects/total)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/total`
Ok(7)
Err(ParseIntError { kind: InvalidDigit })

결과가 같다. a.parse()? 는 "Ok 면 안의 값을 꺼내라. Err 면 그 Err 를 돌려주며 함수를 지금 끝내라" 는 뜻이다. 위의 match 와 똑같은 일을 한다. 두 번째 호출에서는 "four" 의 parse 가 실패해서, y 줄에서 함수가 끝나고 Err 가 돌아왔다.

03 ? 는 Result 를 돌려주는 함수에서만

보통의 main 에서 ? 를 써 보자.

rust
fn main() {
    let x: i32 = "42".parse()?;
    println!("{x}");
}
text
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
 --> src/main.rs:2:30
  |
1 | fn main() {
  | --------- this function should return `Result` or `Option` to accept `?`
2 |     let x: i32 = "42".parse()?;
  |                              ^ cannot use the `?` operator in a function that returns `()`
  |
help: consider adding return type
  |
1 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
2 |     let x: i32 = "42".parse()?;
3 |     println!("{x}");
4 +     Ok(())
  1. 무엇 — ? 는 Result 나 Option 을 돌려주는 함수에서만 쓸 수 있다.
  2. 어디 — 2번째 줄의 ?. 1번째 줄의 fn main() 도 짚는다. 이 함수는 ()(값 없음, 11편)를 돌려준다.
  3. 고치는 법 — help: 가 main 에 반환 타입을 달고 끝에 Ok(()) 를 넣으라고 한다. ~ 줄이 바뀐 줄, + 줄이 더한 줄이다.

? 는 실패하면 Err 를 돌려주는 것이라, 돌려줄 곳이 Result 여야 한다. help 가 쓴 Box<dyn std::error::Error> 는 "어떤 오류든" 이라는 타입인데, 33편의 트레이트를 배운 뒤에 읽으면 된다. 지금은 오류가 하나뿐이니 ParseIntError 를 그대로 쓴다.

04 실습 — main 이 Result 를 돌려주면

rust
use std::num::ParseIntError;

fn main() -> Result<(), ParseIntError> {
    let x: i32 = "42".parse()?;
    println!("x = {x}");
    let y: i32 = "forty".parse()?;
    println!("y = {y}");
    Ok(())
}
text
   Compiling total v0.1.0 (~/projects/total)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/total`
x = 42
Error: ParseIntError { kind: InvalidDigit }
  • fn main() -> Result<(), ParseIntError> 는 "성공하면 값 없음, 실패하면 ParseIntError" 다. 끝의 Ok(()) 가 "잘 끝났다" 다.
  • "42" 는 성공해서 x = 42 를 찍었다.
  • "forty" 에서 ? 가 Err 를 돌려주며 main 이 끝났다. Rust 가 그 오류를 Error: … 로 찍고, 프로그램은 실패를 뜻하는 종료 코드 1 로 끝난다. 패닉(종료 코드 101)과 다르게, 정해진 방법으로 실패를 알린 것이다.

셸에서 cargo run; echo $? 로 돌리면 마지막에 종료 코드가 찍힌다. 다른 프로그램이나 스크립트가 이 숫자를 보고 성공·실패를 안다. 34편의 실습 프로그램이 이 방식으로 실패를 알린다. 다음 편은 같은 코드를 여러 타입에 쓰는 제네릭이다.