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
match
vớiResult<T, E>
match
thật là nhiều! Biểu thứcmatch
rấ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ụngmatch
khi 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
match
nà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_else
trong 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ứcmatch
lồ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: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
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.