English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Rust 오류 처리

Rust는 독특한 예외 처리 메커니즘을 가지고 있으며, 다른 언어의 try 메커니즘과는 간단하지 않습니다.

프로그램에서 일반적으로 발생하는 두 가지 오류는 가능성 오류와 불가恢복 오류입니다.

가능성 오류의 대표적인 예는 파일 접근 오류입니다. 파일 접근이 실패하면, 그 이유가 파일이 사용 중인 것일 수 있으며, 이는 정상적인 현상입니다. 우리는 기다리는 방법으로 이를 해결할 수 있습니다.

또한, 프로그래밍에서 해결할 수 없는 논리적 오류로 인해 발생하는 오류도 있습니다. 예를 들어, 배열의 마지막 위치 이후에 접근하는 경우가 그 예입니다.

대부분의 프로그래밍 언어는 이 두 가지 오류를 구분하지 않으며, Exception (예외) 클래스를 통해 오류를 표현합니다. Rust에서는 Exception이 없습니다.

가능성 오류는 Result<T, E> 클래스를 사용하여 처리하며, 불가恢복 오류는 panic! 매크로를 사용하여 처리합니다.

불가恢복 오류

이 장에서는 Rust 매크로의 문법에 특별히 다루지 않았지만, println! 매크로는 사용되었습니다. 이 매크로의 사용은 간단하므로, 현재는 그를 완전히 이해할 필요가 없습니다. 우리는 같은 방법으로 panic! 매크로의 사용법을 먼저 배울 수 있습니다.

fn main() {
    panic!("error occurred");
    println!("Hello, Rust");
}

실행 결과:

thread 'main' panicked at 'error occured', src\main.rs:3:5
주의: `RUST_BACKTRACE=`를 통해 실행하세요.1`environment variable to display a backtrace.

물론, 프로그램은 println!("Hello, Rust")에 이르기까지 원래 계획대로 실행되지 않고, panic! 매크로가 호출될 때 실행이 중단됩니다.

불가恢복 오류는 프로그램이 치명적인 타격을 받고 실행을 중단하게 됩니다.

오류 출력의 두 행을 주목해 보겠습니다:

  • 첫 번째 행은 panic! 매크로가 호출된 위치와 그 출력 오류 정보를 출력합니다.

  • 제2행은 주의사항이며, 중국어로는 "`RUST_BACKTRACE=`를 통해 백트레이스를 표시하는 환경 변수입니다."로 번역됩니다.1` 环境变量运行以显示回溯"。接下来我们将介绍回溯(backtrace)。

紧接着刚才的实例,我们在 VSCode 中新建一个终端:

在新建的终端里设置环境变量(不同的终端方法不同,这里介绍两种主要的方法):

如果在 Windows 7 及以上的 Windows 系统版本中,默认使用的终端命令行是 Powershell,请使用以下命令:

$env:RUST_BACKTRACE=1 ; cargo run

如果你使用的是 Linux 或 macOS 等 UNIX 系统,一般情况下默认使用的是 bash 命令行,请使用以下命令:

RUST_BACKTRACE=1 cargo run

然后,你会看到以下文字:

thread 'main' panicked at 'error occured', src\main.rs:3:5
stack backtrace:
  ...
  11: greeting::main
             at .\src\main.rs:3
  ...

回溯是不可恢复错误的另一种处理方式,它会展开运行的栈并输出所有的信息,然后程序依然会退出。上面的省略号省略了大量的输出信息,我们可以找到我们编写的 panic! 宏触发的错误。

구현 가능한 오류

이 개념은 Java 프로그래밍 언어의 예외와 매우 유사합니다. 실제로 C 언어에서는 함수의 반환 값을 정수로 설정하여 함수에서 발생한 오류를 표현하는 것이 일반적입니다. Rust에서는 Result<T, E> 열거형으로 반환 값을 사용하여 예외를 표현합니다:

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

Rust 표준 라이브러리에서 예외를 일으킬 수 있는 함수의 반환 값은 모두 Result 타입입니다. 예를 들어: 파일을 열 때:

use std::fs::File;
fn main() {
    let f = File::open("hello.txt");
    match f {
        Ok(file) => {
            println!("파일 열기 성공.");
        },
        Err(err) => {
            println!("파일 열기 실패.");
        }
    }
}

만약 "hello.txt" 파일이 존재하지 않으면, 다음과 같은 텍스트가 출력됩니다: "파일 열기 실패."

물론, 우리가 열거형 클래스 장에서 설명한 if let 문법은 match 문법 블록을 간소화할 수 있습니다:

use std::fs::File;
fn main() {
    let f = File::open("hello.txt");
    if let Ok(file) = f {
        println!("파일 열기 성공.");
    } else {
        println!("파일 열기 실패.");
    }
}

불변 오류를 불변 오류로 처리하려면, Result 클래스는 unwrap()과 expect(message: &str) 두 가지 방법을 제공합니다:

use std::fs::File;
fn main() {
    let f1 = File::open("hello.txt").unwrap();
    let f2 = File::open("hello.txt").expect("Failed to open.");
}

이 프로그램은 Result가 Err이면 panic! 매크로를 호출하는 것과 동일합니다. 두 가지의 차이점은 expect가 panic! 매크로에 특정 오류 메시지를 보낼 수 있다는 것입니다.

불변 오류 전달

이전에 설명한 것은 오류를 수신하여 처리하는 방법입니다. 하지만 우리가 오류가 발생했을 때 전달하고 싶은 함수를 작성하려면 어떻게 해야 하나요?

fn f(i: i32) -> Result<i32, bool> {
    if i >= 0 { Ok(i) }
    else { Err(false) }
}
fn main() {
    let r = f(10000);
    if let Ok(v) = r {
        println!("Ok: f(-1) = {}", v);
    } else {
        println!("Err");
    }
}

실행 결과:

Ok: f(-1) = 10000

이 프로그램에서 함수 f이 오류의 원천입니다. 지금은 오류를 전달하는 함수 g을 다시 작성해 보겠습니다:

fn g(i: i32) -> Result<i32, bool> {
    let t = f(i);
    return match t {
        Ok(i) => Ok(i),
        Err(b) => Err(b)
    ;
}

함수 g은 함수 f이 발생할 수 있는 오류를 전달합니다. 여기서 g은 단순한 예시일 뿐, 실제로 오류를 전달하는 함수는 많은 다른 작업을 포함할 수 있습니다.

이렇게 쓰면 조금 길어 보일 수 있습니다. Rust에서는 Result 객체 뒤에 ? 연산자를 추가하여 같은 품질의 Err을 직접 전달할 수 있습니다:

fn f(i: i32) -> Result<i32, bool> {
    if i >= 0 { Ok(i) }
    else { Err(false) }
}
fn g(i: i32) -> Result<i32, bool> {
    let t = f(i)?;
    Ok(t) // t가 Err가 아니라는 것을 확정하면, t는 여기서 이미 i입니다.32 타입
}
fn main() {
    let r = g(10000);
    if let Ok(v) = r {
        println!("Ok: g(10000) = {}", v);
    } else {
        println!("Err");
    }
}

실행 결과:

Ok: g(10000) = 10000

문자 ?의 실제 작용은 Result 클래스의 비 예외 값을 직접 꺼내고, 예외가 있으면 예외 Result을 반환하는 것입니다. 따라서, ? 문자는 Result<T, E> 타입의 값을 반환하는 함수에만 사용되며, E 타입은 ?가 처리하는 Result의 E 타입과 일치해야 합니다.

kind 메서드

이제까지 Rust는 try 블록과 같은 모든 위치에서 발생하는 동일한 예외를 해결할 수 있는 문법이 없어 보이지만, 이는 Rust가 이를 구현할 수 없다는 의미는 아닙니다. 우리는 try 블록을 독립된 함수에서 구현하고 모든 예외를 해결할 수 있습니다. 실제로 이는 프로그램이 잘 분화된 프로그래밍 방법을 따르는 것이며, 독립적인 기능의 완전성을 중시해야 합니다.

하지만 이렇게 하면 Result의 Err 타입을 판단해야 하며, Err 타입을 가져오는 함수는 kind()입니다.

use std::io;
use std::io::Read;
use std::fs::File;
fn read_text_from_file(path: &str) -> Result<String, io::Error> {
    let mut f = File::open(path)?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}
fn main() {
    let str_file = read_text_from_file("hello.txt");
    match str_file {
        Ok(s) => println!("{}", s),
        Err(e) => {
            match e.kind() {
                io::ErrorKind::NotFound => {
                    println!("No such file");
                },
                _ => {
                    println!("Cannot read the file");
                }
            }
        }
    }
}

실행 결과:

해당 파일 없음