Rust 語言中,為了內部的scope能夠讀取外部的變數,同時外部變數不交出所有權,必須經常性地使用借用運算。然而,每次借用出來的新變數本質上都是舊變數的指針。這樣增加了訪問變數的開銷和類型檢查的成本。是否可以效仿C++中的引用摺疊,使得引用的引用仍然指向原變數,指針層數不增加?或者更進一步,處於同一個函數的棧中,變數在棧中的位置是已知的,此時我們沒必要用一個指針存放變數,我們只需要讓編譯器把借用出的新變數當作是舊變數的別名,這樣就省下了訪問時解引用的開銷。同樣,所有函數只有一個上級模塊,所以讓函數對上級模塊中變數的借用都返回別名也是可以的。類似的還有閉包的捕獲。


"每次借用出來的新變數本質上都是舊變數的指針"

其實並不是這樣,示例: https://play.rust-lang.org/?version=stablemode=debugedition=2015gist=d6bff660f62cba7a5efce07f4aad755f


Rust中的借用檢查只發生在編譯階段,編譯完成之後就沒有生命週期之類的信息。

一般來說,Rust的編譯過程為(省略了部分):Plain Text -&> Token Stream -&> AST -&> HIR -&> MIR -&> LLVM IR -&> ...

早期Rust的借用檢查是基於HIR的,但也因此有很多"誤殺",時不時需要手動套一層大括弧用於規避。

Rust 2018 edition將默認開啟NLL(None Lexical Lifetimes),它的借用檢查基於MIR實現,進而能夠提供更加細粒度的借用檢查。


想當然之後的補充:

準確的說,T確實是指向T的指針,並且兩個指針的值是不一樣的,但是編譯期有自動的deref,所以只要不是手滑非要指定類型為,一般編譯器會自動幫你合併,也就沒啥區別了。

題主的補充(拜服.jpg)

對於泛型函數,類型是泛型的形參是沒有"解引用強制多態"的。也就是對於 fn f&(t:T){...}這樣的函數,t如果是T類型,不會在傳參是被處理成T。這樣可能導致生成新的具體化類型函數,也會導致引用層數的增加。


說實話,完全沒理解你問的是什麼問題。

第一,為什麼你非要在指針的基礎上再取借用,而不是直接在原來的變數基礎上借用?指針層數是完全可以自己控制的,怎麼會每次借用都增加一層?

第二,「讓編譯器把借用出的新變數當作是舊變數的別名,這樣就省下了訪問時解引用的開銷」這說明你把語義和優化搞混了。一般講解Rust的書籍,說的都是語義,它定義了什麼樣的代碼是編譯器可以接受的,以及執行後的效果如何。但它從來沒有定義編譯器必須怎麼生成機器碼。實際上,在你說的這種簡單情況下,編譯器已經自動做了這種優化,完全沒有額外解引用操作。這種屬於優化層面的事情,就不要跟表層的語言特性混到一起了。

第三,「解引用強制多態」這個翻譯我也是頭一回見到。看了你的鏈接才知道,是 deref coercion 的翻譯。然而 coercion 的意義是強制轉換,跟多態毛關係都沒有啊。多態是 polymorphism 這兩單詞一點關係都沒有啊,這個多態從何而解?


整個題目描述都是建立在錯誤的假設上面,不深入分析了。直接說題目下方評論裏那段代碼,修復掉語法錯誤後應該是這樣的:

fn f3&(t: T) {
println!("{:p}", t as *const T);
}

fn f2&(t: T) {
println!("{:p}", t as *const T);
f3(t); // mark
}

fn f1&(t: T) {
println!("{:p}", t as *const T);
f2(t); // mark
}

fn main() {
let x = String::new();
f1(x);
}

上面有 mark 注釋的兩行犯了低級錯誤,在 ref 的基礎上又取了借用,這時候傳入 f2/f3 函數的參數類型不是 T 而是 T,相當於你手動增加了一層。正確的寫法應該是:

fn f2&(t: T) {
println!("{:p}", t as *const T);
f3(t); // mark
}

fn f1&(t: T) {
println!("{:p}", t as *const T);
f2(t); // mark
}

之前的代碼能編譯通過是因為在 deref coercions 的機制下編譯器會自動插入正確數量的 dereference operator, 在 rust 裏就是符號 * , 所以下面幾行都是等價的

fn f1&(t: T) {
println!("{:p}", t as *const T);
f2(t); // same as f2(***t)
f2(t); // same as f2(**t)
f2(t); // same as f2(*t)
f2(t);
}

那在正確的寫法下面 (@DCjanus 回答裏的代碼示例) 這些短函數會被 inline,編譯後也不會有所謂的多層指針這種情況,運行後輸出的地址是一樣的。這裡跟函數是普通函數還是泛型函數沒有關係,上面代碼裏泛型函數會被特化成入參為 str 類型的實現。

而 * 的使用對應 Deref trait,題目裏的錯誤寫法不會做你所期待的優化,會破壞語義,所以輸出後地址都不同這裡是沒問題的。


推薦閱讀:
查看原文 >>
相關文章