本內容是對 RustConf Chian 2025系列演講中 邁向易用的Rust 內容的翻譯與整理。推薦點擊鏈接觀看原視頻。
你好,我住在温哥華。今天我要談的是學習 Rust 的難點、Rust 工具鏈的現狀、語言可能的演進路徑、現有機制,以及我將提出的一個可用於演進 Rust 語言的潛在新增機制。
先簡單介紹一下我自己。我從 2015 年開始使用 Rust,2016 年開始為項目做貢獻,2017 年起成為 Rust 編譯器團隊成員。我的主要關注點是讓語言和工具鏈儘可能易用,讓工具鏈能以人類的方式與人溝通,而不僅僅是以編譯器工程師的方式。這轉化為我在診斷信息方面投入了大量精力。
很多人在考慮是否用 Rust 做項目或開始學習 Rust 時都會問我:Rust 究竟有多容易上手?這個問題比看起來複雜,因為它背後還有更基礎的問題。Rust 到底要讓什麼變得容易?讓某件事更容易,可能會使另一件事變得更難。還要問:對誰來説容易?你期望的用户是誰?為資深工程師設計的語言,與為初學者設計的語言,需求非常不同。Scratch、Python、Java 的目標都不一樣。最後,還得問:是否應該“容易”?這是個有點古怪的問題,但如果為了“容易”而犧牲了你項目的其他目標,可能反而會把你帶到一個並不想去的地方。
要正確回答這些問題,你必須理解語言的目標,並且知道 Rust 是什麼,這樣才能判斷“Rust 是否容易使用”。Rust 是一個靈活的系統編程語言,能夠表達類似於更動態語言的高級概念。這句引言經常被提起,今天早些時候你也看過:“Rust 是一門讓每個人都能構建可靠且高效軟件的語言。”我們都知道什麼是編程語言,不需要展開。“賦能”(empowering)意味着語言的主要目標之一是讓人們能做成事,我們要確保每一位用户都能完成任務。“每個人”(everyone)這個詞沒有限定,我們不把自己限制於某類用户,我們希望無論背景、領域、經驗水平,都能支持。當然,我們永遠不可能在所有方面都做到“屬於每個人”,但我們可以朝這個方向努力。接下來是“構建可靠且高效的軟件”。這與“每個人”的目標存在間接衝突。為了可靠與高效,Rust 所做的許多設計與“易學性”、與“易重構性”直接衝突。這就是我想討論的問題的一部分。
Rust 對自己施加了一些限制:它需要無處不在地運行。在我看來,Rust 的潛在使用範圍極廣:你可以寫跑在微控制器上的代碼,也可以寫跑在瀏覽器裏的代碼,幾乎涵蓋兩者之間的所有場景。能做到這一點的語言非常少。但如果有某個語言特性會妨礙這種靈活性,我們就無法擁有它。Rust 佔據了非常特定的定位:AOT(提前編譯)、無 GC、保證內存安全,同時採納了許多現代語言的設計敏感性。其他語言都會在上述某一點上做權衡,這是有理由的。滿足這些點意味着你必須承擔額外的複雜性。對我們而言,這是值得的。
因為 Rust 專注於性能、可靠性、生產力,任何不符合這些明確目標的潛在特性都會被拒絕。那些會阻礙從其他語言(比如 Swift)遷移者入門的設計,如果對這些目標有明確收益,仍可能被採納;同時我們會盡可能採取緩解措施,提高其易用性。Rust 有個我認為應該更常被討論的核心精神:它是一門“契約”的語言。
舉個例子:在 Rust 中表達泛型有兩種方式。其一是類型參數,經單態化處理---也就是對傳入的每個具體類型,編譯器都會生成一份獨立函數,最終二進制裏會有對應類型數量的代碼副本。其二是 trait object(dyn trait),通過 vtable 指針間接調用,帶來另一組權衡:二進制可能更小、代碼複雜度可能降低,但會有性能成本。根據你做的事,你可能會選擇其一或其二。對比 Java:在語義上“萬物皆 trait object”,經由 vtable 間接調用。但 Java 有 JVM 和 JIT,會插裝代碼,觀察哪些泛型重複出現,然後執行等效於前者的單態化以提速。複雜性被轉移到實現側(JVM 工程師承擔),語言本身則較少暴露表達力。這只是一個例子,展示 Rust 如何把並非對所有人都“有用”的複雜性暴露出來。Rust 可能讓人產生誤解:複雜性始終存在。你可以寫看起來很高層的代碼,甚至把一段 Python 代碼幾乎逐字翻譯也能跑通,但語言與語義的複雜性一直都在。一旦偏離“易路”,你就會直面“難點”。例如閉包在外觀上類似其他語言,但一旦涉及借用並嘗試執行或傳遞閉包,你可能立刻遭遇借用檢查器錯誤,且在不重寫代碼結構的情況下難以甚至無法解決。這就是當你違反了代碼中表達的契約時會發生的事。
當前語言狀態比 2015 年 5 月 1.0 發佈時好太多了。當時甚至不能鏈式調用方法。一些因其他特性而“理應存在”的特性後來被補上,比如關聯常量:常量在 1.0 裏有,但 trait 裏的常量沒有,很快就補齊了。這種演進持續進行中。順帶一提,Rust 的易用性不只是語言本身,還包括文檔、工具鏈、庫,以及編譯器診斷是否可讀可懂。正如所説,語言狀態不是靜止的:我們前進、改進。時間推移帶來更多文檔、更多以易用性為導向的庫、更好的診斷與語言演化。今天學 Rust 的體驗,會比一年後學 Rust 更難一些。
關於語言如何演進,機制多樣:我們有 RFC 流程(請求評議),有模板和一系列章節,説明特性是什麼、為什麼需要、納入語言的代價、如何教授該特性、考慮過但放棄的設計等。這個流程至今帶着我們前進,也會繼續存在並帶我們走向未來,但它非常聚焦於“具體特性”。我們還有 MCP(重大變更提案),更偏戰術層面,用於重構編譯器或修改某個特性的底層行為,只要不產生用户可見影響。還有之前提到的“項目目標”(project goals),我會稍微展開。它們最終都會匯聚成 nightly 特性,並最終通過 RFC 穩定。我們有一個倉庫存放所有發佈過的 RFC,有模板可循。但這一切並非從那裏開始,它從你開始。Rust 的特性不會憑空出現,必須有人提出需求,我們才會考慮和討論。舉個例子:項目成員 Copsol 發現一個特性可以讓“newtype 模式”(不展開,挺簡單)更易用。他不是從寫 RFC 開始的,而是先與他人交流、吸收反饋,寫成自己的文字,然後發成博客,再發一個 pre-RFC(非正式的“我有個想法”)到 internals 論壇供大家討論。經過一輪輪交流後,他提交了 RFC,被接受,如今已實現。這個過程不是隻有項目成員才能做。只要你有具體用例,就可以推動。最終由語言團隊、編譯器團隊等項目團隊討論並決定是否契合。
MCP 更偏內部,你無須太瞭解,它是項目成員之間就將要重構的事情進行輕量博弈的流程。至於項目目標,Niko 在 2023 年開始公開討論,它用於表達那些不能整齊落入單一特性裏的訴求。項目目標以 6 個月為週期進行組織,我們把目標彙集成主題,當前主題分為四大類,其中兩三類都與易用性相關。在主題之下是項目目標,再由項目目標落到 RFC,最終實現。本質上,這是一套我們如何談論特性、如何談論語言變更與演進的共同話語體系。這一切最終轉化為 nightly 特性。這裏不是窮舉,只列一些我想提的例子。nightly 特性所處的演進階段不一:可能實現快好了,即將穩定;也可能我們還不確定當前實現是否正確、語義是否理想,甚至不確定這個特性是否適合語言。我會講一堆特性,並非都必然進入語言,我只是用它們來探索一些想法。
“在模式中使用引用”(references in patterns):如果你熟悉 Scala,這相當於 unapply,允許對需要解引用才能匹配的指針進行模式匹配。比如我們匹配一個字符串字面量,但枚舉變體裏是 String。今天你做不到,必須把 match 改寫成一串 if let 才能表達同樣邏輯。再比如“結構體字段默認值”:這已經在 nightly,將來可能穩定,表達“哪些字段必填、哪些可選”。
“自動克隆”(auto cloning):看這段代碼,今天會不會(通過)編譯?當然不會,是 move 錯誤。因為一旦消費了 Arc,它就不可用。編譯器今天會提示你要麼改函數簽名避免消費,要麼 clone。對 Arc 來説,90% 的時候你可能確實想 clone。我們可以讓這段代碼“自動為你工作”。
“部分 self 借用”:很常見,多個字段同時訪問,當前無法改變契約來表達而不觸發借用檢查器的抱怨。“生成器”(generators)等我就略過了,都是同一主題的變體:編譯器能做而今天沒有做的事情。比如“返回值類型推斷”:編譯器實際上已經會推斷,並給你建議“函數體是整數,請寫上返回類型”。
我們可以把它變成語言的一部分,但對任何公開 API 來説會是大號"腳槍"(譯者注: “big foot gun”是一個比喻,意思是這項技術可能會帶來潛在的重大風險或問題,尤其是在公共API(應用程序編程接口)中使用時。可能暗示着這種功能容易被濫用或導致錯誤)。
同理,更“超前”的是類型參數推斷,不再顯式寫出。你可能會點頭説這很糟糕,Rust 不需要這些。你是對的,很多特性 Rust 並不需要,也永遠不會發生。但如果我們有一個不被 Rust 既定要求束縛的東西呢?Rust 需要服務從微控制器到 Web 服務、到分佈式系統、到前端應用的廣泛場景。但這並非從一開始就是註定的。如果我們放寬某些限制,或許能得到一個“本質上 99% 是 Rust”的語言,但對一大類用例來説更易用。名字不重要,隨便起。為了表達概念,我暫且稱之為“Easy Rust”。
重要的是這個概念,早就被討論過很多次,我不是第一個,也不會是最後一個。Without Boats (譯者注: 知名Rust程序員,有相關公開演講 RustLatam 2019 - Without Boats: Zero-Cost Async IO) 過去也提過一些想法。我想表達的是我的立場,因為人們説“我想要更容易的 Rust”時,心中所指未必相同。
回到之前的問題:為了什麼更容易?給誰更容易?我個人對“Easy Rust”的限制是:它仍然是 Rust,仍與 Rust 互操作;從 crates.io 拿一個 Rust 庫就應該能用;它應允許表達在 Rust 中原本很難表達的東西;它必須是“有內容”的。就我個人而言,凡是編譯器可以給出建議的錯誤,都應該在 Easy Rust 中降級為警告,讓你繼續開發。但它仍然是一門“獨立的語言”,我們討論的是 Easy Rust,而不是把 Rust 本身改成這樣。比如,編譯器可以對返回類型做推斷,今天它只是提示;Easy Rust 會直接接受。再舉一個我想過的更偏工具鏈的例子:如果你在跑測試套件,而代碼庫某處有類型錯誤,今天所有測試都不會運行,你得先修類型錯誤。如果工具鏈能“容忍”這點,先運行所有不涉及該項的測試,最後把測試錯誤與編譯錯誤一併展示,而不是強迫你先修掉它---這甚至不是語言層面的改變,而是工具鏈上的改進。
讓 Rust 變難的不全是語言本身。還有很多讓代碼更易用的機制:庫的寫法、庫是否過多 trait 綁定、是否過度複雜、是否有好文檔。通過寫更易用的庫,或利用宏填補一些空白,今天就能改進,無需新語言。但有了新語言,我們可以走得更遠。並行存在 Rust 與 Easy Rust,對 Rust 有一個明確好處:為用户提供一條“限制更少”的學習路徑;也為重構提供機會。你有一個 Rust 項目,可以把工具鏈切到“這是 Easy Rust”,進行重構、探索 API 變化、打磨最終產物,然後再切回 Rust,處理需要清理的點。
它適用於哪些人?如前所述:重構的人、在學語言的人、在探索 API 長相的人,以及像我們大多數人一樣---對性能並不關心的人。如果 Rust 沒有 GC,但有枚舉與模式匹配---那就是我最喜歡、也是我最常用的 Rust 部分。
當然,“第二種語言”的提案也有問題:它會增加工具鏈複雜度。如果第二種語言集成進 rustc,就意味着編譯器工程師要面對另一種變體。我們已經有處理語言分歧的機制---“版本增量”(edition)系統,但今天的 edition 完全是“基於時間”的。我建議用同一機制來表達:把 Easy Rust 作為一種“為期一年的 Rust 版本增量”。在這一年內,我們提供完整的向後兼容(nightly 特性做不到),但會快速演進這門語言,探索那些我們對納入 Rust 持謹慎態度的特性。這也讓我們能觀察 nightly 特性在"野外"的真實使用,及早識別“其實沒問題”的東西,或“確實有問題”的東西,再考慮是否納入 Rust。因為這是“不同語言、不同穩定性保證”,我們可以更快、更大膽地演化。
還有其他問題:其他生態也嘗試過,“更易用的第二語言”的市場定位會讓一些人反感---“我不要輔助輪,我只要真語言”。但這不是二元對立,而是漸變:它既是更快寫出高性能代碼的一條路,也是通往 Rust 本體的良好上坡道。如前所述,我們已有許多演進機制,但我在此提議增加一門“語言”。
我希望大家參與進來,即便不是為了這門 Easy Rust。參與 RFC 流程,參與 internals 論壇上的討論,參與 RFC 線程。如果你需要某些特性,請發聲。我的時間到了,非常感謝。