에러 타입 정의하기
때로는 모든 서로 다른 에러들을 하나의 에러 타입으로 마스킹하는 것이 코드를 단순화합니다. 이를 커스텀 에러를 통해 보여주겠습니다.
Rust에서는 우리만의 에러 타입을 정의할 수 있습니다. 일반적으로 “좋은” 에러 타입은 다음과 같습니다:
- 서로 다른 에러들을 동일한 타입으로 표현합니다
- 사용자에게 보기 좋은 에러 메시지를 제공합니다
- 다른 타입들과 비교하기 쉽습니다
- 좋은 예:
Err(EmptyVec) - 나쁜 예:
Err("Please use a vector with at least one element".to_owned())
- 좋은 예:
- 에러에 대한 정보를 담을 수 있습니다
- 좋은 예:
Err(BadChar(c, position)) - 나쁜 예:
Err("+ cannot be used here".to_owned())
- 좋은 예:
- 다른 에러들과 잘 구성(compose)됩니다
use std::fmt;
type Result<T> = std::result::Result<T, DoubleError>;
// 우리의 에러 타입들을 정의합니다. 이들은 우리의 에러 처리 케이스에 맞게 커스터마이징될 수 있습니다.
// 이제 우리만의 에러를 작성하거나, 기저 에러 구현에 위임하거나, 혹은 그 사이의 작업을 수행할 수 있습니다.
#[derive(Debug, Clone)]
struct DoubleError;
// 에러의 생성은 표시 방식과 완전히 분리되어 있습니다.
// 표시 스타일 때문에 복잡한 로직이 어지러워질까 봐 걱정할 필요가 없습니다.
//
// 참고로 우리는 에러에 대한 추가 정보를 저장하지 않습니다. 즉, 정보를 담도록 타입을 수정하지 않고서는
// 어떤 문자열이 파싱에 실패했는지 알 수 없습니다.
impl fmt::Display for DoubleError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "두 배로 만들 첫 번째 항목이 유효하지 않습니다")
}
}
fn double_first(vec: Vec<&str>) -> Result<i32> {
vec.first()
// 에러를 우리의 새로운 타입으로 변경합니다.
.ok_or(DoubleError)
.and_then(|s| {
s.parse::<i32>()
// 여기서도 새로운 에러 타입으로 업데이트합니다.
.map_err(|_| DoubleError)
.map(|i| 2 * i)
})
}
fn print(result: Result<i32>) {
match result {
Ok(n) => println!("두 배가 된 첫 번째 값은 {}입니다", n),
Err(e) => println!("에러: {}", e),
}
}
fn main() {
let numbers = vec!["42", "93", "18"];
let empty = vec![];
let strings = vec!["두부", "93", "18"];
print(double_first(numbers));
print(double_first(empty));
print(double_first(strings));
}