在『程式設計的智慧』一文中,我分析和肯定了 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 試圖實現這樣的語義:
1 2 3 4 |
var shoppingList = ["Eggs", "Milk"] // 可以對 array 成員賦值 shoppingList[0] = "Salad" |
1 2 3 4 |
let shoppingList = ["Eggs", "Milk"] // 不能對 array 成員賦值,報錯 shoppingList[0] = "Salad" |
這是錯誤的。在 Swift 1.0 裡面,array 像其它的 object 一樣,是一種“reference type”。為了理解這個問題,你應該清晰地區分 array reference 和 array 成員的區別。在這個例子裡,shoppingList
是一個 array reference,而 shoppingList[0]
是訪問一個 array 成員,這兩者有著非常大的不同。
var 和 let 本來是用於指定 shoppingList
這個 reference 是否可變,也就是決定 shoppingList
是否可以指向另一個 array 物件。正確的用法應該是這樣:
1 2 3 4 5 6 7 |
var shoppingList = ["Eggs", "Milk"] // 可以對 array reference 賦值 shoppingList = ["Salad", "Noodles"] // 可以對 array 成員賦值 shoppingList[0] = "Salad" |
1 2 3 4 5 6 7 |
let shoppingList = ["Eggs", "Milk"] // 不能對 array reference 賦值,報錯 shoppingList = ["Salad", "Noodles"] // let 不能限制對 array 成員賦值,不報錯 shoppingList[0] = "Salad" |
也就是說你可以用 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 程式設計師都知道 NSArray
和 NSMutableArray
的區別。我不知道為什麼 Swift 的設計者看不到這個問題,試圖用同樣的關鍵字(var 和 let)來指定棧和堆兩種不同位置資料的可變性。實際上,不可變陣列和可變陣列,應該使用兩種不同的型別來表示,就像 Objective C 的 NSArray
和 NSMutableArray
那樣,而不應該使用 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 來決定它的成員是否可變。
1 2 3 4 5 6 |
let shoppingList = ["Eggs", "Milk"] // 不能對 array 成員賦值,因為 shoppingList 是 value type // 它表示整個 array 而不是一個指標 // 這個 array 的任何一部分都不可變 shoppingList[0] = "Salad" |
這看似一個可行的解決方案,然而它卻沒有擊中要害。這是一種削足適履的做法,它帶來了另外的問題。把 array 作為 value type,使得每一次對 array 變數的賦值或者引數傳遞,都必須進行拷貝。你沒法讓兩個變數指向同一個 array,也就是說 array 不再能被共享。比如:
1 2 3 4 5 |
var a = [1, 2, 3] // a 的內容被拷貝給 b // a 和 b 是兩個不同的 array,有相同的內容 var b = a |
這違反了程式設計師對於陣列這種大型結構的心理模型,他們不再能清晰方便的對 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 的區別的。” 由於這個問題相當的有趣,我另外寫了一篇文章來回答這個問題。