In this episode of Crust of Rust, we go over static and dynamic dispatch in Rust by diving deep into generics, monomorphization, and trait objects. As part of that, we also discuss what exactly the Sized trait is, what it's for, and how it interacts with Dynamically Sized traits.
開始建置 Rust 專案 :
先看到一個簡單的例子 :
兩個函式做相同的事,但不完全等價。兩者皆為泛型函式,且都接收可以轉成參考的參數。
編譯器會為兩種不同輸入型別的 strlen 產生程式碼 copys :
這種情況不只發生在函式,也會發生在型別上面,例如泛型於 key 和 value 的 HashMap,編譯器會給你完整的結構、以及你使用到的方法的 copys。編譯器只會根據使用到的參數型別來產生 copy,並不會為所有可能的型別來產生 copy,這也是為什麼你編譯完之後並移交給別人作為函式庫使用很困難。
假設現在分別傳入 key 型別為 String 型別和 key 為 number 型別到 HashMap。實際上,當編譯器產生 HashMap 的每個方法的程式碼時,它會 inline 每個 key 型別的 hash function的定義。因此,使用 String 的 HashMap 程式碼將直接將 String 的 hash 程式碼放入 HashMap 程式碼中,並且可以根據該型別是 String 的事實最佳化該程式碼。同樣地,對於 number 之類的東西,它可能能夠完全跳過 hash,因為它意識到一旦對所有泛型進行了 monomorphizes,實際上 hash 只是數字的值。這可能實際上並不準確,但你可以理解,編譯器可以看到用於特定型別的具體程式碼,進而更好地進行最佳化。
編譯器會為泛型或泛函產生多個 copys 的程式碼也是有它的缺點,除了二進位檔比較大,還有程式可能變得比較沒有效率的 side effect,因為多個 copys 意味著 instruction cache 可能無法利用到 locality 的好處。
Q: Is that one of the primary reasons Rust binaries are larger than C ones?
A : Rust 的二進位檔通常包含更多靜態編譯的內容,這可能是原因之一,還有一部分原因是 monomorphization,另一個非常重要的原因是,Rust 通常內建 debug symbols。如果你不從二進位檔中剝除它們,最終會得到一個實際機器碼很少但具有大量 debug symbols 的二進位檔,因此你可能希望嘗試剝除二進位檔的 debug symbols。
Q : Will the compiler try to inline generated functions ?
A : 這就是為什麼 monomorphization 很好的原因之一,因為編譯器不僅僅是 inline string 或 str,它還可以選擇 inline 函式,並基於此進行最佳化。
Q : big reason for larger size is stdlib
A : 這並不完全正確,因為只有你實際使用的標準函式庫部分最終才會放入你的二進位檔中。不過,如果你從標準函式庫中使用了大量的泛型,這確實會增加二進位檔的大小。
Q : How big of a cost is duplication of methods? Is it negligible?
A : 基本上沒有。Jon 認為實際產生多個函式並不那麼重要,但重要的是如果產生函式的許多 copys,則必須將它們中的每一個都編譯為機器碼,這會減慢編譯過程。
Q : wouldn't strlen_string take &String rather than just String by value?
A : 實際上,它可以接受任一種,兩者都 work,這也是採用 AsRef<str>
的原因之一。
Q : How do dynamic libraries handle generics?
A : 它們不這樣做。這就是為什麼動態連結 Rust 函式庫,甚至只是以二進位形式發佈 Rust 函式庫具有挑戰性的原因之一。而且,如果你希望在 Rust 中使用一個 C 的動態連結函式庫,而這個函式庫有很多像是外部函式之類的功能,那麼這些函式是不能使用泛型的。
使用例子來說明 Static Dispatch :
如果你不想編譯器產生一大堆的 copys,你可以使用 Dynamically Sized Types。
接著用 vector 來說明我們的需求 :
Trait Objects 章節談論了將不同具體型別的事物視為同一型別的能力。該章節使用的例子是 gui,你可能有很多可繪製的東西,它們實作了 Draw trait。你可能想要有一個 draw 函式,它只接受所有要繪製的東西的 list 或 iterator。其中一些可能是按鈕,有些可能是圖片,有些可能是文字方塊,但對於 draw 函式來說,這並不重要。它只關心它得到一個可繪製的東西的 iterator,這在某種程度上與前面的例子相似,前面例子只想要一個可以呼叫 hei 方法的東西的 slice。
嘗試編譯以下程式碼 :
得到以下編譯錯誤 :
接下來的直播將談到我們收到這個錯誤的原因、它的含義、修復方法、Sized 的含義以及它與 dynamic dispatch 和 trait 物件的關係。
先看到 Sized trait,它沒有任何的方法,文件寫道 :
Types with a constant size known at compile time.
大部份的型別都是 Sized,即便是以下結構,它也有一個 implicit 的 bound,要求該型別是 Sized :
回到我們的 strlen 的例子 :
當你要呼叫 strlen,你必須傳入某個參數,strlen 函式接收到的參數必須占用 stack 上的空間或者必須被傳遞到一個暫存器中。這意味著編譯器必須確保產生的組合語言中有為該參數分配所需空間的程式碼,這要求編譯器知道該參數的大小。相似地,如果我們的回傳值也是泛型,編譯器也要知道該配置多少 stack 的記憶體給回傳值使用。
觀察 strlen 的具體實作 :
String 的底層有兩個 numbers,分別是儲存字串的長度以及配置到的記憶體空間,以及一個指標指向位於 heap 的首字元。所以編譯器確實知道 String 的 Sized 是 3 個 usize。
回頭看到剛剛的編譯錯誤 :
我們只有告訴編譯器傳入參數應該要實作 Hei trait,我們並沒有告訴它這個參數的大小是什麼,而且傳入參數可能是 String, str 或其他實作 Hei trait 的型別,而這些型別的大小也都不一樣 (String: 2 個 numbers + 1 個指標, str: 1 個 number + 1 個指標)。
傳入參數的型別 &[dyn Hei]
沒有 Sized。slice 實際上只是一段連續的記憶體,每個區塊的大小相同,它是一個陣列,其中所有元素的大小都相同,但如果我們不知道元素的大小,我們就不能保證元素的大小相同。
當你問我「給我第四個元素」時,通常在一個陣列中,因為所有元素的大小相同,第四個元素就是指向起始點的指標加上一個元素大小的三倍。這是一種指標算術,用來獲得第四個元素。但如果它們的大小不同,你無法產生這樣的程式碼。這就是為什麼 Line 8 說「slice 和陣列的元素必須具有大小型別」,因為否則,該型別就毫無意義。
我們該如何解決這個問題呢?我們真的希望能夠做到這一點。另外,並不是只有陣列會遇到 Sized 未給定的問題 :
上面的程式碼編譯後也會得到跟剛剛一樣的錯誤 :
Q : Can you show us implementing Sized?
A : Sized 對於任何可以實作它的型別都會自動實作。因此,如果你確實喜歡沒有欄位的 foo 結構,那麼它就是 Sized :
如果你為它添加欄位,且全部欄位都是 Sized,foo 就會是 Sized :
即便你加了泛型參數, foo 仍是 Sized :
Q : Is the issue stack SIZE? Isn't it just that you need to have statically known stack frames in order for you to compile efficient function code?
A : 問題不是 stack 是不是夠傳入參數用,而是當你呼叫該函式時,你需要知道為該函式的 stack 變數分配多少空間,當你必須為回傳值分配空間時,該空間應有多大?基本上,這就需要編譯器知道這些空間的大小,它才能知道 stack 指標要移動多少。雖然有一些巧妙的方法可以做到這一點,但基本上,為了產生高效的程式碼,編譯器必須知道這些東西的大小。
Q : What things aren't Sized?
A : 例子如下 :
其它非 Sized 的例子如下 :
我們希望能夠擁有一個不被 monomorphize 的方法,但傳入參數是 not Sized,我們該怎麼做 ?
這裡的技巧是使傳入參數的型別始終是 Sized,並且讓這個型別是 Sized 的型別指向型別是 not Sized 的型別。例如我們可以用到參考,因為參考始終具有相同的 Sized (或者在某些情況下是兩個指標的大小 (fat pointer),我們稍後會談到這一點) :
編譯器始終知道參考的占用的空間多大,不論後面跟著的是什麼型別,因為這只是一個指標。
如果你有一個 Box,也可以讓傳入參數的型別是 Sized :
Box 基本上有類似於指標的欄位,Box 有著 ?Sized
bound :
Q : can you just put Box<dyn Asref<str>>
directly on x
?
A : 可以,如下:
Q : When should we use Box over &?
A : Box 的優勢是 static。即使 caller 的 stack frame 消失後,你也可以繼續使用它。一般都會選 Box 來使用。
Q : Can I then also give the function a reference to the stack ?
A : 可以,如下 :
你可以把它想像成基本上是型別擦除。從技術上講,你理論上可以通過 unsafe 的轉換等手段回溯,但一般來說,一旦將其轉換為 trait 物件,你就消除了它曾經是什麼型別的知識。
如果你做了 Box<dyn AsRef<str>>
以及 &dyn AsRef<str>
這兩件事,你僅保留使用這個 trait 的能力。你抹除了有關具體型別是什麼的所有其他知識。因此,在一個被包裝的 AsRef<str>
上,你唯一能做的事情就是對其呼叫 as_ref()
。
思考編譯器如何產生 say_hei
函式的機械碼 :
我們只能在執行時期才知道 s 的型別 :
必須等到執行時期才知道 s 的型別究竟是 &str 還是 &String。
繼續討論 dynmaic dispatch 和 vtable 的 :
Q : are the vtables themselves statically build at compile time or are they also allocated dynamically?
A : vtables 在編譯時構建,它們是 statically 構建的。指向 vtable 的指標也是 statically 已知的,因為該指標是由 trait 物件的原始構造決定的。
從技術上講你可以即時建造 vtable,因為 s.vtable.hei(s.pointer)
的呼叫中沒有任何內容要求它是 statically 已知的,所以你可以想像手動建立一個 vtable,我們稍後會討論到。
Q : but &str also contains the length of the string slice, no?
A : 是的。所以剛剛例子的第一個指標是指向 &str 而不是 str。
Q : can we debug print that vtable struct somehow?
A : Jon 不知道一種方法可以讓編譯器印出其 vtable 結構,但基本上 vtable 只是對於這是一個 trait 物件的相應 trait 的每個方法,每個成員都是相應的方法,並且該值是該方法的具體型別的實作的指標。
Q : Then why are trait functions that don't take self not object safe? Couldn't the compiler just generate a Fn(*mut Self)
that doesn't use Self?
A : 等等將會談到。
Q : does it construct a new vtable every time we create an instance? like, 2 str heis construct two vtables?
A : 不,vtable 通常是為型別 statically 建構的,它不是dynamically 建構的。
Q : Are identical vtable detected?
A : 編譯器並不會被去重。實際上,編譯器保證 vtables 不是重複的。如果你為兩種不同的型別實作一個 trait,即使它們包含相同的程式碼,它們在原始碼和產生的二進位檔中仍然是不同的位置,因此將具有不同的地址。
Q : So Box<dyn ...>
is a thin pointer that points to a wide pointer that points to the object?
A : 並不是,請看以下說明 :
trait 物件的限制是,不能用多個指標指向不同的 vtable :
編譯器提示錯誤 :
help
有提供替代方法 :
note
:
trait 物件還有另一個限制是 Associated Types :
編譯後會出現以下錯誤 :
編譯器的錯誤訊息是說,當我們傳入一個 &dyn Hei
時,我們實際上需要指定 associated types。我們不能僅僅說我們傳入任何 Hei
,而不考慮其 associated types 是什麼。原因在於 type Name
的資訊無法在 vtable 中獲得,因為 type Name
是一個型別,type Name
在二進位檔中沒有地址,沒有任何我們可以放入 vtable 中的東西。因此,type Name
不能直接用於 trait 物件。
你需要這樣做 :
新增 weird() 函式
編譯得到以下錯誤 :
雖然編譯器有提示我們該怎麼解決,但它並沒有說明為什麼一定要有 &self
參數。修改 say_hei 函式來說明原因 :
因为 weird
函式沒有接收 self
,它沒有接收指向任何東西的指標。我們不能隨便建構一個指向 weird
函式的指標,因為這意味著我們需要有一個有效的 instance 才能有指向weird
函式的指標。上面的程式碼就有點像 Python 的 classmethod :
在這個特定情況下,因為 pub trait Hei
提供了一個預設的 weird() 函式,你可能會認為這不是問題。
如果 &str
裡面也實作了 weird() 函式 :
因為現在沒有 &self
的資訊,所以編譯器無法判斷是要用到預設的 weird() 函式,還是 &str 裡面的 weird() 函式。
但如果我真的想要這樣做怎麼辦? 我不在乎透過 trait 物件呼叫 weird() 函式,我只關心 hei() 函式。所以我希望 Hei trait 是 object safe。如果我有一個 trait 物件 Hei,我不介意無法呼叫它的 weird() 函式,可以這麼做 :
這樣的作法基本上是一種選擇退出 vtable 的方式,這意味著 weird() 函式不應該被放置在 vtable 中,最重要的是不應該透過 trait 物件來呼叫。
即便你將 &self
函式傳入 weird() 函式,weird() 函式仍可以不被放在 vtable 中 :
接著嘗試在 say_hei() 函式裡面呼叫 weird() 函式 :
編譯器如預期的報錯,因為我們本來就沒想要透過 trait 物件來呼叫 weird() 函式 :
你也可以讓整個 trait 不能作為 trait 物件使用 :
選擇禁止 trait 物件的情況很少見。有時人們這樣做是出於向後相容性的原因。比如,如果你知道以後可能會添加非 object safe 的方法,你可能會預先添加這個,但這種情況相當罕見。
Q : does the associated type problem also occur with static dispatch?
A : 不,它不會。原因在於 static dispatch,因為它會被 monomorphize,所以你實際上可以知道任何給定函式實作的具體型別。
當你在 Line 21 呼叫 s 的 weird() 方法,你可以想像 weird() 方法被包含在 trait 物件的 vtable 中,但 s 知道不帶 &self
參數呼叫 weird() 方法。這裡的挑戰在於這有點奇怪,如果呼叫 s 需要型別的 instance,那麼為什麼 weird() 不直接接收 &self
參數呢?
在傳統的例子中,weird() 方法實際上被稱為 new() 方法 :
如果你已經有了該型別的 instance,則不需要先呼叫該方法,它應該直接接受 &self
參數。
Jon 同意這樣的事情也許是可能的,但在實踐中,它通常不是很有用。
Q : Doesn't where Self: Sized also disallow implementing the trait for concrete DSTs?
A : 還是可以實作,如下 :
Q : @jonhoo I meant types such as str
, or [u8]
A : 你不能這麼做,因為 Self
必須是 Sized。
Q : The associated type restriction feels weird because the point of associated types is that only one exists for any given concrete type. So shouldn't the vtable know what they associated type should be?
A : 問題是型別被擦除了,只剩下的就是 vtable,所以你無法從 vtable 中看出具體型別曾經是什麼。
有一個例外情況,那就是 Any trait。Any trait 有一個方法,回傳它曾經是的具體型別的 descriptor。如果你沒有理解這一點,沒關係,可以忽略它。但基本上,有一些方法可以繞過這個問題。但基本上,trait 物件是型別擦除的,你不能保留有關它曾經的型別的訊息。
FromIterator trait 是由 Vec 結構實作的。先觀察 colloect() 函式,由於 collect() 函式要求收集的東西要實作 FromIterator trait,而 FromIterator trait 又是 Vec 結構實作的,所以在呼叫 collect 的時候,會把元素放到 Vec 中 :
接著觀察 FromIterator trait 簽章 :
因為 extend() 要求接收 &self
參數,改用 extend() 的程式來說明。以下程式使用了 trait 物件的泛型函式讓 trait 不是 object safe :
看一下 Extend trait 的簽章 :
接著看 extend() 泛型函式:
繼續觀察 add_true 函式 :
雖然我們傳入的 &mut dyn Extend<bool>
讓 T 為已知,但這個 trait 物件並沒有攜帶 extend() 函式要用到的 I 資訊,我們甚至沒有辦法讓傳入的 trait 物件攜帶這 I 的資訊。當編譯器嘗試產生 Line 3 的程式碼時,並沒有指向適當的 extend() 實作的指標。
因此,編譯器無法為 dyn Extend 產生 vtable,因為它會有無限多個 entry,每個 entry 都對應一個可能的 extend 實作。因此,答案只是 dyn Extend 無法存在,你無法為 extend 創建一個 trait 物件。
Q : I can sort of see why we don't want this, but could rustc add a monomorphized version of Extend::extend for each T it's called with, to each type that implements Extend?
A : 這很誘人,但並不總是可行。標準函式庫中有許多對 extend 的使用,但在依賴該標準函式庫的 crate 中,它們可能會使用更多不同的型別來呼叫 extend。在編譯 crate 時,底層的 crate 中 dyn Extend 的 vtable 與更高層 crate 中 dyn Extend 的 vtable 是不同的,因為你可能想要使用更多可能的迭代器實作。因此,你最終將會得到許多不同的 dyn Extend 的 vtable,這就像是一種組合爆炸的問題。這也意味著你不能將來自標準函式庫的 dyn Extend 傳遞給更高層 crate 中接受 dyn Extend 的物件,因為它們的 vtable 型別是不同的。
要讓 trait 物件是 object safe 需要幾個條件 :
就像我們與 Hei 討論過的一樣,你可能有一些方法是 object safe,它們本身仍然很有用,但你可能還有一堆其他方法會使得該 trait 變成 non object safe。然而,你可能希望將這些方法 include 在那些不會通過 trait 物件的型別中,只因為這樣很方便。
看到 Trait std::iter::Iterator,就可以一堆輕鬆找到 non object safe 的方法 :
既然 Iterator 這麼多 non object safe 的方法,那 Iterator 本身為什麼是 object safe ?
答案是 non object safe 方法都有 Self: Sized
的條件,讓這些方法不要放進 vtable,也就是我們前面在處理 weird() 方法用到的技巧 :
如果擁有一個 trait 物件,則無法呼叫 non object safe 方法。但如果沒有一個 trait 物件,那麼就可以呼叫 non object safe 方法。這種方式可以讓一個 trait 擁有一些很好用的方法,但在不將整個 trait 變成 non object safe 的情況下,non object safe 方法可能不會是 object safe。
Q : Can the receiver be anything that includes self or does it have to be &self
or &mut self
?
A : Object Safety 提到的我們幾乎都有提到 :
Sized
must not be a supertrait. In other words, it must not require Self: Sized
.Self: Sized
,就是 non object safe 方法)&dyn xxx<XXX>
沒地方放常數值。也許你可以常數值放到 vtable,但這樣 vtable 會變很大,而且這樣做會讓 vtable 中的值不再都是函式指標,它們可能是任意 Sized 的東西。)Self
except in the type of the receiver.Q : Should library writers always consider adding where Self: Sized
to non-object-safe methods just in case someone downstream wants to use it as a trait object?
A : 是的。這有點取決於你的 trait 是否可用。所以如果你的 trait 作為一個 trait 物件是不可用的,比如說 clone,那麼你當然可以為 clone 方法添加 where Self: Sized
條件,這樣人們就可以創建一個 trait 物件的 clone,但是他們就無法呼叫在 main 函式呼叫該 trait了,所以這可能不值得。通常來說,如果你有一個 trait ,在這個 trait 中有用,即使你只能呼叫 object safe 方法,那麼放棄其他方法可能是有道理的,這樣整個 trait 就是 object safe 的。
Q : did iterator last have the sized restriction? why, given next doesn't?
A : last() 不允許在參考背後進行操作,因為傳入參數不是 &self
。last() 方法會耗用 self,這意味著它接受 self,而 self 是一個 dyn Iterator,它是非固定大小的,而函式參數必須是固定大小的,因此 last() 方法不能透過 trait 物件呼叫。
&mut dyn Drop
,或者技術上是以 Box 方式在卸除,但效果相同。s.as_ref()
。當這個函式返回時,我們會卸除 s,但是當我們卸除一個 Box 時,我們必須釋放記憶體,但 dyn AsRef<str>
是一個 trait 物件,所以它唯一的方法就是 as_ref()
。對於這個問題的答案是每個 vtable 都包含 Drop。你可以想像 trait 物件 implicitly 有一個 Drop :
但在實踐中,對於任何 trait 物件,vtable 都包含指向具體型別的 Drop 函式的指標,因為這是必要的。trait 物件技術上還包含一些額外的資訊,它包括具體型別的大小和對齊,這些資訊在裡面是因為對於像是 Box 這樣需要釋放記憶體的情況,這些資訊是必要的,以便傳遞給記憶體配置器以進行釋放記憶體。因此,每個 trait 物件的 vtable 都包括該 trait 的方法、Drop、大小和對齊。通常,你不必考慮這些,但在我們討論這個話題時,了解這一點是值得的。以下型別皆不是 Sized :
要讓型別從 Sized 變成 Sized 的技巧都是用指標去指向它 :
目前 Dynamically Sized Types 在編譯器中有點特殊,因為你需要知道指標是否是寬型的,因此,你自己處理 not Sized 的型別相當困難。就像如果你想自己實作 Box 並指向 not Sized 的型別,這是可能的,但是一旦涉及到轉換和允許 trait 物件等事情,這就變得相當煩人。但最近有一個 RFC 2580-ptr-meta 獲得了通過 :
再看看幾個 RFC 內提到的東西 :
Thin
trait alias. If this RFC is implemented before type aliases are, uses of Thin
should be replaced with its definition.Struct std::task::Waker 有使用動態地建構 table 的方法。
先看到 Waker 的實作 :
繼續追蹤 RawWaker :
接著看到 RawWakerVTable 的 new() :
Q : thought &[u8] had a start pointer and an end pointer, not a length
A : Jon 認為只是 length。
Q : can we make our own types (DSTs)
A : 可以,如下 :
Q : Is Box<[u8]>
the same thing as Vec<u8>
A: 它們不一樣。Vec<[u8]>
有三個 words,分別是指向位於 heap 的 vector 的指標,vector 的長度以及 vector 的容量。當 vector 的長度大於容量時,vector 會長大,但 Box<[u8]>
不能這麼做,[u8]
無法變大。你可以將 Vec<[u8]>
轉成 Box<[u8]>
,也可以將 Box<[u8]>
轉成 Vec<[u8]>
。
Q : Can we talk about the difference between dyn Fn and function pointers (fn)?
A : 它們不一樣,說明如下 :
Q : when would you use dyn Fn over impl Fn?
A : baz 可以傳入 closure,因為 impl Fn()
在某種程度上是對任何是 fn
的泛型函式的語法糖。因此,我們實際上為每種傳入的 closure 型別都得到了 baz 的具體 copy。因此,你也可以輕鬆地傳遞資料,因為它被 monomorphizes 到每個獨立的 closure。
現在的問題變成,dyn Fn
與 impl Fn
使用時機是什麼 ? 答案是,impl Fn
更通用。因為你不需要在指標後面進行間接操作,但你最終會為每個傳入的 closure 型別產生一個 baz 的 copy,這可能會變得相當多。另一個原因是,有時你想要取一個 trait 物件而不是使其成為泛型,因為否則你必須將泛型向上傳遞。
看到 struct 的例子 :
另外還有 trait 的例子 :
Coherence 會在另一個直播談到 (沒找到)。
Q : could runtime trait detection be implemented in the future using a type's vtable?
(Jon 用他的想法理解聊天室的問題 : "你可以接受一個 trait 物件,或者你可以接受一個寬指標並通過查看其 vtable 來確定它實作了哪些 trait")
A : Jon 認為不行。部分原因是因為每個 trait 物件都會產生不同的 vtable,所以並不是對於 &str 或 String 有一個 vtable。對於 String,有許多 vtable,對於每個 dyn Trait,都有一個 String 的 vtable。所以不僅僅是一個 vtable,你可以通過查看它來找出它實作了哪些 trait。例如你有以下函式 :
vtable 中的唯一東西就是你在此處命名的 Trait 上的任何方法。你將不會得到提供的型別的所有可能方法的 vtable,所以 Jon 不認為你可以透過這種方式動態檢測某些東西實作了哪些 trait。
Q : @jonhoo Does calling a dyn Fn involve a double dereference, once for the vtable pointer and once for the actual function pointer within? Or does that get optimized away?
A : Jon 認為會解兩次不同指標的參考,而不是同一個指標被解參考兩次 :
你可以做到類似以下醜醜的比較 vtable 的功能,但建議不要這麼做 :
一般而言,如果你使用實際的泛型參數或 impl trait,那麼在這些情況下,你將獲得更好的最佳化,因為編譯器在編譯時完全了解所有相關型別,並且可以根據實際的具體實作進行共同最佳化。而一旦通過 dynamic dispatch 進行間接呼叫,編譯器將失去一些本應擁有並能夠根據其進行最佳化的資訊。
Q : can you do a quick example of the slice/vec of dyns?
A : 如下 :
Q : Different compilation units can lead to different vtables, I think there's an issue on that somewhere, and even a clippy lint
A : 大致是對的。在編譯 Rust 程式碼時,Rust 可能使用多個獨立的執行緒來編譯同一個 crate,以編譯該 crate 的不同子集,從而加速編譯。但是因為你希望這些獨立的單元完全並行地工作,而不需要太多的同步,它們可能都會遇到,比如,&dyn AsRef<str>
,它們可能都會產生自己的 vtable,因為它們不想協調產生 vtable,因此即使是相同的型別的相同 trait,也可能有多個 vtable。這就是為什麼 invaraint 可能不成立的一個例子。
Any trait 的簽章 :
如果有一個對 Any 的 trait 物件,你可以對其使用 TypeId,因為它被添加到 vtable 中以獲得該值的唯一的 type identifier,然後可以使用該 type identifier 從 dyn Any
downcast 成具體型別,因為我們知道該 type identifier 是什麼,也就是你可以從 dyn Trait
(只要該 Trait 包含 Any trait)轉換為對實際底層型別的參考。