Swift 語言的設計錯誤[轉]

http://www.yinwang.org/blog-cn/2016/06/06/swift程序員

在『編程的智慧』一文中,我分析和確定了 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

var shoppingList = ["Eggs", "Milk"]

// 能夠對 array 成員賦值
shoppingList[0] = "Salad"
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 對象。正確的用法應該是這樣:性能

var shoppingList = ["Eggs", "Milk"]

// 能夠對 array reference 賦值
shoppingList = ["Salad", "Noodles"]

// 能夠對 array 成員賦值
shoppingList[0] = "Salad"
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 來區分。code

Swift 2.0 修正了這個問題,然而惋惜的是,它的修正方式是錯誤的。Swift 2.0 作出了一個離譜的改動,它把 array 從 reference type 變成了所謂 value type,也就是說把整個 array 放在棧上,而不是堆上。這貌似解決了以上的問題,因爲 array 成了 value type,那麼 shoppingList 就不是 reference,而表明整個 array 自己。因此在 array 是 value type 的狀況下,你確實能夠用 var 和 let 來決定它的成員是否可變。對象

let shoppingList = ["Eggs", "Milk"]

// 不能對 array 成員賦值,由於 shoppingList 是 value type
// 它表示整個 array 而不是一個指針
// 這個 array 的任何一部分都不可變
shoppingList[0] = "Salad"

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

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 3.0 還要繼續。因爲 Apple 的統治地位,這種狀況對於 Swift 語言也許不是世界末日,然而它已經犯了語言設計的大忌。一個好的語言能夠缺乏一些特性,但它毫不應該加入錯誤的設計,致使往後出現不兼容的改變。我但願 Apple 可以早日招募到功力深厚的語言設計專家,虛心採納他們的建議。BTW,若是 Apple 支付足夠多的費用,我倒能夠考慮兼職作他們的語言設計顧問 ;-)

相關文章
相關標籤/搜索