王垠:对 Rust 语言的分析
文章目錄
- 1.變量聲明語法
- 2.變量可以重復(fù)綁定
- 3.類型推導(dǎo)
- 4.動作的“返回值”
- 5.return 語句
- 6.數(shù)組的可變性
- 7.內(nèi)存管理
- 8.完
Rust 是一門最近比較熱的語言,有很多人問過我對 Rust 的看法。由于我本人是一個語言專家,實現(xiàn)過幾乎所有的語言特性,所以我不認(rèn)為任何一種語言是新的。任何“新語言”對我來說,不過是把早已存在的語言特性(或者毛病),挑一些出來放在一起。所以一般情況下我都不會去評論別人設(shè)計的語言,甚至懶得看一眼,除非它歷史悠久(比如像 C 或者 C++),或者它在工作中惹惱了我(像 Go 和 JavaScript 那樣)。這就是為什么這些人問我 Rust 的問題,我一般都沒有回復(fù),或者一筆帶過。
不過最近有點閑,我想既然有人這么熱衷于這種新語言,那我還是稍微湊下熱鬧,順便分享一下我對某些常見的設(shè)計思路的看法。所以這篇文章雖然是在評論 Rust 的設(shè)計,它卻不只是針對 Rust。它是針對某些語言特性,而不只是針對某一種語言。
由于我這人性格很難閉門造車,所以現(xiàn)在我只是把這篇文章的開頭發(fā)布出來,邊寫邊更新。所以你要明白,這只是一個開端,我會按自己理解的進(jìn)度對這篇文章進(jìn)行更新。你看了之后,可以隔一段時間再回來看新的內(nèi)容。如果有特別疑惑的問題,也可以發(fā)信來問,我會匯總之后把看法發(fā)布在這里。
1.變量聲明語法
Rust 的變量聲明跟 Scala 和 Swift 的很像。你用
let x = 8;這樣的構(gòu)造來聲明一個新的變量。大部分時候 Rust 可以推導(dǎo)出變量的類型,所以你不一定需要寫明它的類型。如果你真的要指明變量類型,需要這樣寫:
let x: i32 = 8;在我看來這是丑陋的語法。本來語義是把變量 x 綁定到值 8,可是 x 和 8 之間卻隔著一個“i32”,看起來像是把 8 賦值給了 i32……
變量缺省都是不可變的,也就是不可賦值。你必須用一種特殊的構(gòu)造
let mut x = 8;來聲明可變變量。這跟 Swift/Scala 的 let 和 var 的區(qū)別是一樣的,只是形式不大一樣。
2.變量可以重復(fù)綁定
Rust 的變量定義有一個比其它語言更奇怪的地方,它可以讓你在同一個作用域里面“重復(fù)綁定”同一個名字,甚至可以把它綁定到另外一個類型:
let mut x: i32 = 1; x = 7; let x = x; // 這兩個 x 是兩個不同的變量let y = 4; // 30 lines of code ... let y = "I can also be bound to text!"; // 30 lines of code ... println!("y is {}", y); // 定義在第二個 let y 的地方在 Yin 語言最初的設(shè)計里面,我也是允許這樣的重復(fù)綁定的。第一個 y 和 第二個 y 是兩個不同的變量,只不過它們碰巧叫同一個名字而已。你甚至可以在同一行出現(xiàn)兩個 x,而它們其實是不同的變量!這難道不是一個很酷,很靈活,其他語言都沒有的設(shè)計嗎?后來我發(fā)現(xiàn),雖然這實現(xiàn)起來沒什么難度,可是這樣做不但沒有帶來更大的方便性,反而可能引起程序的混淆不清。在同一個作用域里面,給兩個不同的變量起同一個名字,這有什么用處呢?自找麻煩而已。
比如上面的例子,在下面我們看到一個對變量 y 的引用,它是在哪里定義的呢?你需要在頭腦中對程序進(jìn)行“數(shù)據(jù)流分析”,才能找到它定義的位置。從上面讀起,我們看到 let y = 4,然而這不一定是正確的定義,因為 y 可以被重新綁定,所以我們必須繼續(xù)往下看。30 行代碼之后,我們看到了第二個對 y 的綁定,可是我們?nèi)匀徊荒艽_定。繼續(xù)往下掃,30行代碼之后我們到了引用 y 的地方,沒有再看到其它對 y 的綁定,所以我們才能確信第二個 let 是 y 的定義位置,它是一個字符串。
這難道不是很費事嗎?更糟的是,這種人工掃描不是一次性的工作,每次看到這個變量,你都要疑惑一下它是什么東西,因為它可以被重新綁定,你必須重新確定一下它的定義。如果語言不允許在同一個作用域里面重復(fù)綁定同一個名字,你就根本不需要擔(dān)心這個事情了。你只需要在作用域里面找到唯一的那個 let y = …,那就是它的定義。
也許你會說,只有當(dāng)有人濫用這個特性的時候,才會導(dǎo)致問題。然而語言設(shè)計的問題往往就在于,一旦你允許某種奇葩的用法,就一定會有人自作聰明去用。因為你無法確信別人是否會那樣做,所以你隨時都得提高警惕,而不能放松下心情來。
3.類型推導(dǎo)
另外一個很多人誤解的地方是類型推導(dǎo)。在 Rust 和 C# 之類的語言里面,你不需要像 Java 那樣寫
int x = 8;這樣顯式的指出變量的類型,而是可以讓編譯器把類型推導(dǎo)出來。比如你寫:
let x = 8; // x 的類型推導(dǎo)為 i32編譯器的類型推導(dǎo)就可以知道 x 的類型是 i32,而不需要你把“i32”寫在那里。這似乎是一個很方便的東西。然而看過很多 C# 代碼之后你發(fā)現(xiàn),這看似方便,卻讓程序變得不好讀。在看 C# 代碼的時候,我經(jīng)常看到一堆的變量定義,每一個的前面都是 var。我沒法一眼就看出它們表示什么,是整數(shù),bool,還是字符串,還是某個用戶定義的類?
var correct = ...; var id = ...; var slot = ...; var user = ...; var passwd = ...;我需要把鼠標(biāo)移到變量上面,讓 Visual Studio 顯示出它推導(dǎo)出來的類型,可是鼠標(biāo)移開之后,我可能又忘了它是什么。有時候發(fā)現(xiàn)看同一片代碼,都需要反復(fù)的做這件事,鼠標(biāo)移來移去的。而且要是沒有 Visual Studio,用其它編輯器,或者在 github 上看代碼或者 code review 的時候,你就得不到這種信息了。很多 C# 程序員為了避免這個問題,開始用很長的變量名,把類型的名字加在變量名字里面去,這樣一來反而更復(fù)雜了,卻沒有想到直接把類型寫出來。所以這種形式的類型推導(dǎo),看似先進(jìn)或者方便,其實還不如直接在聲明處寫下變量的類型,就像 Java 那樣。
所以,雖然 Rust 在變量聲明上似乎有更靈活的設(shè)計,然而我覺得 C 和 Java 之類的語言那樣看似死板的方式其實更好。我建議不要使用 Rust 變量的重復(fù)綁定,避免使用類型推導(dǎo),盡量明確的寫出類型,以方便讀者。如果你真的在乎代碼的質(zhì)量,就會發(fā)現(xiàn)大部分時候你的代碼的讀者是你自己,而不是別人,因為你需要反復(fù)的閱讀和提煉你的代碼。
4.動作的“返回值”
Rust 的文檔說它是一種“大部分基于表達(dá)式”的語言,并且給出這樣一個例子:
let mut y = 5; let x = (y = 6); // x has the value `()`, not `6`奇怪的是,這里變量 x 會得到一個值,空的 tuple,()。這種思路不大對,它是從像 OCaml 那樣的語言照搬過來的,而 OCaml 本身就有問題。在 OCaml 里面,如果你使用 print_string,那你會得到如下的結(jié)果:
print_string "hello world!\n";;hello world! - : unit = ()這里,print_string 是一個“動作”,它對應(yīng)過程式語言里面的“statement”。就像 C 語言的 printf。動作通常只產(chǎn)生“副作用”,而不返回值。在 OCaml 里面,為了“理論的優(yōu)雅”,動作也會返回一個值,這個值叫做 ()。其實 () 相當(dāng)于 C 語言的 void。C 語言里面有 void 類型,然而它卻不允許你聲明一個 void 類型的變量。比如你寫
int main() {void x; }程序是沒法編譯通過的(試一試?)。讓人驚訝的是,古老的 C 的做法其實是正確的,這里有比較深入的原因。如果你把一個類型看成是一個集合(比如 int 是機器整數(shù)的集合),那么 void 所表示的集合是個空集,它里面是不含有任何元素的。聲明一個 void 類型的變量是沒有任何意義的,因為它不可能有一個值。如果一個函數(shù)返回 void,你是沒法把它賦值給一個變量的。
可是在 Rust 里面,不但動作(比如 y = 6 )會返回一個值 (),你居然可以把這個值賦給一個變量。其實這是錯誤的作法。原因在于 y = 6 只是一個“動作”,它只是把 6 放進(jìn)變量 y 里面,這個動作發(fā)生了就發(fā)生了,它根本不應(yīng)該返回一個值,它不應(yīng)該可以出現(xiàn)在 let x = (y = 6); 的右邊。就算你牽強附會說 y = 6 的返回值是 (),這個值是沒有任何用處的。更不要說使用空的 tuple 來表示這個值,會引起更大的類型混淆,因為 () 本身有另外的,更有用的含義。
你根本就不應(yīng)該可以寫 let x = (y = 6); 這樣的代碼。只有當(dāng)你犯錯誤或者邏輯不清晰的時候,才有可能把 y = 6 當(dāng)成一個值來用。Rust 允許你把這種毫無意義的返回值賦給一個變量,這種錯誤就沒有被及時發(fā)現(xiàn),反而能夠通過變量傳播到另外一個地方去。有時候這種錯誤會傳播挺遠(yuǎn),然后導(dǎo)致問題(運行時錯誤或者類型檢查錯誤),可是當(dāng)它出問題的時候,你就不大容易找到錯誤的起源了。
這是很多語言的通病,特別是像 JavaScript 或者 PHP 之類的語言。它們把毫無意義或者牽強附會的結(jié)果(比如 undefined)到處傳播,結(jié)果使錯誤很難被發(fā)現(xiàn)和追蹤。
5.return 語句
Rust 的設(shè)計者似乎很推崇“面向表達(dá)式”的語言,所以在 Rust 里面你不需要直接寫“return”這個語句。比如,這個例子里面,你可以直接這樣寫:
fn add_one(x: i32) -> i32 {x + 1 }返回函數(shù)里的最后一個表達(dá)式,而不需要寫 return 語句,這是函數(shù)式語言共有的特征。然而其實我覺得直接寫 return 其實是更好的作法,像這個樣子:
fn foo(x: i32) -> i32 {return x + 1; }編程有一個容易引起問題的作法,叫做“不夠明確”,總想讓編譯器自動去處理一些問題,在這里也是一樣的問題。如果你隱性的返回函數(shù)里最后一個表達(dá)式,那么每一次看見這個函數(shù),你都必須去搞清楚最后一個表達(dá)式是什么,這并不是每次都那么明顯的。比如下面這段代碼:
fn main() {println!("{}", add_one(7)); }fn add_one(x: i32) -> i32 {if (x < 5) {if (x < 10) {// 做很多事...x * 2} else {// 做很多事...x + 1}} else {// 做很多事...x / 2} }由于 if 語句里面有嵌套,每個分支又有好些代碼,而且 if 語句又是最后一個語句,所以這個嵌套 if 的三個出口的最后一個表達(dá)式都是返回值。如果你寫了“return”,那么你可以直接看有幾個“return”,或者拿編輯器加亮一下,就知道這個函數(shù)有幾個出口。然而現(xiàn)在沒有了“return”這個關(guān)鍵字,你就必須把最后那個 if 語句自己看清楚了,找到每一個分支的“最后表達(dá)式”。很多時候這不是那么明顯,你總需要找一下,而且這件事在讀代碼的時候總是反復(fù)做。
所以對于返回值,我的建議是總是明確的寫上“return”,就像第二個例子那樣。Rust 的文檔說這是“poor style”,那不是真的。有一個例外,那就是當(dāng)函數(shù)體里面只有一條語句的時候,那個時候沒有任何歧義哪一個是返回表達(dá)式。
這個問題類似于重復(fù)綁定變量和類型推導(dǎo)的問題,屬于一種“用戶體驗設(shè)計”問題。無論如何,編譯器都很容易實現(xiàn),然而不同樣式的代碼,對于人類閱讀的工作量,是很不一樣的。很多時候最省人力的做法并不是那種看來最聰明,最酷,打字量最少的辦法,而是寫得最明確,讓讀者省事的辦法。人們常說,代碼讀的時候比寫的時候多得多,所以要想語言好用省事,我們應(yīng)該更加重視讀的時候,而不是寫的時候。
6.數(shù)組的可變性
Rust 的數(shù)組可變性標(biāo)記,跟 Swift 犯了一樣的錯誤。Swift 的問題,我已經(jīng)在之前的文章有詳細(xì)敘述,所以這里就不多說了。簡言之,同一個標(biāo)記能表示的可變性,要么針對數(shù)組指針,要么針對數(shù)組元素,應(yīng)該只能選擇其一。而在 Rust 里面,你只有一個地方可以放“mut”進(jìn)去,所以要么數(shù)組指針和元素全部都可變,要么數(shù)組指針和元素都不可變。你沒有辦法制定一個不可變的數(shù)組指針,而它指向的數(shù)組的元素卻是可變的。
請對比下面兩個例子:
fn main() {let m = [1, 2, 3]; // 指針和元素都不可變m[0] = 10; // 出錯m = [4, 5, 6]; // 也出錯 } fn main() {let mut m = [1, 2, 3]; // 指針和元素都可變m[0] = 10; // 不出錯m = [4, 5, 6]; // 也不出錯 }7.內(nèi)存管理
Rust 號稱實現(xiàn)了非常先進(jìn)的內(nèi)存管理機制,不需要垃圾回收(GC)或者引用計數(shù)(RC)就可以“靜態(tài)”的管理內(nèi)存的分配和釋放。然而仔細(xì)思考之后你就會發(fā)現(xiàn),這很可能是不切實際的夢想(或者廣告)。內(nèi)存的分配和釋放(如果要及時釋放的話),本身是一個動態(tài)的過程,無法用靜態(tài)分析來實現(xiàn)。現(xiàn)在你說可以通過一些特殊的構(gòu)造,特殊的指針和傳值方式,靜態(tài)的決定內(nèi)存的回收時間,真的有可能嗎?
實際上我有一個類似的夢。我曾經(jīng)向我的教授們提出過 N 多種不需 GC 和 RC 就能靜態(tài)管理內(nèi)存的辦法,結(jié)果每一次都被他們給我的小例子給打敗了,以至于我很難相信有任何人可以想到比 GC 和 RC 更好的方法。
Rust 那些炫酷的 move semantics, borrowing, lifetime 之類的概念加在一起,不但讓語言變得復(fù)雜不堪,我感覺并不能從根本上解決內(nèi)存管理問題。很多人在 blog 里面為這些概念熱情洋溢地做宣傳,顯得自己很懂一樣,拿一些玩具代碼來演示,可是從沒看到任何人說清楚這些東西為什么可以從根本上解決問題,能用到復(fù)雜一點的代碼里面去。所以我覺得這些東西有“皇帝的新裝”之嫌。
連 Rust 自己的文檔都說,你可能需要“fight with the borrow checker”。為了通過這些檢查,你必須用很怪異的方式來寫程序,隨著問題復(fù)雜度的增加,就要求有更怪異的寫法。如果用了 lifetime,很簡單一個代碼看起來就會是這種樣子。真夠煩的,我感覺我的眼睛都沒法 parse 這段代碼了。
fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str { }上一次我看 Rust 文檔的時候,沒發(fā)現(xiàn)有 lifetime 這概念。文檔對此的介紹非常粗略,仔細(xì)看了也不知道他們在說些什么,更不要說相信這辦法真的管用了。對不起,我根本不想去理解這些尖括號里的 'a 和 'b 是什么,除非你先向我證明這些東西真的能解決內(nèi)存管理的問題。實際上這個 lifetime 我感覺像是跨過程靜態(tài)分析時產(chǎn)生的一些標(biāo)記,要知道靜態(tài)分析是無法解決內(nèi)存管理的問題的,我猜想這種 lifetime 在有遞歸函數(shù)的情況下就會遇到麻煩。
實際上我最開頭看 Rust 的時候,它號稱只用 move semantics 和好幾種不同的指針,就可以解決內(nèi)存管理的問題。可是一旦有了那幾種不同的指針,就已經(jīng)復(fù)雜不堪了,比 C 語言還要麻煩,而且顯然不能解決問題。Lifetime 恐怕是后來發(fā)現(xiàn)有新的問題解決不了才加進(jìn)去的,可是我不知道他們這次是不是又少考慮了某些情況。
Rust 的設(shè)計者顯然受了 Linear Logic 一類看似很酷的邏輯的啟發(fā)和熏陶,想用類似的方式奇跡般的解決內(nèi)存和資源的回收問題。然而研究過一陣子 Linear Logic 之后我發(fā)現(xiàn),這個邏輯自己都沒有解決任何問題,只不過給對象的引用方式施加了一些無端的限制,這樣使得對象的引用計數(shù)是一個固定的值(1)。內(nèi)存管理當(dāng)然容易了,可是這樣導(dǎo)致有很多程序你沒法表達(dá)。
開頭讓你感覺很有意思,似乎能解決一些小問題。到后來遇到大一點的實際問題的時候,你就發(fā)現(xiàn)需要引入越來越復(fù)雜的概念,使用越來越奇葩的寫法,才能達(dá)到目的,而且你總是會在將來某個時候發(fā)現(xiàn)它沒法解決的問題。因為這個問題很可能從根本上是無法解決的,所以每當(dāng)遇到有超越現(xiàn)有能力的事情,你就得增加新的“繞過方法”(workaround)。縫縫補補,破敗不堪。最后你發(fā)現(xiàn),除了垃圾回收(GC)和引用計數(shù)(RC),內(nèi)存管理還是沒有其它更好更簡單的辦法。
當(dāng)然我的意見也許不是完全準(zhǔn)確,可我真是沒有時間去琢磨這么多亂七八糟,不知道管不管用的概念(特別是 lifetime),更不要說真的用它來構(gòu)建大型的系統(tǒng)程序了。有用來理解這些概念,把程序改成奇葩樣子的時間,我可能已經(jīng)用 C 語言寫出很好的手動內(nèi)存管理代碼了。如果你真的看進(jìn)去理解了,發(fā)現(xiàn)這些東西可以用的話,告訴我一聲!不過你必須說明原因,不要只告訴我“皇帝是穿了衣服的” 😛
8.完
本來想寫一個更詳細(xì)的評價的,可是到了這個地方,我感覺已經(jīng)失去興趣了,困就一個字啊…… Rust 比 C 語言復(fù)雜太多,我很難想象用這樣的語言來構(gòu)造大型的操作系統(tǒng)。而構(gòu)造系統(tǒng)程序,是 Rust 設(shè)計的初衷。說真的,寫操作系統(tǒng)那樣的程序,C 語言真的不算討厭。用戶空間的程序,Java,C# 和 Swift 完全可以勝任。所以我覺得 Rust 的市場空間恐怕非常狹小……
來源:http://www.yinwang.org/blog-cn/2016/09/18/rust
總結(jié)
以上是生活随笔為你收集整理的王垠:对 Rust 语言的分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 重磅!西安交通大学使用脑机接口技术实现了
- 下一篇: FFmpeg命令(四)、 图片转视频