性完全指南:從 OOP 繼承思維到 Trait 驅(qū)動(dòng)的泛型與動(dòng)態(tài)分發(fā))
Rust 多態(tài)性完全指南從 OOP 繼承思維到 Trait 驅(qū)動(dòng)的泛型與動(dòng)態(tài)分發(fā)【免費(fèi)下載鏈接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust本文基于 Google Android 團(tuán)隊(duì)的 Rust 培訓(xùn)課程comprehensive-rust中 idiomatic/polymorphism 一章整理而成。該章節(jié)旨在幫助具備 Java、C 等 OOP 背景的開(kāi)發(fā)者快速理解 Rust 獨(dú)特的多態(tài)機(jī)制如何用 trait 表達(dá)行為契約、用泛型實(shí)現(xiàn)編譯期單態(tài)化、用dyn Trait實(shí)現(xiàn)運(yùn)行時(shí)動(dòng)態(tài)分發(fā)以及為什么 Rust 拋棄了繼承而選擇組合。讀完本文你將掌握 Rust 多態(tài)代碼的編寫(xiě)范式、繼承替代方案與異構(gòu)集合的實(shí)現(xiàn)思路并能在實(shí)際項(xiàng)目中做出正確的泛型與動(dòng)態(tài)分發(fā)取舍。一、Rust 多態(tài)機(jī)制全景泛型與 trait 對(duì)象的交匯Rust 的多態(tài)機(jī)制與 Java、C 等主流語(yǔ)言有明顯差異它沒(méi)有繼承、沒(méi)有虛函數(shù)表驅(qū)動(dòng)的默認(rèn)多態(tài)而是圍繞trait 泛型 trait 對(duì)象三件套構(gòu)建。課程在 polymorphism.md 中給出了一個(gè)濃縮所有核心概念的示例pub trait Trait {} pub struct HasGenericT(T); pub enum EitherA, B { Left(A), Right(B), } fn takes_genericT: Trait(value: T) {} fn takes_dyn(value: dyn Trait) {}這段代碼揭示了 Rust 多態(tài)的兩條核心路徑編譯期多態(tài)靜態(tài)分發(fā)takes_genericT: Trait通過(guò)泛型參數(shù)加 trait bound 實(shí)現(xiàn)調(diào)用時(shí)由編譯器為每個(gè)具體類(lèi)型生成專(zhuān)屬代碼單態(tài)化運(yùn)行時(shí)多態(tài)動(dòng)態(tài)分發(fā)takes_dyn(value: dyn Trait)通過(guò) trait 對(duì)象實(shí)現(xiàn)函數(shù)接收的是指向任何實(shí)現(xiàn)了Trait的類(lèi)型的引用具體行為在運(yùn)行時(shí)通過(guò) vtable 解析。HasGenericT與EitherA, B則展示了泛型在類(lèi)型定義層的應(yīng)用前者是任意單類(lèi)型包裝后者是經(jīng)典的二選一標(biāo)簽聯(lián)合sum type二者都保持類(lèi)型信息的靜態(tài)可知性。本章節(jié)接下來(lái)將先從 trait 與泛型的基礎(chǔ)Refresher講起再過(guò)渡到從 OOP 思維遷移到 Rust 的組合式設(shè)計(jì)From OOP to Rust兩部分內(nèi)容分別組織在 refresher.md 與 from-oop-to-rust.md 中。二、復(fù)習(xí)trait 是 Rust 多態(tài)的基石2.1 Traits、協(xié)議與接口編譯期檢查的靜態(tài)鴨子類(lèi)型Rust 的多態(tài)與泛型體系重度建立在 trait 之上。在 refresher/traits.md 中課程給出了一個(gè)直觀的示例定義Receivertrait再讓兩個(gè)完全不同的類(lèi)型分別實(shí)現(xiàn)它。trait Receiver { fn send(self, message: str); } struct EmailAddress(String); impl Receiver for EmailAddress { fn send(self, message: str) { println!(Email to {}: {}, self.0, message); } } struct ChatId { uuid: [u8; 16], } impl Receiver for ChatId { fn send(self, message: str) { println!(Chat message sent to {:?}: {}, self.uuid, message); } }課程對(duì) trait 給出了兩個(gè)高價(jià)值的心智模型靜態(tài)鴨子類(lèi)型static duck typing鴨子類(lèi)型源于 Python 等動(dòng)態(tài)語(yǔ)言——如果它走路像鴨子、叫起來(lái)像鴨子那它就是鴨子。trait 同樣指定行為而非具體類(lèi)型但額外提供了編譯期檢查類(lèi)型是否真的具備所需行為在編譯時(shí)就已被驗(yàn)證命題與證明propositions and proofstrait 是一組關(guān)于類(lèi)型的命題集合trait 中的必需方法就是需要履行的義務(wù)為一個(gè)類(lèi)型實(shí)現(xiàn) trait補(bǔ)全這些方法就是提交了該類(lèi)型可用于任何要求此 trait 的場(chǎng)合的證明。2.2 泛型上的 trait bound聲明最小可用行為trait 最常見(jiàn)的用法是作為泛型類(lèi)型參數(shù)的 bound。在 refresher/trait-bounds.md 中課程解釋了其必要性沒(méi)有 trait bound 的泛型參數(shù)無(wú)法調(diào)用任何方法因?yàn)榫幾g器不知道類(lèi)型具備什么行為bound 的作用是聲明一個(gè)類(lèi)型要想在泛型代碼中工作至少需要具備哪些行為。use std::fmt::Display; fn print_with_lengthT: Display(item: T) { println!(Item: {}, item); println!(Length: {}, item.to_string().len()); } fn main() { let number 42; let text Hello, Rust!; print_with_length(number); // 整數(shù)可以 print_with_length(text); // 字符串也可以 }這里T: Display即最小可行行為任何實(shí)現(xiàn)了Display的類(lèi)型都能傳入42與Hello, Rust!均可復(fù)用同一份函數(shù)邏輯。2.3 單態(tài)化編譯期的代碼生成與二進(jìn)制體積權(quán)衡在 refresher/monomorphization.md 中課程揭示了泛型的底層機(jī)制每一個(gè)帶泛型的函數(shù)或類(lèi)型實(shí)例都會(huì)在編譯時(shí)被轉(zhuǎn)換成一份針對(duì)具體類(lèi)型的獨(dú)立版本泛型在運(yùn)行時(shí)并不存在存在的只有具體類(lèi)型。fn print_vecT: std::fmt::Debug(debug_vec: VecT) { for item in debug_vec { println!({:?}, item); } } fn main() { let ints vec![1u32, 2, 3]; let floats vec![1.1f32, 2.2, 3.3]; // 實(shí)例一Vecu32 - () print_vec(ints); // 實(shí)例二Vecf32 - () print_vec(floats); }單態(tài)化帶來(lái)幾點(diǎn)需要權(quán)衡的工程事實(shí)性能紅利編譯期就確定了具體類(lèi)型沒(méi)有運(yùn)行時(shí)查找開(kāi)銷(xiāo)優(yōu)化空間大內(nèi)聯(lián)、常量折疊等都能針對(duì)具體類(lèi)型進(jìn)行成本二進(jìn)制體積與編譯時(shí)間會(huì)上升按需付費(fèi)單態(tài)化只針對(duì)最終程序或動(dòng)態(tài)庫(kù)中實(shí)際使用到的類(lèi)型實(shí)例展開(kāi)未被使用的泛型實(shí)例不會(huì)產(chǎn)生代碼何時(shí)需要關(guān)注在瀏覽器內(nèi) WebAssembly 或嵌入式系統(tǒng)等體積敏感場(chǎng)景中應(yīng)謹(jǐn)慎設(shè)計(jì)泛型的使用方式課程在此章節(jié)未展開(kāi)體積優(yōu)化的具體手段只提示了影響面。2.4 Sized 與 ?Sized靜態(tài)大小與動(dòng)態(tài)大小類(lèi)型在 refresher/sized.md 中課程介紹了區(qū)分編譯期已知大小與運(yùn)行時(shí)已知大小類(lèi)型的能力use std::fmt::Debug; pub struct AlwaysSizedT /* : Sized */(T); pub struct OptionallySizedT: ?Sized(T); type Dyn1 OptionallySizeddyn Debug;關(guān)鍵規(guī)則Sizedtrait 由所有編譯期已知大小的類(lèi)型自動(dòng)實(shí)現(xiàn)且默認(rèn)自動(dòng)添加到每個(gè)未顯式退出opt-out的類(lèi)型參數(shù)上——這就是T默認(rèn)是Sized的原因[T]、str、dyn Trait屬于動(dòng)態(tài)大小類(lèi)型DST其大小作為引用的一部分被存儲(chǔ)切片引用含長(zhǎng)度trait 對(duì)象引用含 vtable 指針只有通過(guò)?Sized顯式放寬 bound類(lèi)型參數(shù)才可能接收 DST。2.5 默認(rèn)方法實(shí)現(xiàn)用最小義務(wù)換取完整能力在 refresher/default-impls.md 中課程展示了 trait 設(shè)計(jì)中必需方法 默認(rèn)實(shí)現(xiàn)的經(jīng)典模式pub trait CollectLeaves { type Leaf; // 必需方法實(shí)現(xiàn)者必須提供 fn collect_leaves_buffered(self, buf: mut VecSelf::Leaf); // 默認(rèn)實(shí)現(xiàn)基于必需方法派生 fn collect_leaves(self) - VecSelf::Leaf { let mut buf vec![]; self.collect_leaves_buffered(mut buf); buf } }要點(diǎn)方法體存在即為默認(rèn)實(shí)現(xiàn)它可基于 trait 內(nèi)其他方法或 supertrait 的方法編寫(xiě)標(biāo)準(zhǔn)庫(kù)中的典型例子是Ord實(shí)現(xiàn)者只需提供核心的compare/cmpmax、min、clamp等都以默認(rèn)實(shí)現(xiàn)形式給出默認(rèn)方法可以被 derive 宏覆蓋因?yàn)?derive 宏會(huì)在實(shí)現(xiàn)中生成任意 AST。2.6 條件方法實(shí)現(xiàn)把約束放在 impl 而非類(lèi)型定義上在 refresher/conditional-methods.md 中課程介紹了對(duì)泛型類(lèi)型按參數(shù)能力分條件開(kāi)放方法的寫(xiě)法// 類(lèi)型定義上不加任何 trait bound pub struct ValueT(T); // 把 bound 放在 impl 塊上 implT: std::fmt::Display ValueT { fn log(self) { println!({}, self.0); } } // 或者用 where 子句 implT ValueT { fn log_error(self) where T: std::error::Error, { eprintln!({}, self.0); } }課程強(qiáng)調(diào)這是對(duì)有序集合等希望內(nèi)部類(lèi)型總是Ord場(chǎng)景的首選做法不要把這些約束放在類(lèi)型定義上否則該類(lèi)型每次以泛型參數(shù)出現(xiàn)時(shí)都會(huì)被迫承擔(dān)約束影響下游所有使用點(diǎn)把約束放在 impl 上既能維持不變量又不會(huì)污染類(lèi)型定義本身。上面代碼中的兩種寫(xiě)法等價(jià)where子句在約束復(fù)雜時(shí)更易讀。三、從 OOP 到 Rust為什么沒(méi)有繼承以及替代方案3.1 OOP 繼承速覽在 from-oop-to-rust/inheritance.md 中課程用一段 C 代碼幫助讀者回顧傳統(tǒng)繼承// 基類(lèi) class Vehicle { public: void accelerate() { } void brake() { } }; // 繼承類(lèi) class Car : public Vehicle { public: void honk() { } }; int main() { Car myCar; // 創(chuàng)建 Car 對(duì)象 myCar.accelerate(); // 繼承的方法 myCar.honk(); // Car 自己的方法 myCar.brake(); // 繼承的方法 return 0; }繼承的機(jī)制要點(diǎn)子類(lèi)型獲得父類(lèi)型的字段與方法方法可按需被重寫(xiě)子類(lèi)可通過(guò)super調(diào)用父類(lèi)方法。繼承是 OOP 范式成功的關(guān)鍵幾十年來(lái)無(wú)數(shù)業(yè)務(wù)邏輯圍繞它構(gòu)建——這也是 from-oop-to-rust.md 提出的問(wèn)題顯得尖銳的原因既然繼承如此成功為什么 Rust 要回避它3.2 繼承的三大缺陷from-oop-to-rust/why-no-inheritance.md 給出了 Rust 拒絕繼承的理由并用一段 Rust 代碼直觀對(duì)比了想要繼承與用組合實(shí)現(xiàn)的寫(xiě)法差異pub struct Id { pub id: u32 } impl Id { // 方法 } // ? Rust 沒(méi)有繼承 // pub struct Data: Id { // pub name: String, // } // ? 組合把 Id 作為字段 pub struct Data { pub id: Id, pub name: String, } impl Data { // Data 自身的、非來(lái)自 trait 的方法 } impl SomeTrait for Data { // trait 實(shí)現(xiàn)放在獨(dú)立的 impl 塊中 }課程列出的繼承三大缺陷默認(rèn)異構(gòu)Heterogeneous by default類(lèi)繼承隱式允許不同類(lèi)的類(lèi)型互換使用卻無(wú)法指定具體類(lèi)型或判斷兩個(gè)類(lèi)型是否相同。在相等性、比較等操作上這會(huì)導(dǎo)致運(yùn)行時(shí)拋錯(cuò)甚至 panic 的相等比較數(shù)據(jù)結(jié)構(gòu)的多事實(shí)來(lái)源類(lèi)型字段被繼承層級(jí)遮蔽方法可能覆蓋父類(lèi)或被子類(lèi)覆蓋——在多方維護(hù)的復(fù)雜代碼庫(kù)中很難判斷一個(gè)類(lèi)型的真實(shí)行為默認(rèn)動(dòng)態(tài)分發(fā)帶來(lái) vtable 開(kāi)銷(xiāo)動(dòng)態(tài)分發(fā)需要一處存儲(chǔ)該調(diào)用哪個(gè)方法等運(yùn)行時(shí)信息即值的vtable。方法調(diào)用因此比編譯期已知類(lèi)型的直接調(diào)用多出若干次解引用。3.3 組合優(yōu)于繼承用字段拼裝類(lèi)型from-oop-to-rust/composition.md 展示了替代方案——不是 mixin 也不是繼承而是通過(guò)創(chuàng)建不同類(lèi)型字段來(lái)組合類(lèi)型pub struct Uuid([u8; 16]); pub struct Address { street: String, city_or_province: String, code: String, country: String, } pub struct User { id: Uuid, address: Address, }課程對(duì)組合的評(píng)述非常務(wù)實(shí)優(yōu)點(diǎn)開(kāi)發(fā)者對(duì)類(lèi)型做什么、能訪問(wèn)什么擁有完全的控制力與清晰度缺點(diǎn)字段訪問(wèn)的易用性有所下降需要多一層.派生 trait 時(shí)的注意事項(xiàng)使用 derive 宏時(shí)務(wù)必確保 struct 的所有字段類(lèi)型或 enum 的所有變體類(lèi)型都已實(shí)現(xiàn)對(duì)應(yīng) trait——derive 宏通常假設(shè)組成新類(lèi)型的各個(gè)成員類(lèi)型已經(jīng)實(shí)現(xiàn)了該 trait。四、實(shí)踐建議如何在項(xiàng)目中正確選擇多態(tài)方案結(jié)合課程 polymorphism.md 及各子章節(jié)可以將 Rust 多態(tài)方案的選擇歸納為以下決策要點(diǎn)異構(gòu)集合用 trait 對(duì)象需要在一個(gè)容器如VecBoxdyn Trait中存放多種類(lèi)型時(shí)使用dyn Trait課程在 from-oop-to-rust.md 中將如何在 Rust 中表示異構(gòu)集合列為核心議題之一熱點(diǎn)代碼優(yōu)先泛型追求性能、希望內(nèi)聯(lián)與靜態(tài)優(yōu)化時(shí)用帶 trait bound 的泛型接受單態(tài)化帶來(lái)的編譯時(shí)間與二進(jìn)制體積成本monomorphization.md體積敏感場(chǎng)景警惕泛型WebAssembly、嵌入式開(kāi)發(fā)中注意泛型實(shí)例的展開(kāi)數(shù)量同上約束優(yōu)先放 impl泛型類(lèi)型的約束盡量通過(guò)條件方法實(shí)現(xiàn)表達(dá)避免污染類(lèi)型定義conditional-methods.md復(fù)用行為用默認(rèn)實(shí)現(xiàn)設(shè)計(jì) trait 時(shí)用必需方法 基于它的默認(rèn)實(shí)現(xiàn)降低實(shí)現(xiàn)者負(fù)擔(dān)default-impls.md領(lǐng)域建模用組合用字段組合替代繼承層級(jí)保持類(lèi)型事實(shí)來(lái)源單一composition.md。五、延伸閱讀本主題在課程中位于 idiomatic/polymorphism.md 一章其下還有更多進(jìn)階小節(jié)如dynamic-dispatch目錄下的dyn-vs-generics、heterogeneous、dyn-compatible以及sealed-traits、supertraits、blanket-impls、orphan-rule等。在本倉(cāng)庫(kù)中可以繼續(xù)研讀復(fù)習(xí)部分全部小節(jié)refresher.mdtraits、trait-bounds、monomorphization、sized、default-impls、conditional-methods 等OOP 遷移部分全部小節(jié)from-oop-to-rust.mdwhy-no-inheritance、composition、problem-solving、switch-perspective、sealed-traits 等若想系統(tǒng)補(bǔ)強(qiáng)泛型與 trait 基礎(chǔ)可配合課程早期章節(jié) generics 與 methods-and-traits 一起學(xué)習(xí)?!久赓M(fèi)下載鏈接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考