Rust 錯誤處理的五種方式及學習特質如何支持繼承
在編寫代碼時,學習如何處理錯誤是非常重要的。錯誤通常是由于用戶對程序目的的誤解而產生的。錯誤不僅可以作為用戶的教學工具,還能提高代碼的效率。與我們用于控制流程的邏輯條件不同,錯誤是由于某種期望未被滿足或某個選項不可用而產生的。Rust提供了五種不同的方法來直接解決這些“期望”和“不可用”的問題。
錯誤最終服務于用戶和開發者
當用戶收到晦澀難懂的錯誤反饋時,他們會感到沮喪。而開發者則希望看到導致崩潰的一系列事件或函數調用。這導致我們將錯誤視為字符串和結構化類型。
觀察Python如何處理錯誤
在學習Rust的錯誤處理時,通過比較和對比Python的錯誤處理方式是非常有效的。這樣可以更容易地發現模式。
Python與Rust的比較與對比
無錯誤處理:主函數引發異常并失敗
result = 10 / 0
print(result)
輸出:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
Try Except:捕獲邊界情況
try:
result = 10 / 0 # This will raise ZeroDivisionError
except ZeroDivisionError:
print("Cannot divide by zero!")
except TypeError:
print("Type error occurred!")
Try Except Finally:確保異常期間的正確清理
try:
file = open("example.txt", "r")
except FileNotFoundError:
print("File not found.")
finally:
print("Closing file (if open).")
if 'file' in locals() and not file.closed:
file.close()
Raising Exception:崩潰代碼/由調用函數處理
age = -1
if age < 0:
raise ValueError("Age cannot be negative!")
創建自定義錯誤:為開發者提供結構化錯誤
class ApplicationError(Exception):
"""Base class for all application errors."""
pass
class DatabaseError(ApplicationError):
"""Exception raised for database-related errors."""
pass
class APIError(ApplicationError):
"""Exception raised for API-related errors."""
pass
# Usage
try:
raise DatabaseError("Unable to connect to the database.")
except DatabaseError as e:
print(f"Database error: {e}")
except ApplicationError:
print("General application error.")
通過這些處理,程序可以繼續運行,直到用戶關閉應用程序。在此過程中,我們可以看到用戶請求是如何被服務的,以及支持設備和應用程序在做什么。
觀察Rust如何處理錯誤
現在輪到Rust了。錯誤處理中的術語如Panic、Unwrap、Expect,以及“?”,每一個都讓我們采取行動,編寫更好的代碼。在Rust中讓代碼Panic被認為是不好的做法。構建應用程序或庫時,定義良好的錯誤處理是關鍵。
無錯誤處理:Panic!
fn main() {
let x = 50;
let y = 0;
let rs = x / y;
println!("{}", rs)
}
輸出:
thread 'main' panicked at 'attempt to divide by zero', src/main.rs:11:14
Catch Unwind:捕獲邊界情況
fn main() {
let x = 50;
let y = 10;
let cuw = std::panic::catch_unwind(|| x / y);
match cuw {
Ok(val) => println!("Got the {val}"),
Err(e) => println!("Error: {:?}", e),
}
}
Rust中的存在檢測:match、Results和Options
Rust是一種強類型和內存安全的語言,這導致了Enum的獨特子數據類型Options和Results。兩者都處理我們感興趣的事物的“存在”和“不存在”。match關鍵字用于檢查返回類型是否為Enum的任何變體。在Option的情況下,它是None或一個可以處理和使用的“已知Rust數據類型”。在Result的情況下,它是Error或我們追求的“值”。
在函數中引發錯誤:傳播錯誤
use std::error::Error;
fn div_by_zero(a: i32, b: i32) -> Result<i32, Box<dyn Error>> {
if b == 0 {
return Err("Divide by 0, leads to infinity....".into());
}
Ok(a / b)
}
fn main() {
println!("Doing what catch unwind does, manually");
let err_rs = div_by_zero(57, 0);
let val_rs = div_by_zero(50, 5);
match err_rs {
Ok(val) => println!("Got {val}"),
Err(e) => eprintln!("Got {:?}", e),
}
match val_rs {
Ok(val) => println!("Got {val}"),
Err(e) => eprintln!("Got {:?}", e),
}
}
創建自定義錯誤:為開發者提供結構化錯誤輸出
use std::error::Error;
#[derive(Debug)]
struct ZeroDivideError {
details: String,
}
impl ZeroDivideError {
fn new(msg: &str) -> ZeroDivideError {
ZeroDivideError {
details: msg.to_string(),
}
}
}
use std::fmt::Display;
impl Error for ZeroDivideError {}
impl Display for ZeroDivideError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Zero divide: {}", self.details)
}
}
fn div_by_zero_error(a: i32, b: i32) -> Result<i32, ZeroDivideError> {
if b == 0 {
return Err(ZeroDivideError::new("Structured output"));
}
Ok(a / b)
}
fn main() {
println!("Creating Custom Errors");
let err_rs = div_by_zero_error(57, 0);
let val_rs = div_by_zero_error(50, 5);
match err_rs {
Ok(val) => println!("Got {val}"),
Err(e) => eprintln!("Got {:?}", e),
}
match val_rs {
Ok(val) => println!("Got {val}"),
Err(e) => eprintln!("Got {:?}", e),
}
}
輸出:
Creating Custom Errors
Got ZeroDivideError { details: "Structured output" }
Got 10
錯誤類型依賴于實現的Traits
結構體ZeroDivideError是Rust中的一個標準結構體。在Rust中,結構體相當于Python中的類。通過impl Error for ZeroDivideError,我們將ZeroDivideError變成了一個“錯誤類型結構體”。我們還需要為ZeroDivideError實現std::fmt::Display,以便將錯誤顯示給用戶或開發者。
動態處理不同錯誤:使用Box
Box<dyn Error>在函數可能返回不同類型的錯誤時非常有用,可以統一處理這些錯誤。動態分派(dyn)通過允許運行時多態性實現這一點。
use std::error::Error;
#[derive(Debug)]
struct ZeroDivideError {
details: String,
}
impl ZeroDivideError {
fn new(msg: &str) -> ZeroDivideError {
ZeroDivideError {
details: msg.to_string(),
}
}
}
use std::fmt::Display;
impl Error for ZeroDivideError {}
impl Display for ZeroDivideError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Zero divide: {}", self.details)
}
}
// Combining different error types using `Box<dyn Error>`
pub fn parse_and_double(input: &str) -> Result<i32, Box<dyn Error>> {
let number = input.parse::<i32>()?;
Ok(number * 2)
}
pub fn parse_and_dbl(input: &str) -> Result<i32, ZeroDivideError> {
let number = input.parse::<i32>();
match number {
Err(_) => {
return Err(ZeroDivideError::new(
"Negative number due to number parsing",
))
}
Ok(number) => return Ok(number * 2),
}
}
fn main() {
println!("Creating Custom Errors");
let parseme = parse_and_double("56,");
let prsme = parse_and_dbl("56,");
match parseme {
Ok(val) => println!("Got {val}"),
Err(e) => eprintln!("Got {:?}", e.to_string()),
}
match prsme {
Ok(val) => println!("Got {val}"),
Err(e) => eprintln!("Got {:?}", e.to_string()),
}
}
輸出:
Creating Custom Errors
Got "invalid digit found in string"
Got "Zero divide: Negative number due to number parsing"
這并不是Rust中錯誤處理的全部。在Rust中,大約有20種處理錯誤的模式,而我們這里只看到了其中的5種。練習多種錯誤模式將有助于更容易地識別這些模式。