進一步簡化 Rust 中的自我參照型別

https://blog.yoshuawuyts.com/self-referential-types-2/

總結:Rust 的自我參照類型進一步簡化
在最後一個帖子中,我談到了我們如何可能在 Rust 中引入符合人體工程學的自我參照類型(SRTs),主要是通過引入我們知道我們無論如何都想要的功能。所列出的功能有:

某種形式的 ‘unsafe 和 ‘self 生命週期。

Rust 的安全出指針表示法(超 let / -> super Type)。

引入出指針而不會破壞向後相容的方式。

可以用來標記類型為不可移動(!Move)的新 Move 自動 Trait。

可以安全地初始化自我參照類型的視圖類型。

這篇帖子得到了很好的回應,後續的討論我覺得相當有趣。我學到了一些東西,我認為這將有助於進一步完善設計,我認為寫出來會很好。

不是所有的自我參照類型都是不可移動的

Niko Matsakis 指出,不是所有的自我參照類型都一定是 !Move。例如:如果被參照的數據是堆分配的,那麼該類型實際上不一定是 !Move。寫協議解析器時,對數據進行堆分配讀取實際上是相當常見的。看來有相當多的自我參照類型實際上並不需要是 !Move 或者根本不需要任何移動概念就能工作。這也意味著我們不需要某種 super let / -> super Type 來就地構造類型。

如果我們只想要為堆分配的類型啟用自我參照,那麼我們需要的只是一種初始化它們的方式(視圖類型)和能描述自我生命週期的能力(至少 ‘unsafe)。這應該讓我們很好地知道我們可以從哪裡開始優先啟用一個有限形式的自我參照。

‘self 生命週期是不夠的

說到生命週期,Mattieum 指出’self 可能還不夠。’self 指向整個結構,這實際上對實用性來說過於粗糙。我們需要能夠指向單個字段來描述生命週期。

顯然 Niko 也已經提出了一個這方面的功能,這是基於位置的生命週期。與我們用來關聯值的抽象生命週期 ‘a 不同,這是更好的,如果引用總是有隱式獨特的生命週期名字。擁有這種功能後,我們應該從上一篇帖子中重寫動機示例,使其基於位置:

這個變動在本示例中可能不太重要;但一旦引入可變性,情況就迅速惡化了。不使用魔法的’self 而是始終要求’self.field 看起來似乎更好。這需要基於位置的生命週期,這無論如何似乎都是一個很好的想法。

自動參照穩定性

在本帖的一開始,我們已經確定對於將值存儲在堆中的自我參照類型,我們實際上不需要編碼為!Move。這並不是所有的自我參照類型 – 但描述了其中相當多的部分。現在,如果我們對於幾乎整個剩餘的自我參照類型都不需要編碼!Move,會怎樣呢?

如果這聽起來像是移動構造函數,你是對的 – 但有一個問題!與我在上一篇帖子中描述的 Relocate 特性不同,DoveOfHope 指出我們甚至可能不需要這樣做。畢竟:如果編譯器已經知道我們正在指向結構中包含的字段 – 當我們嘗試移動該結構時,編譯器不能確保更新指針嗎?

直到我讀到基於位置的生命週期,我對這種可能性才產生了懷疑。有了這個,似乎我們會有足夠的細微度來知道在進行移動時如何更新哪些字段。在成本方面:這僅僅是在移動值時更新指針值 – 這實際上是免費的。這幾乎完全消除了我們需要編碼!Move 的需要。

這樣做不涵蓋的唯一情況是 ‘unsafe 參考或實際 *const T / *mut T 指向堆棧數據。編譯器實際上不知道它們指向什麼,因此無法在移動時對它們進行更新。對此,某種形式的 Relocate 特性實際上似乎會很有用。但這也不是必須立即添加的東西。

Relocate 可能應該接受&own self

即使我們並不希望人們需要編寫自己的指針更新邏輯,這實際上仍應該提供。而且當我們這樣做時,我們應該正確地進行編碼。Nadrieril 非常有幫助地指出,Relocate 特性上的&mut self 限制可能實際上並不是我們想要的 – 因為我們並不只是借用一個值 – 我們實際上想銷毀它。相反,他們向我們介紹了向&own 所做的工作,它將允許使用所謂的 “擁有引用”。

Daniel Henry-Mantilla 是 stackbox crate 的作者,也是 stdlib 中 pin!宏背後的生命週期擴展系統的主要負責人。他曾經分享過一份關於&own 的非常有幫助的講解。這個想法的核心是,我們應該將 “數據在哪裡被具體存儲?” 的概念與 “誰擁有這些數據?” 的概念分離出來,最終產生了具有以下含義的引用:

T 的語義

用於後端分配

&T

共享訪問

借用

&mut T

獨占訪問

借用

&own T

擁有訪問
(放棄責任)

借用

將這個應用到我們的帖子中,我們將使用這個來改變 Relocate 特性,讓它不再接受&mut self,而是接受&own self,這將取得對一個類型的永久獨占訪問,並且實際上可以銷毀這個類型。

這實際上需要在固定的內存位置構造類型,這個特性將需要某種形式的 -> super Self 語法。畢竟:這將是唯一一個仍然需要它的地方。對於任何對於保持對&own 方法感興趣的人,這裡是相應的 Rust 問題(也恰好由 Niko 提交)。

動機示例,再次重做

有了這些想法,我們可以再次重做上一篇帖子中的動機示例。為了刷新每個人的記憶,這是我們將進行去糖化的高階異步/.await-based 的 Rust 代碼:

在本帖中,我們還可以將其轉換,這次不需要引用 Move 或就地構造,這要歸功於基於位置的生命週期和編譯器自動保持引用穩定性:

這在其定義中顯著地比我們以前所擁有的更加簡單。甚至實際剝離調用最終也變得更加簡單,不再需要中間的 IntoFuture 構造函數來保證就地構造。

僅僅需要的是編譯器在移動時更新 self 指針的地址。這僅僅是在值被移動時進行一點額外的代碼生成 – 與其說它是一份位拷貝,不如說它需要更新指針值。但這似乎是可以實現的,應該表現得非常好,最重要的是:用戶很少(如果有的話)需要考慮這一點。寫起來 &’self.field 將總是有效。

在仍然需要不可移動類型時

我並不想完全否定不可移動類型的想法。擁有不能移動的類型這一點肯定是有好處的。尤其是當需要使用要求不可移動性的 FFI 結構時。或者一些高性能的數據結構,它們使用了大量對堆棧數據的自我參照,進行更新會太昂貴。這種用例當然是存在的,但它們可能會相當特殊。例如:Rust for Linux 對於使用其內的不可移動類型進行了很多的優化,我認為這些可能需要某種形式的不可移動性來實際工作。

然而,如果編譯器不需要不可移動類型來提供自我參照,那麼不可移動類型突然不再是支持負荷,而是更接近於一種優化。這可能還是值得添加它們,因為它們無疑是更有效率的。但如果我們做得對,添加不可移動類型將是向後相容的,並且將是一種我們後來可以引入作為優化的東西。

當涉及到異步{}應該返回 impl Future 還是 impl IntoFuture 時:我認為答案真的應該是 impl IntoFuture。在 2024 年版中,正在將範圍語法(0..12)從返回 Iterator 更改為返回 IntoIterator。這與 Swift 的行為一致,其中 0..12 返回的是 Sequence 而不是 IteratorProtocol。我認為這表明異步{}和 gen{}可能也應該返回 impl Into* trait 而不僅僅是它們各自的 trait。

總結

當我的某個作品被討論並且我能夠得到其他相關工作的信息時,我是很喜歡的。我認為為了實現自我參照類型,我現在絕對更喜歡作為語言一部分的內置指針更新,而不是不可移動類型。然而,如果我們確實需要不可移動類型 – 我認為我上一篇帖子提供了一個有條理並且好用的設計,可以讓我們達到目標。

如果我們要實現一個完整的自我參照類型方案,就會有相當多的依賴。幸運的是,我們可以逐步實現功能,逐步啟用越來越多表達力更強的自我參照類型形式。在重要性方面:某種 ‘unsafe 隱然無暇似乎是一個很好的起點。接著是基於位置的生命週期。視圖類型似乎很有用,但並不是必不可少的,因為我們可以通過使用選項進行階段性初始化來解決。這是所有功能及其依賴的圖表。

這樣分解這些功能實際上進一步鞏固了我對這一切似乎是很可行的看法。 ‘unsafe 似乎並不遙遠。而 Niko 似乎對基於位置的生命週期和視圖類型有些正式的感覺。我們將不得不看看這些東西在實際中到底會如何迅速地得到開發 – 但是我像這樣把所有這些都展現出來後,我覺得有些樂觀!

via Yosh Wuyts — Blog

July 8, 2024 at 06:58PM

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *