Các lỗi có thể khôi phục với Result
Hầu hết các lỗi không đủ nghiêm trọng để yêu cầu chương trình dừng hoàn toàn. Đôi khi khi một hàm thất bại, đó là vì lý do mà bạn có thể dễ dàng hiểu và phản hồi. Ví dụ, nếu bạn cố mở một tập tin và thao tác đó thất bại vì tập tin không tồn tại, bạn có thể muốn tạo tập tin đó thay vì kết thúc tiến trình.
Nhắc lại từ "Xử lý thất bại tiềm ẩn với Result" trong Chương 2 rằng enum Result được định nghĩa với hai biến thể, Ok
và Err, như sau:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
T và E là các tham số kiểu generic: chúng ta sẽ thảo luận chi tiết về
generics trong Chương 10. Điều bạn cần biết ngay bây giờ là T đại diện cho
kiểu của giá trị sẽ được trả về trong trường hợp thành công bên trong biến thể
Ok, và E đại diện cho kiểu của lỗi sẽ được trả về trong trường hợp thất bại
bên trong biến thể Err. Bởi vì Result có các tham số kiểu generic này, chúng
ta có thể sử dụng kiểu Result và các hàm được định nghĩa trên nó trong nhiều
tình huống khác nhau, nơi giá trị thành công và giá trị lỗi mà chúng ta muốn trả
về có thể khác nhau.
Hãy gọi một hàm trả về giá trị Result vì hàm có thể thất bại. Trong Listing
9-3, chúng ta thử mở một tập tin.
use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); }
Kiểu trả về của File::open là Result<T, E>. Tham số generic T đã được điền
bởi quá trình triển khai của File::open với kiểu của giá trị thành công,
std::fs::File, là một handle của tập tin. Kiểu của E được sử dụng trong giá
trị lỗi là std::io::Error. Kiểu trả về này có nghĩa là lời gọi đến
File::open có thể thành công và trả về một handle của tập tin mà chúng ta có
thể đọc hoặc ghi. Lời gọi hàm cũng có thể thất bại: ví dụ, tập tin có thể không
tồn tại, hoặc chúng ta có thể không có quyền truy cập vào tập tin. Hàm
File::open cần có cách để nói cho chúng ta biết liệu nó đã thành công hay thất
bại và đồng thời cung cấp cho chúng ta handle của tập tin hoặc thông tin lỗi.
Thông tin này chính xác là những gì mà enum Result truyền đạt.
Trong trường hợp File::open thành công, giá trị trong biến
greeting_file_result sẽ là một thực thể của Ok chứa một handle của tập tin.
Trong trường hợp thất bại, giá trị trong greeting_file_result sẽ là một thực
thể của Err chứa thêm thông tin về loại lỗi đã xảy ra.
Chúng ta cần thêm vào mã trong Listing 9-3 để thực hiện các hành động khác nhau
tùy thuộc vào giá trị mà File::open trả về. Listing 9-4 cho thấy một cách để
xử lý Result bằng cách sử dụng công cụ cơ bản, biểu thức match mà chúng ta
đã thảo luận trong Chương 6.
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:?}"), }; }
Lưu ý rằng, giống như enum Option, enum Result và các biến thể của nó đã
được đưa vào phạm vi bởi prelude, vì vậy chúng ta không cần chỉ định Result::
trước các biến thể Ok và Err trong các nhánh của match.
Khi kết quả là Ok, mã này sẽ trả về giá trị file bên trong từ biến thể Ok,
và sau đó chúng ta gán giá trị handle của tập tin đó cho biến greeting_file.
Sau match, chúng ta có thể sử dụng handle của tập tin để đọc hoặc ghi.
Nhánh khác của match xử lý trường hợp chúng ta nhận được giá trị Err từ
File::open. Trong ví dụ này, chúng ta đã chọn gọi macro panic!. Nếu không có
tập tin nào có tên hello.txt trong thư mục hiện tại của chúng ta và chúng ta
chạy mã này, chúng ta sẽ thấy đầu ra sau đây từ macro panic!:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling`
thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Như thường lệ, đầu ra này cho chúng ta biết chính xác điều gì đã sai.
Xử lý các lỗi khác nhau
Mã trong Listing 9-4 sẽ gọi panic! bất kể vì sao File::open thất bại. Tuy
nhiên, chúng ta muốn thực hiện các hành động khác nhau với các lý do thất bại
khác nhau. Nếu File::open thất bại vì tập tin không tồn tại, chúng ta muốn tạo
tập tin và trả về handle cho tập tin mới. Nếu File::open thất bại vì bất kỳ lý
do nào khác—ví dụ, vì chúng ta không có quyền mở tập tin—chúng ta vẫn muốn mã
gọi panic! theo cách giống như trong Listing 9-4. Để làm điều này, chúng ta
thêm một biểu thức match bên trong, như trong Listing 9-5.
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:?}"),
},
_ => {
panic!("Problem opening the file: {error:?}");
}
},
};
}
Kiểu của giá trị mà File::open trả về bên trong biến thể Err là io::Error,
đây là một struct được cung cấp bởi thư viện chuẩn. Struct này có một phương
thức kind mà chúng ta có thể gọi để lấy một giá trị io::ErrorKind. Enum
io::ErrorKind được cung cấp bởi thư viện chuẩn và có các biến thể đại diện cho
các loại lỗi khác nhau có thể phát sinh từ một thao tác io. Biến thể mà chúng
ta muốn sử dụng là ErrorKind::NotFound, cho biết tập tin mà chúng ta đang cố
gắng mở không tồn tại. Vì vậy, chúng ta khớp với greeting_file_result, nhưng
chúng ta cũng có một match bên trong trên error.kind().
Điều kiện mà chúng ta muốn kiểm tra trong match bên trong là liệu giá trị trả về
bởi error.kind() có phải là biến thể NotFound của enum ErrorKind hay
không. Nếu đúng, chúng ta cố gắng tạo tập tin với File::create. Tuy nhiên, vì
File::create cũng có thể thất bại, chúng ta cần một nhánh thứ hai trong biểu
thức match bên trong. Khi tập tin không thể được tạo, một thông báo lỗi khác
được in ra. Nhánh thứ hai của match bên ngoài không đổi, vì vậy chương trình sẽ
panic khi có bất kỳ lỗi nào khác ngoài lỗi tập tin không tồn tại.
Các lựa chọn thay thế cho việc sử dụng
matchvớiResult<T, E>
matchthật là nhiều! Biểu thứcmatchrất hữu ích nhưng cũng rất nguyên thủy. Trong Chương 13, bạn sẽ học về closures, được sử dụng với nhiều phương thức được định nghĩa trênResult<T, E>. Các phương thức này có thể ngắn gọn hơn việc sử dụngmatchkhi xử lý các giá trịResult<T, E>trong mã của bạn.Ví dụ, đây là một cách khác để viết logic tương tự như đã hiển thị trong Listing 9-5, lần này sử dụng closures và phương thức
unwrap_or_else: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!("Có vấn đề khi tạo tập tin: {error:?}"); }) } else { panic!("Có vấn đề khi mở tập tin: {error:?}"); } }); }Mặc dù mã này có cùng hành vi với Listing 9-5, nhưng nó không chứa bất kỳ biểu thức
matchnào và dễ đọc hơn. Quay lại ví dụ này sau khi bạn đã đọc Chương 13, và tìm kiếm phương thứcunwrap_or_elsetrong tài liệu của thư viện chuẩn. Nhiều phương thức khác có thể làm gọn các biểu thứcmatchlồng nhau lớn khi bạn xử lý lỗi.
Các cách viết tắt cho Panic khi có lỗi: unwrap và expect
Sử dụng match hoạt động khá tốt, nhưng có thể hơi dài dòng và không phải lúc
nào cũng truyền đạt ý định rõ ràng. Kiểu Result<T, E> có nhiều phương thức trợ
giúp được định nghĩa trên nó để thực hiện các tác vụ khác nhau, cụ thể hơn.
Phương thức unwrap là một phương thức viết tắt được triển khai giống như biểu
thức match mà chúng ta đã viết trong Listing 9-4. Nếu giá trị Result là biến
thể Ok, unwrap sẽ trả về giá trị bên trong Ok. Nếu Result là biến thể
Err, unwrap sẽ gọi macro panic! cho chúng ta. Đây là một ví dụ về unwrap
đang hoạt động:
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt").unwrap(); }
Nếu chúng ta chạy mã này mà không có tập tin hello.txt, chúng ta sẽ thấy một
thông báo lỗi từ lời gọi panic! mà phương thức unwrap thực hiện:
thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
Tương tự, phương thức expect cho phép chúng ta chọn thông báo lỗi panic!. Sử
dụng expect thay vì unwrap và cung cấp thông báo lỗi tốt có thể truyền đạt ý
định của bạn và làm cho việc theo dõi nguồn gốc của một panic dễ dàng hơn. Cú
pháp của expect trông như thế này:
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt") .expect("hello.txt should be included in this project"); }
Chúng ta sử dụng expect theo cách tương tự như unwrap: để trả về handle tập
tin hoặc gọi macro panic!. Thông báo lỗi được sử dụng bởi expect trong lời
gọi của nó đến panic! sẽ là tham số mà chúng ta truyền cho expect, thay vì
thông báo panic! mặc định mà unwrap sử dụng. Đây là cách nó trông:
thread 'main' panicked at src/main.rs:5:10:
hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }
Trong mã chất lượng sản xuất, hầu hết các Rustacean chọn expect thay vì
unwrap và cung cấp thêm ngữ cảnh về lý do tại sao thao tác được mong đợi sẽ
luôn thành công. Bằng cách đó, nếu giả định của bạn được chứng minh là sai, bạn
có thêm thông tin để sử dụng trong việc gỡ lỗi.
Lan truyền lỗi
Khi triển khai của một hàm gọi thứ gì đó có thể thất bại, thay vì xử lý lỗi bên trong chính hàm, bạn có thể trả về lỗi cho mã gọi hàm đó để nó có thể quyết định phải làm gì. Điều này được gọi là lan truyền lỗi và cung cấp nhiều quyền kiểm soát hơn cho mã gọi hàm, nơi có thể có nhiều thông tin hoặc logic hơn để quyết định cách xử lý lỗi so với những gì bạn có sẵn trong ngữ cảnh của mã của bạn.
Ví dụ, Listing 9-6 hiển thị một hàm đọc tên người dùng từ một tập tin. Nếu tập tin không tồn tại hoặc không thể đọc được, hàm này sẽ trả về những lỗi đó cho mã đã gọi hàm.
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let username_file_result = File::open("hello.txt"); let mut username_file = match username_file_result { Ok(file) => file, Err(e) => return Err(e), }; let mut username = String::new(); match username_file.read_to_string(&mut username) { Ok(_) => Ok(username), Err(e) => Err(e), } } }
Hàm này có thể được viết theo cách ngắn gọn hơn nhiều, nhưng chúng ta sẽ bắt đầu
bằng cách làm nhiều thao tác thủ công để khám phá việc xử lý lỗi; cuối cùng,
chúng ta sẽ hiển thị cách viết ngắn gọn hơn. Hãy xem xét kiểu trả về của hàm
trước: Result<String, io::Error>. Điều này có nghĩa là hàm đang trả về một giá
trị thuộc kiểu Result<T, E>, trong đó tham số generic T đã được điền với
kiểu cụ thể String và kiểu generic E đã được điền với kiểu cụ thể
io::Error.
Nếu hàm này thành công mà không có bất kỳ vấn đề nào, mã gọi hàm này sẽ nhận
được một giá trị Ok chứa một String—tên người dùng mà hàm này đã đọc từ tập
tin. Nếu hàm này gặp bất kỳ vấn đề nào, mã gọi sẽ nhận được một giá trị Err
chứa một instance của io::Error có chứa thêm thông tin về những vấn đề đã xảy
ra. Chúng ta đã chọn io::Error làm kiểu trả về của hàm này vì đó là kiểu của
giá trị lỗi được trả về từ cả hai thao tác mà chúng ta đang gọi trong thân hàm
có thể thất bại: hàm File::open và phương thức read_to_string.
Thân hàm bắt đầu bằng cách gọi hàm File::open. Sau đó, chúng ta xử lý giá trị
Result với một match tương tự như match trong Listing 9-4. Nếu
File::open thành công, handle tập tin trong biến mẫu file sẽ trở thành giá
trị trong biến có thể thay đổi username_file và hàm tiếp tục. Trong trường hợp
Err, thay vì gọi panic!, chúng ta sử dụng từ khóa return để thoát sớm khỏi
hàm hoàn toàn và truyền giá trị lỗi từ File::open, hiện đang nằm trong biến
mẫu e, trở lại cho mã gọi như là giá trị lỗi của hàm này.
Vì vậy, nếu chúng ta có một handle tập tin trong username_file, hàm tiếp theo
sẽ tạo một String mới trong biến username và gọi phương thức
read_to_string trên handle tập tin trong username_file để đọc nội dung của
tập tin vào username. Phương thức read_to_string cũng trả về một Result vì
nó có thể thất bại, ngay cả khi File::open đã thành công. Vì vậy, chúng ta cần
một match khác để xử lý Result đó: nếu read_to_string thành công, thì hàm
của chúng ta đã thành công, và chúng ta trả về tên người dùng từ tập tin hiện
đang nằm trong username được bọc trong một Ok. Nếu read_to_string thất
bại, chúng ta trả về giá trị lỗi theo cách tương tự như cách chúng ta trả về giá
trị lỗi trong match đã xử lý giá trị trả về của File::open. Tuy nhiên, chúng
ta không cần nói return một cách rõ ràng, vì đây là biểu thức cuối cùng trong
hàm.
Mã gọi mã này sẽ xử lý việc nhận giá trị Ok chứa tên người dùng hoặc giá trị
Err chứa một io::Error. Mã gọi quyết định phải làm gì với những giá trị này.
Nếu mã gọi nhận được giá trị Err, nó có thể gọi panic! và làm crash chương
trình, sử dụng một tên người dùng mặc định, hoặc tìm kiếm tên người dùng từ một
nơi khác ngoài tập tin, ví dụ vậy. Chúng ta không có đủ thông tin về những gì mã
gọi thực sự đang cố gắng làm, vì vậy chúng ta lan truyền tất cả thông tin thành
công hoặc lỗi lên trên để nó xử lý một cách phù hợp.
Mẫu lan truyền lỗi này rất phổ biến trong Rust đến nỗi Rust cung cấp toán tử dấu
hỏi ? để làm điều này dễ dàng hơn.
Một cách viết tắt để lan truyền lỗi: Toán tử ?
Listing 9-7 hiển thị một triển khai của read_username_from_file có cùng chức
năng như trong Listing 9-6, nhưng triển khai này sử dụng toán tử ?.
#![allow(unused)] fn main() { 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) } }
Dấu ? được đặt sau một giá trị Result được định nghĩa để hoạt động theo cách
gần như giống như các biểu thức match mà chúng ta đã định nghĩa để xử lý các
giá trị Result trong Listing 9-6. Nếu giá trị của Result là Ok, giá trị
bên trong Ok sẽ được trả về từ biểu thức này, và chương trình sẽ tiếp tục. Nếu
giá trị là Err, Err sẽ được trả về từ toàn bộ hàm như thể chúng ta đã sử
dụng từ khóa return để giá trị lỗi được lan truyền đến mã gọi.
Có một sự khác biệt giữa những gì mà biểu thức match từ Listing 9-6 làm và
những gì toán tử ? làm: các giá trị lỗi mà toán tử ? được gọi trên chúng đi
qua hàm from, được định nghĩa trong trait From trong thư viện chuẩn, được sử
dụng để chuyển đổi giá trị từ một kiểu sang một kiểu khác. Khi toán tử ? gọi
hàm from, kiểu lỗi nhận được được chuyển đổi thành kiểu lỗi được định nghĩa
trong kiểu trả về của hàm hiện tại. Điều này hữu ích khi một hàm trả về một kiểu
lỗi để đại diện cho tất cả các cách mà một hàm có thể thất bại, ngay cả khi các
phần có thể thất bại vì nhiều lý do khác nhau.
Ví dụ, chúng ta có thể thay đổi hàm read_username_from_file trong Listing 9-7
để trả về một kiểu lỗi tùy chỉnh có tên OurError mà chúng ta định nghĩa. Nếu
chúng ta cũng định nghĩa impl From<io::Error> for OurError để xây dựng một
instance của OurError từ một io::Error, thì các lời gọi toán tử ? trong
thân của read_username_from_file sẽ gọi from và chuyển đổi các kiểu lỗi mà
không cần thêm bất kỳ mã nào vào hàm.
Trong ngữ cảnh của Listing 9-7, dấu ? ở cuối của lời gọi File::open sẽ trả
về giá trị bên trong Ok cho biến username_file. Nếu xảy ra lỗi, toán tử ?
sẽ thoát sớm khỏi toàn bộ hàm và đưa bất kỳ giá trị Err nào cho mã gọi. Điều
tương tự cũng áp dụng cho dấu ? ở cuối lời gọi read_to_string.
Toán tử ? loại bỏ rất nhiều mã soạn sẵn và làm cho triển khai của hàm này đơn
giản hơn. Chúng ta thậm chí có thể làm cho mã này ngắn gọn hơn nữa bằng cách nối
các lời gọi phương thức ngay sau ?, như trong Listing 9-8.
#![allow(unused)] fn main() { 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) } }
Chúng ta đã di chuyển việc tạo String mới trong username lên đầu hàm; phần
đó không thay đổi. Thay vì tạo biến username_file, chúng ta đã nối lời gọi đến
read_to_string trực tiếp vào kết quả của File::open("hello.txt")?. Chúng ta
vẫn có một dấu ? ở cuối lời gọi read_to_string, và chúng ta vẫn trả về một
giá trị Ok chứa username khi cả File::open và read_to_string đều thành
công thay vì trả về lỗi. Chức năng một lần nữa giống như trong Listing 9-6 và
Listing 9-7; đây chỉ là một cách viết khác, phong cách hơn.
Listing 9-9 hiển thị một cách để làm cho nó thậm chí còn ngắn gọn hơn bằng cách
sử dụng fs::read_to_string.
#![allow(unused)] fn main() { use std::fs; use std::io; fn read_username_from_file() -> Result<String, io::Error> { fs::read_to_string("hello.txt") } }
Đọc một tập tin vào một chuỗi là một hoạt động khá phổ biến, vì vậy thư viện
chuẩn cung cấp hàm fs::read_to_string thuận tiện để mở tập tin, tạo một
String mới, đọc nội dung của tập tin, đưa nội dung vào String đó, và trả về
nó. Tất nhiên, sử dụng fs::read_to_string không cho chúng ta cơ hội để giải
thích tất cả việc xử lý lỗi, vì vậy chúng ta đã làm nó theo cách dài dòng hơn
trước.
Toán tử ? có thể được sử dụng ở đâu
Toán tử ? chỉ có thể được sử dụng trong các hàm có kiểu trả về tương thích với
giá trị mà ? được sử dụng trên đó. Điều này là vì toán tử ? được định nghĩa
để thực hiện việc trả về sớm một giá trị từ hàm, theo cùng cách như biểu thức
match mà chúng ta đã định nghĩa trong Listing 9-6. Trong Listing 9-6, match
đang sử dụng một giá trị Result, và nhánh trả về sớm trả về một giá trị
Err(e). Kiểu trả về của hàm phải là một Result để nó tương thích với
return này.
Trong Listing 9-10, hãy xem lỗi mà chúng ta sẽ nhận được nếu chúng ta sử dụng
toán tử ? trong một hàm main với kiểu trả về không tương thích với kiểu của
giá trị mà chúng ta sử dụng ? trên đó:
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
Mã này mở một tập tin, điều này có thể thất bại. Toán tử ? theo sau giá trị
Result được trả về bởi File::open, nhưng hàm main này có kiểu trả về là
(), không phải Result. Khi chúng ta biên dịch mã này, chúng ta nhận được
thông báo lỗi sau:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
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() {
| --------- this function should return `Result` or `Option` to accept `?`
4 | let greeting_file = File::open("hello.txt")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
help: consider adding return type
|
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 | let greeting_file = File::open("hello.txt")?;
5 + Ok(())
|
For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error
Lỗi này chỉ ra rằng chúng ta chỉ được phép sử dụng toán tử ? trong một hàm trả
về Result, Option, hoặc một kiểu khác mà có triển khai FromResidual.
Để sửa lỗi, bạn có hai lựa chọn. Một lựa chọn là thay đổi kiểu trả về của hàm để
tương thích với giá trị mà bạn đang sử dụng toán tử ? trên đó miễn là bạn
không có hạn chế nào ngăn bạn làm điều đó. Lựa chọn khác là sử dụng match hoặc
một trong các phương thức của Result<T, E> để xử lý Result<T, E> theo cách
phù hợp.
Thông báo lỗi cũng đề cập rằng ? có thể được sử dụng với các giá trị
Option<T> cũng như. Như với việc sử dụng ? trên Result, bạn chỉ có thể sử
dụng ? trên Option trong một hàm trả về một Option. Hành vi của toán tử
? khi được gọi trên một Option<T> tương tự như hành vi của nó khi được gọi
trên một Result<T, E>: nếu giá trị là None, None sẽ được trả về sớm từ hàm
tại điểm đó. Nếu giá trị là Some, giá trị bên trong Some là giá trị kết quả
của biểu thức, và hàm tiếp tục. Listing 9-11 có một ví dụ về một hàm tìm ký tự
cuối cùng của dòng đầu tiên trong văn bản đã cho.
fn last_char_of_first_line(text: &str) -> Option<char> { text.lines().next()?.chars().last() } fn main() { assert_eq!( last_char_of_first_line("Hello, world\nHow are you today?"), Some('d') ); assert_eq!(last_char_of_first_line(""), None); assert_eq!(last_char_of_first_line("\nhi"), None); }
Hàm này trả về Option<char> vì có thể có một ký tự ở đó, nhưng cũng có thể
không có. Mã này nhận tham số slice chuỗi text và gọi phương thức lines trên
nó, trả về một iterator trên các dòng trong chuỗi. Vì hàm này muốn kiểm tra dòng
đầu tiên, nó gọi next trên iterator để lấy giá trị đầu tiên từ iterator. Nếu
text là chuỗi rỗng, lời gọi này đến next sẽ trả về None, trong trường hợp
đó chúng ta sử dụng ? để dừng và trả về None từ last_char_of_first_line.
Nếu text không phải là chuỗi rỗng, next sẽ trả về một giá trị Some chứa
một slice chuỗi của dòng đầu tiên trong text.
Dấu ? trích xuất slice chuỗi, và chúng ta có thể gọi chars trên slice chuỗi
đó để lấy một iterator của các ký tự của nó. Chúng ta quan tâm đến ký tự cuối
cùng trong dòng đầu tiên này, vì vậy chúng ta gọi last để trả về mục cuối cùng
trong iterator. Đây là một Option vì có thể dòng đầu tiên là chuỗi rỗng; ví
dụ, nếu text bắt đầu bằng một dòng trống nhưng có các ký tự trên các dòng
khác, như trong "\nhi". Tuy nhiên, nếu có một ký tự cuối cùng trên dòng đầu
tiên, nó sẽ được trả về trong biến thể Some. Toán tử ? ở giữa cung cấp cho
chúng ta một cách súc tích để biểu thị logic này, cho phép chúng ta triển khai
hàm trong một dòng. Nếu chúng ta không thể sử dụng toán tử ? trên Option,
chúng ta sẽ phải triển khai logic này bằng cách sử dụng nhiều lời gọi phương
thức hơn hoặc một biểu thức match.
Lưu ý rằng bạn có thể sử dụng toán tử ? trên một Result trong một hàm trả về
Result, và bạn có thể sử dụng toán tử ? trên một Option trong một hàm trả
về Option, nhưng bạn không thể kết hợp và phù hợp. Toán tử ? sẽ không tự
động chuyển đổi một Result thành một Option hoặc ngược lại; trong những
trường hợp đó, bạn có thể sử dụng các phương thức như phương thức ok trên
Result hoặc phương thức ok_or trên Option để thực hiện chuyển đổi một cách
rõ ràng.
Cho đến nay, tất cả các hàm main mà chúng ta đã sử dụng đều trả về (). Hàm
main rất đặc biệt vì nó là điểm vào và điểm ra của một chương trình thực thi,
và có các hạn chế về kiểu trả về của nó để chương trình hoạt động như mong đợi.
May mắn thay, main cũng có thể trả về một Result<(), E>. Listing 9-12 có mã
từ Listing 9-10, nhưng chúng ta đã thay đổi kiểu trả về của main thành
Result<(), Box<dyn Error>> và đã thêm một giá trị trả về Ok(()) vào cuối. Mã
này sẽ biên dịch bây giờ.
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
Kiểu Box<dyn Error> là một trait object, chúng ta sẽ nói về nó trong "Sử
dụng các Trait Object cho phép các giá trị của các kiểu khác
nhau" trong Chương 18. Hiện tại, bạn có thể đọc Box<dyn Error> như là "bất
kỳ loại lỗi nào." Sử dụng ? trên một giá trị Result trong một hàm main với
kiểu lỗi Box<dyn Error> được cho phép vì nó cho phép bất kỳ giá trị Err nào được
trả về sớm. Mặc dù thân của hàm main này sẽ chỉ trả về các lỗi của kiểu std::io::Error,
nhưng bằng cách chỉ định Box<dyn Error>, chữ ký này sẽ tiếp tục chính xác ngay
cả khi thêm mã trả về các lỗi khác được thêm vào thân của main.
Khi một hàm main trả về một Result<(), E>, chương trình thực thi sẽ thoát
với giá trị 0 nếu main trả về Ok(()) và sẽ thoát với giá trị khác 0 nếu
main trả về một giá trị Err. Các chương trình thực thi được viết bằng C trả
về số nguyên khi chúng thoát: các chương trình thoát thành công trả về số nguyên
0, và các chương trình bị lỗi trả về một số nguyên khác 0. Rust cũng trả về số
nguyên từ các chương trình thực thi để tương thích với quy ước này.
Hàm main có thể trả về bất kỳ kiểu nào triển khai trait
std::process::Termination, chứa một hàm report
trả về một ExitCode. Tham khảo tài liệu của thư viện chuẩn để biết thêm thông
tin về việc triển khai trait Termination cho các kiểu của riêng bạn.
Bây giờ chúng ta đã thảo luận về chi tiết của việc gọi panic! hoặc trả về
Result, hãy quay lại chủ đề về cách quyết định sử dụng cái nào trong trường
hợp nào.