Rust中類型轉(zhuǎn)換在錯誤處理中的應用小結(jié)
隨著項目的進展,關于Rust的故事又翻開了新的一頁,今天來到了服務器端的開發(fā)場景,發(fā)現(xiàn)錯誤處理中的錯誤類型轉(zhuǎn)換有必要分享一下。
Rust抽象出來了Result<T,E>,T是返回值的類型,E是錯誤類型。只要函數(shù)的返回值的類型被定義為Resut<T,E>,那么作為開發(fā)人員就有責任來處理調(diào)用這個函數(shù)可能發(fā)生的錯誤。通過Result<T,E>,Rust其實給開發(fā)人員指明了一條錯誤處理的道路,使代碼更加健壯。
場景
- 服務器端處理api請求的框架:Rocket
- 服務器端處理數(shù)據(jù)持久化的框架:tokio_postgres
在api請求的框架中,我把返回類型定義成了 Result<T, rocket::response::status::Custom\<String>>
,即錯誤類型是 rocket::response::status::Custom\<String>
。
在tokio_postgres中,直接使用 tokio_postgres::error::Error
。
即如果要處理錯誤,就必須將 tokio_postgres::error::Error
轉(zhuǎn)換成 rocket::response::status::Custom\<String>
。那么我們從下面的原理開始,逐一領略Rust的錯誤處理方式,通過對比找到最合適的方式吧。
原理
對錯誤的處理,Rust有3種可選的方式
- 使用match
- 使用if let
- 使用map_err
下面我結(jié)合場景,逐一演示各種方式是如何處理錯誤的。
下面的代碼中涉及到2個模塊(文件)。 /src/routes/notes.rs
是路由層,負責將api請求導向合適的service。 /src/services/note_books.rs
是service層,負責業(yè)務邏輯和數(shù)據(jù)持久化的處理。這里的邏輯也很簡單,就是route層調(diào)用service層,將數(shù)據(jù)寫入到數(shù)據(jù)庫中。
使用match
src/routes/notes.rs
#[post("/api/notes", format = "application/json", data = "<note>")] pub async fn post_notes(note: Json<Note>) -> Result<(), rocket::response::status::Custom<String>> { insert_or_update_note(¬e.into_inner()).await }
/src/services/note_book.rs
pub async fn insert_or_update_note( note: &Note, ) -> Result<(), rocket::response::status::Custom<String>> { let (client, connection) = match connect( "host=localhost dbname=notes_db user=postgres port=5432", NoTls, ) .await { Ok(res) => res, Err(err) => { return Err(rocket::response::status::Custom( rocket::http::Status::ExpectationFailed, format!("{}", err), )); } }; ... match client .execute( "insert into notes (id, title, content) values($1, $2, $3);", &[&get_system_seconds(), ¬e.title, ¬e.content], ) .await { Ok(res) => Ok(()), Err(err) => Err(rocket::response::status::Custom( rocket::http::Status::ExpectationFailed, format!("{}", err), )), } }
通過上面的代碼我們可以讀出一下內(nèi)容:
- 在service層定義了route層相同的錯誤類型
- 在service層將持久層的錯誤轉(zhuǎn)換成了route層的錯誤類型
- 使用match的代碼量還是比較大
使用if let
/src/services/note_book.rs
pub async fn insert_or_update_note( note: &Note, ) -> Result<(), rocket::response::status::Custom<String>> { if let Ok((client, connection)) = connect( "host=localhost dbname=notes_db user=postgres port=5432", NoTls, ) .await { ... if let Ok(res) = client .execute( "insert into notes (id, title, content) values($1, $2, $3);", &[&get_system_seconds(), ¬e.title, ¬e.content], ) .await { Ok(()) } else { Err(rocket::response::status::Custom( rocket::http::Status::ExpectationFailed, format!("{}", "unknown error"), )) } } else { Err(rocket::response::status::Custom( rocket::http::Status::ExpectationFailed, format!("{}", "unknown error"), )) } }
src/routes/notes.rs
#[post("/api/notes", format = "application/json", data = "<note>")] pub async fn post_notes(note: Json<Note>) -> Result<(), rocket::response::status::Custom<String>> { insert_or_update_note(¬e.into_inner()).await }
使用了 if let ...
,代碼更加的別扭,并且在else分支中,拿不到具體的錯誤信息。
其實,不難看出,我們的目標是將api的請求,經(jīng)過route層和service層,將數(shù)據(jù)寫入到數(shù)據(jù)中。但這其中的錯誤處理代碼的干擾就特別大,甚至要有邏輯嵌套現(xiàn)象。這種代碼的已經(jīng)離初衷比較遠了,是否有更加簡潔的方式,使代碼能夠最大限度的還原邏輯本身,把錯誤處理的噪音降到最低呢?答案肯定是有的。那就是map_err
map_err
map_err是Result上的一個方法,專門用于錯誤的轉(zhuǎn)換。下面的代碼經(jīng)過了map_err的改寫,看上去是不是清爽了不少啊。/src/services/note_book.rs
pub async fn insert_or_update_note( note: &Note, ) -> Result<(), rocket::response::status::Custom<String>> { let (client, connection) = connect( "host=localhost dbname=notes_db user=postgres port=5432", NoTls, ) .await .map_err(|err| { rocket::response::status::Custom( rocket::http::Status::ExpectationFailed, format!("{}", err), ) })?; ... let _ = client .execute( "insert into notes (id, title, content) values($1, $2, $3);", &[&get_system_seconds(), ¬e.title, ¬e.content], ) .await .map_err(|err| { rocket::response::status::Custom( rocket::http::Status::ExpectationFailed, format!("{}", err), ) })?; Ok(()) }
src/routes/notes.rs
#[post("/api/notes", format = "application/json", data = "<note>")] pub async fn post_notes(note: Json<Note>) -> Result<(), rocket::response::status::Custom<String>> { insert_or_update_note(¬e.into_inner()).await }
經(jīng)過map_err改寫后的代碼,代碼的邏輯流程基本上還原了邏輯本身,但是map_err要額外占4行代碼,且錯誤對象的初始化代碼存在重復。在實際的工程項目中,service層的處理函數(shù)可能是成百上千,如果再乘以4,那多出來的代碼量也不少啊,這會給后期的維護帶來不小的壓力。
那是否還有改進的空間呢?答案是Yes。
Rust為我們提供了From<T> trait,用于類型轉(zhuǎn)換。它定義了從一種類型T到另一種類型Self的轉(zhuǎn)換方法。我覺得這是Rust語言設計亮點之一。
但是,Rust有一個顯示,即實現(xiàn)From<T> trait的結(jié)構,必須有一個在當前的crate中,也就是說我們不能直接通過From<T>來實現(xiàn)從 tokio_postgres::error::Error
到 rocket::response::status::Custom<String>
。也就是說下面的代碼編譯器會報錯。
impl From<tokio_postgres::Error> for rocket::response::status::Custom<String> {}
報錯如下:
32 | impl From<tokio_postgres::Error> for rocket::response::status::Custom<String> {}
| ^^^^^---------------------------^^^^^----------------------------------------
| | | |
| | | `rocket::response::status::Custom` is not defined in the current crate
| | `tokio_postgres::Error` is not defined in the current crate
| impl doesn't use only types from inside the current crate
因此,我們要定義一個類型 MyError
作為中間類型來轉(zhuǎn)換一下。/src/models.rs
pub struct MyError { pub message: String, } impl From<tokio_postgres::Error> for MyError { fn from(err: Error) -> Self { Self { message: format!("{}", err), } } } impl From<MyError> for rocket::response::status::Custom<String> { fn from(val: MyError) -> Self { status::Custom(Status::ExpectationFailed, val.message) } }
/src/services/note_book.rs
pub async fn insert_or_update_note( note: &Note, ) -> Result<(), rocket::response::status::Custom<String>> { let (client, connection) = connect( "host=localhost dbname=notes_db user=postgres port=5432", NoTls, ) .await .map_err(MyError::from)?; ... let _ = client .execute( "insert into notes (id, title, content) values($1, $2, $3);", &[&get_system_seconds(), ¬e.title, ¬e.content], ) .await .map_err(MyError::from)?; Ok(()) }
src/routes/notes.rs
#[post("/api/notes", format = "application/json", data = "<note>")] pub async fn post_notes(note: Json<Note>) -> Result<(), rocket::response::status::Custom<String>> { insert_or_update_note(¬e.into_inner()).await }
而 MyError
到 rocket::response::status::Custom<String>
之間的轉(zhuǎn)換是隱式的,由編譯器來完成。因此我們的錯誤類型的轉(zhuǎn)換最終縮短為 map_err(|err|MyError::from(err))
,再簡寫為 map_err(MyError::from)
。
關于錯誤處理中的類型轉(zhuǎn)換應用解析就到這里。通過分析這個過程,我們可以看到,在設計模塊時,我們應該確定一種錯誤類型,就像tokio_postgres庫一樣,只暴露了tokio_postgress::error::Error一種錯誤類型。這種設計既方便我們在設計模塊時處理錯誤轉(zhuǎn)換,也方便其我們的模塊在被調(diào)用時,其它代碼進行錯誤處理。
到此這篇關于Rust中類型轉(zhuǎn)換在錯誤處理中的應用解析的文章就介紹到這了,更多相關Rust錯誤處理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Rust 中的閉包之捕獲環(huán)境的匿名函數(shù)
這篇文章介紹了Rust編程語言中的閉包,包括閉包的定義、使用、捕獲環(huán)境中的變量、類型推斷與注解、與函數(shù)的比較以及實際應用,閉包具有捕獲環(huán)境、類型推斷和高效性等特性,是Rust中一個非常強大的工具,感興趣的朋友一起看看吧2025-02-02