rust为什么显示不了国服_捋捋 Rust 中的 impl Trait 和 dyn Trait
緣起
一切都要從年末換工作碰上特殊時期, 在家閑著無聊又讀了幾首詩, 突然想寫一個可以瀏覽和背誦詩詞的 TUI 程序說起. 我選擇了 Cursive 這個 Rust TUI 庫. 在實現時有這么一個函數, 它會根據參數的不同返回某個組件(如 Button, TextView 等). 在 Cursive 中, 每個組件都實現了 View 這個 trait, 最初這個函數只會返回某個確定的組件, 所以函數簽名可以這樣寫
fn some_fn(param: SomeType) -> Button隨著開發進度增加, 這個函數需要返回 Button, TextView 等組件中的一個, 我下意識地寫出了類似于下面的代碼
fn some_fn(param1: i32, param2: i32) -> impl View {if param1 > param2 {// do something...return Button {};} else {// do something...return TextView {};} }可惜 Rust 編譯器一如既往地打臉, Rust 編譯器報錯如下
--> srcmain.rs:19:16| 13 | fn some_fn(param1: i32, param2: i32) -> impl View {| --------- expected because this return type... ... 16 | return Button {};| --------- ...is found to be `Button` here ... 19 | return TextView {};| ^^^^^^^^^^^ expected struct `Button`, found struct `TextView`error: aborting due to previous errorFor more information about this error, try `rustc --explain E0308`.從編譯器報錯信息看函數返回值雖然是 impl View 但其從 if 分支推斷返回值類型為 Button 就不再接受 else 分支返回的 TextView. 這與 Rust 要求 if else 兩個分支的返回值類型相同的特性一致. 那能不能讓函數返回多種類型呢? Rust 之所以要求函數不能返回多種類型是因為 Rust 在需要在 編譯期確定返回值占用的內存大小, 顯然不同類型的返回值其內存大小不一定相同. 既然如此, 把返回值裝箱, 返回一個胖指針, 這樣我們的返回值大小可以確定了, 這樣也許就可以了吧. 嘗試把函數修改成如下形式:
fn some_fn(param1: i32, param2: i32) -> Box<View> {if param1 > param2 {// do something...return Box::new(Button {});} else {// do something...return Box::new(TextView {});} }現在代碼通過編譯了, 但如果使用 Rust 2018, 你會發現編譯器會拋出警告:
warning: trait objects without an explicit `dyn` are deprecated--> srcmain.rs:13:45| 13 | fn some_fn(param1: i32, param2: i32) -> Box<View> {| ^^^^ help: use `dyn`: `dyn View`|= note: `#[warn(bare_trait_objects)]` on by default編譯器告訴我們使用 trait object 時不使用 dyn 的形式已經被廢棄了, 并且還貼心的提示我們把 Box<View> 改成 Box<dyn View>, 按編譯器的提示修改代碼, 此時代碼 no warning, no error, 完美.
但 impl Trait 和 Box<dyn Trait> 除了允許多種返回值類型的之外還有什么區別嗎? trait object 又是什么? 為什么 Box<Trait> 形式的返回值會被廢棄而引入了新的 dyn 關鍵字呢?
埋坑
impl Trait 和 dyn Trait 在 Rust 分別被稱為靜態分發和動態分發. 在第一版的 Rust Book 這樣解釋分發(dispatch)
When code involves polymorphism, there needs to be a mechanism to determine which specific version is actually run. This is called ‘dispatch’. There are two major forms of dispatch: static dispatch and dynamic dispatch. While Rust favors static dispatch, it also supports dynamic dispatch through a mechanism called ‘trait objects’.即當代碼涉及多態時, 需要某種機制決定實際調用類型. Rust 的 Trait 可以看作某些具有通過特性類型的集合, 以上面代碼為例, 在寫代碼時我們不關心具體類型, 但在編譯或運行時必須確定 Button 還是 TextView. 靜態分發, 正如靜態類型語言的"靜態"一詞說明的, 在編譯期就確定了具體調用類型. Rust 編譯器會通過單態化(Monomorphization) 將泛型函數展開.
假設 Foo 和 Bar 都實現了 Noop 特性, Rust 會把函數
fn x(...) -> impl Noop展開為
fn x_for_foo(...) -> Foo fn x_for_bar(...) -> Bar(僅作原理說明, 不保證編譯會這樣展開函數名).
通過單態化, 編譯器消除了泛型, 而且沒有性能損耗, 這也是 Rust 提倡的形式, 缺點是過多展開可能會導致編譯生成的二級制文件體積過大, 這時候可能需要重構代碼.
靜態分發雖然有很高的性能, 但在文章開頭其另一個缺點也有所體現, 那就是無法讓函數返回多種類型, 因此 Rust 也支持通過 trait object 實現動態分發. 既然 Trait 是具有某種特性的類型的集合, 那我們可以把 Trait 也看作某種類型, 但它是"抽象的", 就像 OOP 中的抽象類或基類, 不能直接實例化.
Rust 的 trait object 使用了與 c++ 類似的 vtable 實現, trait object 含有1個指向實際類型的 data 指針, 和一個指向實際類型實現 trait 函數的 vtable, 以此實現動態分發. 更加詳細的介紹可以在
Exploring Dynamic Dispatch in Rust?alschwalm.com看到. 既然 trait object 在實現時可以確定大小, 那為什么不用 fn x() -> Trait 的形式呢? 雖然 trait object 在實現上可以確定大小, 但在邏輯上, 因為 Trait 代表類型的集合, 其大小無法確定. 允許 fn x() -> Trait 會導致語義上的不和諧. 那 fn x() -> &Trait 呢? 當然可以! 但鑒于這種場景下都是在函數中創建然后返回該值的引用, 顯然需要加上生命周期:
fn some_fn(param1: i32, param2: i32) -> &'static View {if param1 > param2 {// do something...return &Button {};} else {// do something...return &TextView {};} }我不喜歡添加額外的生命周期說明, 想必各位也一樣. 所以我們可以用擁有所有權的 Box 智能指針避免煩人的生命周期說明. 至此 Box<Trait> 終于出現了. 那么問題來了, 為什么編譯器會提示 Box<Trait> 會被廢棄, 特地引入了 dyn 關鍵字呢? 答案可以在 RFC-2113 中找到.
RFC-2113 明確說明了引入 dyn 的原因, 即語義模糊, 令人困惑, 原因在于沒有 dyn 讓 Trait 和 trait objects 看起來完全一樣, RFC 列舉了3個例子說明.
第一個例子, 加入你看到下面的代碼, 你知道作者要干什么嗎?
impl SomeTrait for AnotherTrait impl<T> SomeTrait for T where T: Another你看懂了嗎? 說實話我也看不懂 : ) PASS
第二個例子, impl MyTrait {} 是正確的語法, 不過這樣會讓人以為這會在 Trait 上添加默認實現, 擴展方法或其他 Trait 自身的一些操作. 實際上這是在 trait object 上添加方法.
如在下面代碼說明的, Trait 默認實現的正確定義方法是在定義 Trait 時指定, 而不應該在 impl Trait {} 語句塊中.
trait Foo {fn default_impl(&self) {println!("correct impl!");} }impl Foo {fn trait_object() {println!("trait object impl");} }struct Bar {}impl Foo for Bar {}fn main() {let b = Bar{};b.default_impl();// b.trait_object();Foo::trait_object(); }Bar 在實現了 Foo 后可以通過 b.default_impl 調用, 無需額外實現, 但 b.trait_object 則不行, 因為 trait_object 方法是 Foo 的 trait object 上的方法.
如果是 Rust 2018 編譯器應該還會顯示一條警告, 告訴我們應該使用 impl dyn Foo {}
第三個例子則以函數類型和函數 trait 作對比, 兩者差別只在于首字母是否大寫(Fn代表函數trait object, fn則是函數類型), 難免會把兩者弄混.
更加詳細的說明可以移步
RFC-2113?github.com.
總結
impl trait 和 dyn trait 區別在于靜態分發于動態分發, 靜態分發性能 好, 但大量使用有可能造成二進制文件膨脹; 動態分發以 trait object 的概念通過虛表實現, 會帶來一些運行時開銷. 又因 trait object 與 Trait 在不引入 dyn 的情況下經常導致語義混淆, 所以 Rust 特地引入 dyn 關鍵字, 在 Rust 2018 中已經穩定.
引用
以下是本文參考的資料
impl Trait for returning complex types with ease?doc.rust-lang.orgimpl trait 社區跟蹤?github.comrust-lang/rfcs?github.comTraits and Trait Objects in Rust?joshleeb.comDynamic vs. Static Dispatch?lukasatkinson.deExploring Dynamic Dispatch in Rust?alschwalm.comPS: 題圖為盧浦大橋, 全上海我最喜歡的大橋, 沒有之一~
總結
以上是生活随笔為你收集整理的rust为什么显示不了国服_捋捋 Rust 中的 impl Trait 和 dyn Trait的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: redis key失效的事件_《分享几道
- 下一篇: 金蝶kls软件教程(金蝶kls标准版使用