Swift 語言的設計錯誤

發表於2016-06-12

在『程式設計的智慧』一文中,我分析和肯定了 Swift 語言的 optional type 設計,但這並不等於 Swift 語言的整體設計是完美沒有問題的。其實 Swift 1.0 剛出來的時候,我就發現它的 array 可變性設計存在嚴重的錯誤。Swift 2.0 修正了這個問題,然而他們的修正方法卻沒有擊中要害,所以導致了其它的問題。這個錯誤一直延續到今天。

Swift 1.0 試圖利用 var 和 let 的區別來指定 array 成員的可變性,然而其實 var 和 let 只能指定 array reference 的可變性,而不能指定 array 成員的可變性。舉個例子,Swift 1.0 試圖實現這樣的語義:

這是錯誤的。在 Swift 1.0 裡面,array 像其它的 object 一樣,是一種“reference type”。為了理解這個問題,你應該清晰地區分 array reference 和 array 成員的區別。在這個例子裡,shoppingList 是一個 array reference,而 shoppingList[0] 是訪問一個 array 成員,這兩者有著非常大的不同。

var 和 let 本來是用於指定 shoppingList 這個 reference 是否可變,也就是決定 shoppingList 是否可以指向另一個 array 物件。正確的用法應該是這樣:

也就是說你可以用 var 和 let 來限制 shoppingList 這個 reference 的可變性,而不能用來限制 shoppingList[0] 這樣的成員訪問的可變性。

var 和 let 一旦被用於指定 array reference 的可變性,就不再能用於指定 array 成員的可變性。實際上 var 和 let 用於區域性變數定義的時候,只能指定棧上資料的可變性。如果你理解 reference 是放在棧(stack)上的,而 Swift 1.0 的 array 是放在堆(heap)上的,就會明白array 成員(一種堆資料)可變性,必須用另外的方式來指定,而不能用 var 和 let。

很多古老的語言都已經看清楚了這個問題,它們明確的用兩種不同的方式來指定棧和堆資料的可變性。C++ 程式設計師都知道 int const *int * const 的區別。Objective C 程式設計師都知道 NSArrayNSMutableArray 的區別。我不知道為什麼 Swift 的設計者看不到這個問題,試圖用同樣的關鍵字(var 和 let)來指定棧和堆兩種不同位置資料的可變性。實際上,不可變陣列和可變陣列,應該使用兩種不同的型別來表示,就像 Objective C 的 NSArrayNSMutableArray 那樣,而不應該使用 var 和 let 來區分。

Swift 2.0 修正了這個問題,然而可惜的是,它的修正方式是錯誤的。Swift 2.0 做出了一個離譜的改動,它把 array 從 reference type 變成了所謂 value type,也就是說把整個 array 放在棧上,而不是堆上。這貌似解決了以上的問題,由於 array 成了 value type,那麼 shoppingList 就不是 reference,而代表整個 array 本身。所以在 array 是 value type 的情況下,你確實可以用 var 和 let 來決定它的成員是否可變。

這看似一個可行的解決方案,然而它卻沒有擊中要害。這是一種削足適履的做法,它帶來了另外的問題。把 array 作為 value type,使得每一次對 array 變數的賦值或者引數傳遞,都必須進行拷貝。你沒法讓兩個變數指向同一個 array,也就是說 array 不再能被共享。比如:

這違反了程式設計師對於陣列這種大型結構的心理模型,他們不再能清晰方便的對 array 進行思考。由於 array 會被不經意的自動拷貝,很容易犯錯誤。陣列拷貝需要大量時間,就算接收者不修改它也必須拷貝,所以效率上有很大影響。不能共享同一個 array,在裡面讀寫資料,是一個很大的功能缺失。由於這個原因,沒有任何其它現代語言(Java,C#,……)把 array 作為 value type。

如果你看透了 value type 的實質,就會發現這整個概念的存在,在具有垃圾回收(GC)的現代語言裡,幾乎是沒有意義的。有些新語言比如 Swift 和 Rust,試圖利用 value type 來解決記憶體管理的效率問題,然而它帶來的效能提升其實是微乎其微的,給程式設計師帶來的麻煩和困擾卻是有目共睹的。完全使用 reference type 的語言(比如 Java,Scheme,Python),程式設計師不需要思考 value type 和 reference type 的區別,大大簡化和加速了程式設計的思維過程。Java 不但有非常高效的 GC,還可以利用 escape analysis 自動把某些堆資料放在棧上,程式設計師不需要思考就可以達到 value type 帶來的那麼一點點效能提升。相比之下,Swift,Rust 和 C# 的 value type 製造的更多是麻煩,而沒有帶來實在的效能優勢。

Swift 1.0 犯下這種我一眼就看出來的低階錯誤,你也許從中發現了一個道理:編譯器專家並不等於程式語言專家。很多經驗老到的程式語言專家一看到 Swift 最初的 array 設計,就知道那是錯的。只要團隊裡有一個語言專家指出了這個問題,就不需要這樣反覆的修改折騰。為什麼 Swift 直到 1.0 釋出都沒有發現這個問題,到了 2.0 修正卻仍然是錯的?我猜這是因為 Apple 並沒有聘請到合格的程式語言專家來進行 Swift 的設計,或者有合格的人,然而他們的建議卻沒有被領導採納。Swift 的首席設計師是 Chris Lattner,也就是 LLVM 的設計者。他是不錯的編譯器專家,然而在程式語言設計方面,恐怕只能算業餘水平。編譯器和程式語言,真的是兩個非常不同的領域。Apple 的領導們以為好的編譯器作者就能設計出好的程式語言,以至於讓 Chris Lattner 做了總設計師。

Swift 團隊不像 Go 語言團隊完全是一知半解的外行,他們在語言方面確實有一定的基礎,所以 Swift 在大體上不會有特別嚴重的問題。然而可以看出來這些人功力還不夠深厚,略帶年輕人的自負,浮躁,盲目的創新和借鑑精神。有些設計並不是出自自己深入的見解,而只是“借鑑”其它語言的做法,所以可能犯下經驗豐富的語言專家根本不會犯的錯誤。第一次就應該做對的事情,卻需要經過多次返工。以至於每出一個新的版本,就出現一些“不相容改動”,導致老版本語言寫出來的程式碼不再能用。這個趨勢在 Swift 3.0 還要繼續。由於 Apple 的統治地位,這種情況對於 Swift 語言也許不是世界末日,然而它確實犯了語言設計的大忌。一個好的語言可以缺少一些特性,但它絕不應該加入錯誤的設計,導致日後出現不相容的改變。我希望 Apple 能夠早日招募到資深一些的語言設計專家,虛心採納他們的建議。BTW,如果 Apple 支付足夠多的費用,我倒可以考慮兼職做他們的語言設計顧問 ;-)

Java 有 value type 嗎?

有人看了以上的內容,問我:“你說 Java 只有 reference type,但是根據 Java 的官方文件,Java 也有 value type 和 reference type 的區別的。” 由於這個問題相當的有趣,我另外寫了一篇文章來回答這個問題。

相關文章