回調賦予我們很好的函數處理能力,那麼非同步回調則是在非同步編程中的靈魂,這是不可缺少的存在,本章主要是關於Rust中的trait的說明,希望對你有所幫助 ...
wmproxy
wmproxy
已用Rust
實現http/https
代理, socks5
代理, 反向代理, 靜態文件伺服器,四層TCP/UDP轉發,七層負載均衡,內網穿透,後續將實現websocket
代理等,會將實現過程分享出來,感興趣的可以一起造個輪子
項目地址
國內: https://gitee.com/tickbh/wmproxy
github: https://github.com/tickbh/wmproxy
關於 ++trait++
trait是Rust
中的概念,類似於其他語言中的介面(interface)
。
在Rust
中不存在繼承的概念,所有關於結構體的拓展功能全部均由trait
來代替。比如std::io::Read
這是一個關於io的trait
,在TcpStream
中和在File
中均實現了該功能,這樣子如果上層只關心讀操作的,我們就可以將其轉化成std::io::Read
的一個對象,比如io: std::io::Read
,後面我們也可以把他包裹成BufferReader
等實現功能的轉化。
為什麼不在Rust中使用繼承
- 繼承破壞了封裝性,父類的改變會影響子類。如果父類的出現發生變化,所有繼承自該父類的子類都需要相應地進行修改,這會增加代碼的維護成本。
- Rust追求記憶體安全和無數據競爭,繼承不利於編譯器進行靜態檢查。
- 繼承關係的耦合度高。子類和父類之間是緊密耦合的關係,這會影響代碼的靈活性和可移植性。
- 繼承往往被過度使用,導致子類與父類功能緊密耦合。
下麵舉下例子,設計關於車的通用基類,能跑能停等
public class BaseCar {
//... 省略其他屬性和方法...
public void run() { //... }
public void stop() { //... }
}
一開始自行車都很完美,接下來設計摩托車,摩托車需要加油,那麼基類被改成
public class BaseCar {
//... 省略其他屬性和方法...
public void run() { //... }
public void stop() { //... }
public void refuel() { //... }
}
但是自行車又沒有加油的需求
// 自行車
public class Bicycle extends BaseCar {
//... 省略其他屬性和方法...
public void refuel() {
throw new UnSupportedMethodException("我不需要加油!");
}
}
如果接下來又有修理引擎的介面,那基類又得加repairEngine
的介面。自行車繼承這個基類將會產生嚴重的負擔,不繼承又得重新寫一些關於基礎能力的函數,又會增加重覆代碼。
那麼接下來是以trait
方案的實現
pub trait Base {
fn run(&self);
fn stop(&self);
}
pub trait Refuel {
fn refuel(&mut self);
}
pub trait RepairEngine {
fn repair_engine(&mut self);
}
那麼自行車只需要實現Base
能力,然後摩托車在自行車的基礎上實現Refuel
及RepairEngine
即可實現解耦。
非同步的trait
在程式中均使用的是非同步(async)
編程,那麼我們可能需要將trait實現成:
pub trait Base {
async fn run(&self);
async fn stop(&self);
}
當我們如此寫的時候編譯器就會提示我們:
functions in traits cannot be declared `async`
`async` trait functions are not currently supported
consider using the `async-trait` crate: https://crates.io/crates/async-trait
see issue #91611 <https://github.com/rust-lang/rust/issues/91611> for more informationrustcClick for full compiler diagnostic
原來非同步的trait實現還沒有進入到stable
階段,暫時只能有預覽版即nightly
版本進行使用。
那麼本文將探討該功能在未stable
前如何實現非同步的trait
。
假如返回一個非同步的Future
trait Base {
type FetchData<'a>: std::future::Future<Output = String> + 'a where Self: 'a;
fn run<'a>(&'a self) -> Self::FetchData<'a>;
}
那麼實現自行車的函數將為:
trait Base {
type FetchData<'a>: /* 將要何種類型呢?? */;
fn run<'a>(&'a self) -> Self::FetchData<'a>;
}
我們嘗試過各種類型,編譯器都無法通過編譯,所以我們需要進行返回值的修改,我們將通過運行時類型擦除來實現。
首先,我們可以通過用 擦除 future 類型來避免編寫 future 類型。以上面的例子為例,你可以這樣寫你的特征:dyn
trait Base {
fn run<'a>(&'a self) -> Pin<Box<dyn Future<Output = String> + Send + '_>>;
}
那麼實現將為:
impl Base for Bicycle {
fn run<'a>(&'a self) -> std::pin::Pin<Box<dyn std::future::Future<Output = String> + Send + '_>> {
Box::pin(async {
"ok".to_string()
})
}
}
可以看出整個函數非常的冗餘,相當的讓人難受。
那麼此時我們可以藉助async-trait
的巨集處理庫,他將幫我們自動處理掉無用的數據,那麼我們的代碼將變成如下:
#[async_trait]
trait Base {
async fn run(&self) -> String;
}
#[async_trait]
impl Base for Bicycle {
async fn run(&self) -> String {
"ok".to_string()
}
}
當然現在此方法會造成額外的開銷,像Box
,Send
等都會造成一定的性能損失,如果要零損失實現非同步還可以嘗試以下方案
手動實現Poll
需要零開銷或在no_std上下文中工作的特征還有另一種選擇:它們可以從 Future 特征中獲取輪詢的概念,並將其直接構建到它們的界面中。如果 future 已完成,並且 future 正在等待其他事件,則該方法將返回。Future::poll
,Poll::Ready(Output)
,Poll::Pending
pub trait Base {
type Item;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>
) -> Poll<Option<Self::Item>>;
}
當然控制Poll的方式相當的麻煩,只要在對性能要求極高的情況下在進行此操作。
預期的官方實現
在最新的Beta或者nightly版本中可以用#![feature(async_fn_in_trait)]
來啟用該能力,那麼我們就可以如下編程:
#![feature(async_fn_in_trait)]
trait Base {
async fn run(&self) -> String;
}
impl Base for Bicycle {
async fn run(&self) -> String {
"ok".to_string()
}
}
這樣子就和普通的實現沒有什麼差別了。
實現該功能的難點
理論上來說,一個非同步只有你在調用await的時候他才會真正的被調用,如果在此前有引用對話的存在,那麼他的生命周期管理才是比較麻煩的存在。
小結
當前的Rust版本為1.74.0
,好消息的是當前async trait
已經Beta Channel
了,如果不出意外的話下一次發佈版本的穩定版將會擁有該能力了。該功能的官方實現將會給非同步編程的帶來極大的方便。讓async/await
能力越來越強。預期2023年末就可以直接使用了。下一章節我們將講async trait
在項目中的應用。
點擊 [關註],[在看],[點贊] 是對作者最大的支持