소개
Recoverable Errors With Result에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.
이 랩에서는 Rust 에서 Result 열거형을 사용하여 복구 가능한 오류를 처리하는 방법을 배웁니다. 이를 통해 프로그램을 종료하지 않고 오류를 해석하고 대응할 수 있습니다.
Recoverable Errors With Result에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.
이 랩에서는 Rust 에서 Result 열거형을 사용하여 복구 가능한 오류를 처리하는 방법을 배웁니다. 이를 통해 프로그램을 종료하지 않고 오류를 해석하고 대응할 수 있습니다.
대부분의 오류는 프로그램이 완전히 중단될 정도로 심각하지 않습니다. 때로는 함수가 실패하는 이유가 쉽게 해석하고 대응할 수 있는 경우도 있습니다. 예를 들어, 파일을 열려고 시도했는데 파일이 존재하지 않아 해당 작업이 실패하면 프로세스를 종료하는 대신 파일을 생성할 수 있습니다.
"Result 를 사용한 잠재적 실패 처리"에서 기억하듯이, Result 열거형은 다음과 같이 Ok와 Err의 두 가지 변형을 갖도록 정의됩니다.
enum Result<T, E> {
Ok(T),
Err(E),
}
T와 E는 제네릭 타입 매개변수입니다. 제네릭에 대해서는 10 장에서 자세히 논의할 것입니다. 지금 알아야 할 것은 T가 Ok 변형 내에서 성공 사례에서 반환될 값의 타입을 나타내고, E가 Err 변형 내에서 실패 사례에서 반환될 오류의 타입을 나타낸다는 것입니다. Result는 이러한 제네릭 타입 매개변수를 가지므로, 반환하려는 성공 값과 오류 값이 다를 수 있는 다양한 상황에서 Result 타입과 해당 타입에 정의된 함수를 사용할 수 있습니다.
함수가 실패할 수 있으므로 Result 값을 반환하는 함수를 호출해 보겠습니다. Listing 9-3 에서는 파일을 열려고 시도합니다.
파일 이름: src/main.rs
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
}
Listing 9-3: 파일 열기
File::open의 반환 타입은 Result<T, E>입니다. 제네릭 매개변수 T는 성공 값의 타입인 std::fs::File로 File::open의 구현에 의해 채워졌습니다. 이는 파일 핸들입니다. 오류 값에 사용되는 E의 타입은 std::io::Error입니다. 이 반환 타입은 File::open 호출이 성공하여 읽거나 쓸 수 있는 파일 핸들을 반환할 수 있음을 의미합니다. 또한 함수 호출이 실패할 수도 있습니다. 예를 들어, 파일이 존재하지 않거나 파일에 액세스할 권한이 없을 수 있습니다. File::open 함수는 성공했는지 실패했는지 알려주는 동시에 파일 핸들 또는 오류 정보를 제공할 수 있어야 합니다. 이 정보가 바로 Result 열거형이 전달하는 것입니다.
File::open이 성공하는 경우, 변수 greeting_file_result의 값은 파일 핸들을 포함하는 Ok의 인스턴스가 됩니다. 실패하는 경우, greeting_file_result의 값은 발생한 오류 종류에 대한 추가 정보를 포함하는 Err의 인스턴스가 됩니다.
Listing 9-3 의 코드에 File::open이 반환하는 값에 따라 다른 작업을 수행하도록 추가해야 합니다. Listing 9-4 는 6 장에서 논의한 기본 도구인 match 표현식을 사용하여 Result를 처리하는 한 가지 방법을 보여줍니다.
파일 이름: src/main.rs
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => {
panic!("Problem opening the file: {:?}", error);
}
};
}
Listing 9-4: 반환될 수 있는 Result 변형을 처리하기 위해 match 표현식 사용
Option 열거형과 마찬가지로, Result 열거형과 해당 변형은 prelude 에 의해 범위 내로 가져왔으므로, match arm 에서 Ok와 Err 변형 앞에 Result::를 지정할 필요가 없습니다.
결과가 Ok인 경우, 이 코드는 Ok 변형에서 내부 file 값을 반환하고, 해당 파일 핸들 값을 변수 greeting_file에 할당합니다. match 이후에는 파일 핸들을 사용하여 읽거나 쓸 수 있습니다.
match의 다른 arm 은 File::open에서 Err 값을 얻는 경우를 처리합니다. 이 예제에서는 panic! 매크로를 호출하도록 선택했습니다. 현재 디렉토리에 hello.txt라는 파일이 없고 이 코드를 실행하면 panic! 매크로에서 다음 출력을 볼 수 있습니다.
thread 'main' panicked at 'Problem opening the file: Os { code:
2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:8:23
평소와 같이, 이 출력은 정확히 무엇이 잘못되었는지 알려줍니다.
Listing 9-4 의 코드는 File::open이 실패한 이유에 관계없이 panic!을 발생시킵니다. 그러나 서로 다른 실패 이유에 대해 서로 다른 작업을 수행하려고 합니다. File::open이 파일이 존재하지 않아서 실패한 경우, 파일을 생성하고 새 파일에 대한 핸들을 반환하려고 합니다. File::open이 다른 이유로 실패한 경우 (예: 파일을 열 권한이 없는 경우) 에도 Listing 9-4 에서와 마찬가지로 코드가 panic!을 발생시키도록 하려고 합니다. 이를 위해 Listing 9-5 에 표시된 내부 match 표현식을 추가합니다.
파일 이름: src/main.rs
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => {
match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!(
"Problem creating the file: {:?}",
e
),
}
}
other_error => {
panic!(
"Problem opening the file: {:?}",
other_error
);
}
},
};
}
Listing 9-5: 서로 다른 방식으로 서로 다른 종류의 오류 처리
File::open이 Err 변형 내에서 반환하는 값의 타입은 표준 라이브러리에서 제공하는 구조체인 io::Error입니다. 이 구조체에는 io::ErrorKind 값을 얻기 위해 호출할 수 있는 kind 메서드가 있습니다. io::ErrorKind 열거형은 표준 라이브러리에서 제공되며, io 작업으로 인해 발생할 수 있는 다양한 종류의 오류를 나타내는 변형을 갖습니다. 우리가 사용하려는 변형은 ErrorKind::NotFound로, 열려고 하는 파일이 아직 존재하지 않음을 나타냅니다. 따라서 greeting_file_result에 대해 매칭하지만, error.kind()에 대한 내부 매칭도 있습니다.
내부 match 에서 확인하려는 조건은 error.kind()에서 반환된 값이 ErrorKind 열거형의 NotFound 변형인지 여부입니다. 그렇다면 File::create로 파일을 생성하려고 시도합니다. 그러나 File::create도 실패할 수 있으므로 내부 match 표현식에 두 번째 arm 이 필요합니다. 파일을 생성할 수 없는 경우 다른 오류 메시지가 인쇄됩니다. 외부 match의 두 번째 arm 은 동일하게 유지되므로, 프로그램은 파일 누락 오류 외의 모든 오류에 대해 패닉을 발생시킵니다.
match가 많네요! match 표현식은 매우 유용하지만, 또한 매우 기본적인 것입니다. 13 장에서 클로저 (closure) 에 대해 배우게 될 것입니다. 클로저는 Result<T, E>에 정의된 많은 메서드와 함께 사용됩니다. 이러한 메서드는 코드에서 Result<T, E> 값을 처리할 때 match를 사용하는 것보다 더 간결할 수 있습니다.
예를 들어, Listing 9-5 에 표시된 것과 동일한 로직을 작성하는 또 다른 방법은 클로저와 unwrap_or_else 메서드를 사용하는 것입니다.
// src/main.rs
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {:?}", error);
})
} else {
panic!("Problem opening the file: {:?}", error);
}
});
}
이 코드는 Listing 9-5 와 동일한 동작을 하지만, match 표현식을 포함하지 않고 읽기 더 쉽습니다. 13 장을 읽은 후 이 예제로 돌아와서 표준 라이브러리 문서에서 unwrap_or_else 메서드를 찾아보세요. 오류를 처리할 때 이러한 메서드 중 많은 수가 거대한 중첩된 match 표현식을 정리할 수 있습니다.
match를 사용하는 것은 충분히 잘 작동하지만, 약간 장황할 수 있으며 항상 의도를 잘 전달하지는 않습니다. Result<T, E> 타입에는 다양한, 더 구체적인 작업을 수행하기 위해 정의된 많은 헬퍼 메서드가 있습니다. unwrap 메서드는 Listing 9-4 에서 작성한 match 표현식과 유사하게 구현된 단축 메서드입니다. Result 값이 Ok 변형인 경우, unwrap은 Ok 내부의 값을 반환합니다. Result가 Err 변형인 경우, unwrap은 우리를 위해 panic! 매크로를 호출합니다. 다음은 unwrap의 작동 예시입니다.
파일 이름: src/main.rs
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
hello.txt 파일 없이 이 코드를 실행하면, unwrap 메서드가 만드는 panic! 호출에서 오류 메시지가 표시됩니다.
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os {
code: 2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:4:49
마찬가지로, expect 메서드를 사용하면 panic! 오류 메시지도 선택할 수 있습니다. unwrap 대신 expect를 사용하고 좋은 오류 메시지를 제공하면 의도를 전달하고 패닉의 원인을 더 쉽게 추적할 수 있습니다. expect의 구문은 다음과 같습니다.
파일 이름: src/main.rs
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
unwrap과 동일한 방식으로 expect를 사용하여 파일 핸들을 반환하거나 panic! 매크로를 호출합니다. expect가 panic!을 호출할 때 사용되는 오류 메시지는 unwrap이 사용하는 기본 panic! 메시지가 아닌, expect에 전달하는 매개변수가 됩니다. 다음과 같습니다.
thread 'main' panicked at 'hello.txt should be included in this project: Os {
code: 2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:5:10
프로덕션 품질의 코드에서 대부분의 Rust 개발자는 unwrap 대신 expect를 선택하고, 작업이 항상 성공할 것으로 예상되는 이유에 대한 더 많은 컨텍스트를 제공합니다. 그렇게 하면, 가정이 틀린 것으로 판명될 경우, 디버깅에 사용할 수 있는 더 많은 정보를 얻을 수 있습니다.
함수의 구현이 실패할 수 있는 무언가를 호출할 때, 함수 자체 내에서 오류를 처리하는 대신, 오류를 호출하는 코드에 반환하여 무엇을 할지 결정하도록 할 수 있습니다. 이것을 오류를 전파하는 것이라고 하며, 코드의 컨텍스트에서 사용할 수 있는 것보다 오류를 처리하는 방법에 대한 더 많은 정보나 로직이 있을 수 있는 호출하는 코드에 더 많은 제어를 제공합니다.
예를 들어, Listing 9-6 은 파일에서 사용자 이름을 읽는 함수를 보여줍니다. 파일이 존재하지 않거나 읽을 수 없는 경우, 이 함수는 해당 오류를 함수를 호출한 코드에 반환합니다.
파일 이름: src/main.rs
use std::fs::File;
use std::io::{self, Read};
1 fn read_username_from_file() -> Result<String, io::Error> {
2 let username_file_result = File::open("hello.txt");
3 let mut username_file = match username_file_result {
4 Ok(file) => file,
5 Err(e) => return Err(e),
};
6 let mut username = String::new();
7 match username_file.read_to_string(&mut username) {
8 Ok(_) => Ok(username),
9 Err(e) => Err(e),
}
}
Listing 9-6: match를 사용하여 오류를 호출하는 코드에 반환하는 함수
이 함수는 훨씬 더 짧은 방식으로 작성할 수 있지만, 오류 처리를 탐구하기 위해 많은 부분을 수동으로 시작할 것입니다. 마지막에 더 짧은 방법을 보여드리겠습니다. 먼저 함수의 반환 타입을 살펴보겠습니다: Result<String, io::Error> [1]. 이것은 함수가 Result<T, E> 타입의 값을 반환한다는 것을 의미하며, 여기서 제네릭 매개변수 T는 구체적인 타입 String으로 채워지고, 제네릭 타입 E는 구체적인 타입 io::Error로 채워졌습니다.
이 함수가 문제 없이 성공하면, 이 함수를 호출하는 코드는 String을 포함하는 Ok 값을 받게 됩니다. 즉, 이 함수가 파일에서 읽은 username [8]입니다. 이 함수가 어떤 문제에 직면하면, 호출하는 코드는 문제에 대한 더 많은 정보를 포함하는 io::Error의 인스턴스를 포함하는 Err 값을 받게 됩니다. 이 함수의 반환 타입으로 io::Error를 선택한 이유는 이 함수 본문에서 실패할 수 있는 두 가지 연산, 즉 File::open 함수 [2]와 read_to_string 메서드 [7]에서 반환되는 오류 값의 타입이기 때문입니다.
함수의 본문은 File::open 함수 [2]를 호출하는 것으로 시작합니다. 그런 다음 Listing 9-4 의 match와 유사한 match로 Result 값을 처리합니다. File::open이 성공하면, 패턴 변수 file [4]의 파일 핸들이 가변 변수 username_file [3]의 값이 되고 함수는 계속됩니다. Err의 경우, panic!을 호출하는 대신, return 키워드를 사용하여 함수에서 조기에 반환하고, File::open에서 가져온 오류 값 (이제 패턴 변수 e에 있음) 을 이 함수의 오류 값으로 호출하는 코드에 다시 전달합니다 [5].
따라서 username_file에 파일 핸들이 있는 경우, 함수는 변수 username [6]에 새로운 String을 생성하고, username_file의 파일 핸들에 대해 read_to_string 메서드를 호출하여 파일의 내용을 username [7]에 읽어들입니다. read_to_string 메서드도 Result를 반환하는데, File::open이 성공했음에도 불구하고 실패할 수 있기 때문입니다. 따라서 해당 Result를 처리하기 위해 다른 match가 필요합니다. read_to_string이 성공하면, 우리 함수가 성공한 것이고, 파일에서 가져온 사용자 이름 (이제 username에 있음) 을 Ok로 래핑하여 반환합니다. read_to_string이 실패하면, File::open의 반환 값을 처리한 match에서 오류 값을 반환한 것과 동일한 방식으로 오류 값을 반환합니다. 그러나 이것이 함수의 마지막 표현식이므로 [9] 명시적으로 return을 말할 필요는 없습니다.
이 코드를 호출하는 코드는 사용자 이름을 포함하는 Ok 값 또는 io::Error를 포함하는 Err 값을 받게 됩니다. 이러한 값으로 무엇을 할지는 호출하는 코드에 달려 있습니다. 호출하는 코드가 Err 값을 받으면, panic!을 호출하여 프로그램을 충돌시키거나, 기본 사용자 이름을 사용하거나, 예를 들어 파일이 아닌 다른 곳에서 사용자 이름을 찾아볼 수 있습니다. 호출하는 코드가 실제로 무엇을 하려고 하는지에 대한 충분한 정보가 없으므로, 모든 성공 또는 오류 정보를 적절하게 처리할 수 있도록 위로 전파합니다.
이러한 오류 전파 패턴은 Rust 에서 매우 일반적이므로 Rust 는 이를 더 쉽게 만들기 위해 물음표 연산자 ?를 제공합니다.
Listing 9-7 은 Listing 9-6 과 동일한 기능을 가진 read_username_from_file의 구현을 보여주지만, 이 구현은 ? 연산자를 사용합니다.
파일 이름: src/main.rs
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
Listing 9-7: ? 연산자를 사용하여 오류를 호출하는 코드에 반환하는 함수
Result 값 뒤에 배치된 ?는 Listing 9-6 에서 Result 값을 처리하기 위해 정의한 match 표현식과 거의 동일한 방식으로 작동하도록 정의되어 있습니다. Result의 값이 Ok이면, Ok 내부의 값이 이 표현식에서 반환되고 프로그램은 계속됩니다. 값이 Err이면, Err은 전체 함수에서 return 키워드를 사용한 것처럼 반환되므로 오류 값이 호출하는 코드로 전파됩니다.
Listing 9-6 의 match 표현식과 ? 연산자가 하는 일 사이에는 차이점이 있습니다. ? 연산자가 호출된 오류 값은 표준 라이브러리의 From 트레이트에서 정의된 from 함수를 거칩니다. 이 함수는 한 타입의 값을 다른 타입으로 변환하는 데 사용됩니다. ? 연산자가 from 함수를 호출하면, 수신된 오류 타입은 현재 함수의 반환 타입에 정의된 오류 타입으로 변환됩니다. 이는 함수가 여러 가지 이유로 실패할 수 있음에도 불구하고, 함수가 실패할 수 있는 모든 방식을 나타내기 위해 하나의 오류 타입을 반환할 때 유용합니다.
예를 들어, Listing 9-7 의 read_username_from_file 함수를 우리가 정의한 OurError라는 사용자 정의 오류 타입을 반환하도록 변경할 수 있습니다. 또한 impl From<io::Error> for OurError를 정의하여 io::Error에서 OurError의 인스턴스를 생성하면, read_username_from_file 본문에서 ? 연산자 호출은 from을 호출하고 함수에 더 많은 코드를 추가할 필요 없이 오류 타입을 변환합니다.
Listing 9-7 의 컨텍스트에서, File::open 호출 끝에 있는 ?는 Ok 내부의 값을 변수 username_file로 반환합니다. 오류가 발생하면, ? 연산자는 전체 함수에서 조기에 반환하고 모든 Err 값을 호출하는 코드에 제공합니다. read_to_string 호출 끝에 있는 ?에도 동일하게 적용됩니다.
? 연산자는 많은 상용구를 제거하고 이 함수의 구현을 더 간단하게 만듭니다. Listing 9-8 과 같이 ? 다음에 메서드 호출을 바로 연결하여 이 코드를 더 짧게 만들 수도 있습니다.
파일 이름: src/main.rs
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
Listing 9-8: ? 연산자 다음에 메서드 호출 연결
새로운 String을 username에 생성하는 것을 함수의 시작 부분으로 옮겼습니다. 해당 부분은 변경되지 않았습니다. 변수 username_file을 생성하는 대신, read_to_string 호출을 File::open("hello.txt")?의 결과에 직접 연결했습니다. read_to_string 호출 끝에도 여전히 ?가 있으며, File::open과 read_to_string이 모두 성공하면 오류를 반환하는 대신 username을 포함하는 Ok 값을 반환합니다. 기능은 다시 Listing 9-6 및 Listing 9-7 과 동일합니다. 이것은 단지 다른, 더 사용하기 쉬운 방식으로 작성된 것입니다.
Listing 9-9 는 fs::read_to_string을 사용하여 이를 훨씬 더 짧게 만드는 방법을 보여줍니다.
파일 이름: src/main.rs
use std::fs;
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
Listing 9-9: 파일을 열고 읽는 대신 fs::read_to_string 사용
파일을 문자열로 읽는 것은 매우 일반적인 작업이므로, 표준 라이브러리는 파일을 열고, 새로운 String을 생성하고, 파일의 내용을 읽고, 해당 String에 내용을 넣고, 반환하는 편리한 fs::read_to_string 함수를 제공합니다. 물론, fs::read_to_string을 사용하면 모든 오류 처리를 설명할 기회가 없으므로, 먼저 더 긴 방식을 사용했습니다.
? 연산자는 반환 타입이 ?가 사용되는 값과 호환되는 함수에서만 사용할 수 있습니다. 이는 ? 연산자가 Listing 9-6 에서 정의한 match 표현식과 동일한 방식으로 함수에서 값을 조기에 반환하도록 정의되었기 때문입니다. Listing 9-6 에서 match는 Result 값을 사용했고, 조기 반환 분기는 Err(e) 값을 반환했습니다. 함수의 반환 타입은 이 return과 호환되도록 Result여야 합니다.
Listing 9-10 에서, ? 연산자를 main 함수에서 사용하고 반환 타입이 ?를 사용하는 값의 타입과 호환되지 않는 경우 발생하는 오류를 살펴보겠습니다.
파일 이름: src/main.rs
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
Listing 9-10: ()를 반환하는 main 함수에서 ?를 사용하려고 하면 컴파일되지 않습니다.
이 코드는 파일을 엽니다. 이는 실패할 수 있습니다. ? 연산자는 File::open에서 반환된 Result 값을 따르지만, 이 main 함수는 Result가 아닌 ()의 반환 타입을 가지고 있습니다. 이 코드를 컴파일하면 다음과 같은 오류 메시지가 나타납니다.
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:4:48
|
3 | / fn main() {
4 | | let greeting_file = File::open("hello.txt")?;
| | ^ cannot use the `?`
operator in a function that returns `()`
5 | | }
| |_- this function should return `Result` or `Option` to accept `?`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not
implemented for `()`
이 오류는 ? 연산자를 Result, Option 또는 FromResidual을 구현하는 다른 타입을 반환하는 함수에서만 사용할 수 있음을 지적합니다.
오류를 수정하려면 두 가지 선택지가 있습니다. 한 가지 선택지는 해당 작업을 방해하는 제한 사항이 없는 한, 함수의 반환 타입을 ? 연산자를 사용하는 값과 호환되도록 변경하는 것입니다. 다른 선택지는 match 또는 Result<T, E> 메서드 중 하나를 사용하여 적절한 방식으로 Result<T, E>를 처리하는 것입니다.
오류 메시지는 또한 ?가 Option<T> 값과 함께 사용될 수 있다고 언급했습니다. Result에 ?를 사용하는 것과 마찬가지로, Option에 ?를 사용하는 것은 Option을 반환하는 함수에서만 가능합니다. Option<T>에서 호출될 때 ? 연산자의 동작은 Result<T, E>에서 호출될 때와 유사합니다. 값이 None이면, None은 해당 지점에서 함수에서 조기에 반환됩니다. 값이 Some이면, Some 내부의 값이 표현식의 결과 값이고 함수는 계속됩니다. Listing 9-11 은 주어진 텍스트에서 첫 번째 줄의 마지막 문자를 찾는 함수의 예시입니다.
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
}
Listing 9-11: Option<T> 값에 ? 연산자 사용
이 함수는 문자가 있을 수도 있고 없을 수도 있기 때문에 Option<char>를 반환합니다. 이 코드는 text 문자열 슬라이스 인수를 가져와서 lines 메서드를 호출합니다. 이 메서드는 문자열의 줄에 대한 반복자를 반환합니다. 이 함수는 첫 번째 줄을 검사하려는 것이므로, 반복자에서 next를 호출하여 반복자에서 첫 번째 값을 가져옵니다. text가 빈 문자열이면, 이 next 호출은 None을 반환하며, 이 경우 ?를 사용하여 멈추고 last_char_of_first_line에서 None을 반환합니다. text가 빈 문자열이 아니면, next는 text의 첫 번째 줄의 문자열 슬라이스를 포함하는 Some 값을 반환합니다.
?는 문자열 슬라이스를 추출하고, 해당 문자열 슬라이스에서 chars를 호출하여 문자의 반복자를 얻을 수 있습니다. 우리는 이 첫 번째 줄의 마지막 문자에 관심이 있으므로, last를 호출하여 반복자의 마지막 항목을 반환합니다. 이것은 Option입니다. 왜냐하면 첫 번째 줄이 빈 문자열일 가능성이 있기 때문입니다. 예를 들어, text가 빈 줄로 시작하지만 다른 줄에 문자가 있는 경우, "\nhi"와 같습니다. 그러나 첫 번째 줄에 마지막 문자가 있으면, Some 변형에서 반환됩니다. 중간의 ? 연산자는 이 로직을 간결하게 표현하는 방법을 제공하여, 한 줄로 함수를 구현할 수 있도록 합니다. Option에서 ? 연산자를 사용할 수 없다면, 더 많은 메서드 호출이나 match 표현식을 사용하여 이 로직을 구현해야 합니다.
Result를 반환하는 함수에서 Result에 ? 연산자를 사용할 수 있으며, Option을 반환하는 함수에서 Option에 ? 연산자를 사용할 수 있지만, 혼합하여 사용할 수는 없습니다. ? 연산자는 자동으로 Result를 Option으로 또는 그 반대로 변환하지 않습니다. 이러한 경우, Result의 ok 메서드 또는 Option의 ok_or 메서드와 같은 메서드를 사용하여 명시적으로 변환할 수 있습니다.
지금까지 사용한 모든 main 함수는 ()를 반환합니다. main 함수는 실행 가능한 프로그램의 진입점과 종료점이므로 특별하며, 프로그램이 예상대로 작동하기 위해 반환 타입에 대한 제한이 있습니다.
다행히, main은 Result<(), E>도 반환할 수 있습니다. Listing 9-12 는 Listing 9-10 의 코드를 가지고 있지만, main의 반환 타입을 Result<(), Box<dyn Error>>로 변경하고 마지막에 반환 값 Ok(())를 추가했습니다. 이 코드는 이제 컴파일됩니다.
파일 이름: src/main.rs
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
Listing 9-12: main을 Result<(), E>로 변경하면 Result 값에 ? 연산자를 사용할 수 있습니다.
Box<dyn Error> 타입은 트레이트 객체이며, "다양한 타입의 값을 허용하는 트레이트 객체 사용"에서 이에 대해 이야기할 것입니다. 지금은 Box<dyn Error>를 "모든 종류의 오류"로 읽을 수 있습니다. 오류 타입이 Box<dyn Error>인 main 함수에서 Result 값에 ?를 사용하는 것은 모든 Err 값을 조기에 반환할 수 있기 때문에 허용됩니다. 이 main 함수의 본문은 std::io::Error 타입의 오류만 반환하더라도, Box<dyn Error>를 지정함으로써, 이 시그니처는 main의 본문에 다른 오류를 반환하는 코드가 더 추가되더라도 계속 정확하게 유지됩니다.
main 함수가 Result<(), E>를 반환하면, 실행 파일은 main이 Ok(())를 반환하면 값 0으로 종료되고, main이 Err 값을 반환하면 0 이 아닌 값으로 종료됩니다. C 로 작성된 실행 파일은 종료될 때 정수를 반환합니다. 성공적으로 종료되는 프로그램은 정수 0을 반환하고, 오류가 발생하는 프로그램은 0이 아닌 다른 정수를 반환합니다. Rust 도 이 규칙과 호환되도록 실행 파일에서 정수를 반환합니다.
main 함수는 std::process::Termination 트레이트를 구현하는 모든 타입을 반환할 수 있으며, 이 트레이트에는 ExitCode를 반환하는 함수 report가 포함되어 있습니다. 자체 타입에 대한 Termination 트레이트를 구현하는 방법에 대한 자세한 내용은 표준 라이브러리 문서를 참조하십시오.
이제 panic!을 호출하거나 Result를 반환하는 세부 사항에 대해 논의했으므로, 어떤 경우에 어떤 것을 사용하는 것이 적절한지 결정하는 방법에 대한 주제로 돌아가겠습니다.
축하합니다! 복구 가능한 오류 (Recoverable Errors) with Result 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 실력을 향상시킬 수 있습니다.