예외 처리
.NET에서 예외는 System.Exception 클래스를
상속한 타입입니다. 코드 실행 중 문제가 생기면 예외를 던집니다.
프로그램이 예외를 처리하거나 종료할 때까지 예외는 호출 스택을 따라
전파됩니다.
Rust에는 예외가 없습니다. 대신 복구 가능한 오류와 복구할 수 없는
오류를 구분합니다. 복구 가능한 오류는 문제를 보고한 뒤 프로그램을
계속 실행할 수 있는 경우를 뜻합니다. 이런 오류가 발생할 수 있는
연산은 Result<T, E> 타입을 반환합니다. 여기서
E는 오류 변형의 타입입니다. 복구할 수 없는 오류를 만나면
panic! 매크로가 실행을 중단합니다. 복구할 수 없는 오류는
항상 버그의 징후입니다.
사용자 정의 오류 타입
.NET의 사용자 정의 예외는 Exception 클래스를 상속합니다.
사용자 정의 예외 작성 방법 문서에는
다음 예제가 있습니다.
public class EmployeeListNotFoundException : Exception
{
public EmployeeListNotFoundException() { }
public EmployeeListNotFoundException(string message)
: base(message) { }
public EmployeeListNotFoundException(string message, Exception inner)
: base(message, inner) { }
}
Rust에서는 Error 트레이트를 구현해 오류 값에
일반적으로 기대하는 기능을 제공할 수 있습니다. 최소한의 사용자
정의 오류 구현은 다음과 같습니다.
#[derive(Debug)]
pub struct EmployeeListNotFound;
impl std::fmt::Display for EmployeeListNotFound {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Could not find employee list.")
}
}
impl std::error::Error for EmployeeListNotFound {}
.NET의 Exception.InnerException 속성에 대응하는 기능은 Rust의
Error::source() 메서드입니다. 다만 이 메서드를 반드시 구현할
필요는 없습니다. 기본 일괄 구현은 None을 반환합니다.
C#과 달리 Rust의 오류 타입이 반드시
std::error::Error를 구현해야 하는 것은 아닙니다. 이 트레이트 없이도Result에서 사용할 수 있습니다. 공개 API의 오류 타입이라면 구현하는 관례가 있습니다.
오류 발생시키기
C#에서는 예외 인스턴스를 던져 예외를 발생시킵니다.
void ThrowIfNegative(int value)
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
}
Rust에서는 복구 가능한 오류가 발생하면 메서드에서 Ok 또는
Err 변형을 반환합니다.
fn error_if_negative(value: i32) -> Result<(), &'static str> {
if value < 0 {
Err("Specified argument was out of the range of valid values. (Parameter 'value')")
} else {
Ok(())
}
}
panic! 매크로는 복구할 수 없는 오류를 발생시킵니다.
fn panic_if_negative(value: i32) {
if value < 0 {
panic!("Specified argument was out of the range of valid values. (Parameter 'value')")
}
}
오류 전파
.NET에서는 예외를 처리하거나 프로그램이 종료될 때까지 예외가 호출 스택을 따라 전파됩니다. Rust에서 복구할 수 없는 오류도 비슷하게 동작하지만 이를 처리하는 일은 드뭅니다.
복구 가능한 오류는 명시적으로 전파하고 처리합니다. Rust의 함수나 메서드 시그니처는 이런 오류가 발생할 수 있음을 나타냅니다. C#에서는 예외를 잡아 오류 발생 여부에 따라 다음 동작을 선택할 수 있습니다.
void Write()
{
try
{
File.WriteAllText("file.txt", "content");
}
catch (IOException)
{
Console.WriteLine("Writing to file failed.");
}
}
Rust에서는 대략 다음 코드에 대응합니다.
fn write() {
match std::fs::write("file.txt", b"content")
{
Ok(_) => {}
Err(_) => println!("Writing to file failed."),
};
}
복구 가능한 오류를 직접 처리하지 않고 전파하기만 하는 경우도 많습니다.
이때 메서드 시그니처의 오류 타입이 전파할 오류와 호환되어야 합니다.
? 연산자를 사용하면 간결하게 전파할 수
있습니다.
fn write() -> Result<(), std::io::Error> {
std::fs::write("file.txt", b"content")?;
Ok(())
}
? 연산자로 오류를 전파하려면 오류 전파의 간단한 방법
절에서 설명하는 것처럼 오류 구현이 서로 _호환_되어야 합니다. 가장
범용적인 호환 오류 타입은 Box<dyn Error> 트레이트 객체입니다.
스택 추적
.NET에서 처리되지 않은 예외를 던지면 런타임이 스택 추적을 출력해 문제의 맥락을 파악하도록 돕습니다.
Rust에서 복구할 수 없는 오류는 panic!의 역추적으로
비슷한 정보를 얻을 수 있습니다.
원문은 안정화된 Rust의 복구 가능한 오류에서 역추적을 지원하지
않는다고 설명합니다. 실험 기능의 provide 메서드를
사용하는 경우를 별도로 언급합니다.