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 試圖實現這樣的語義:

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 來區分。

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 不再能被共享。比如:

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 支付足夠多的費用,我倒可以考慮兼職做他們的語言設計顧問 ;-)

來源:王垠

上一篇: 如何理解高階函數

下一篇: 一名iOS程序員眼中的「小程序」

分享到: 更多
pk10每期必中万能6码 pk10最牛稳赚公式5码 十一选五任三稳赚50元 双色球胆拖预测 重庆市彩开奖号码记录 重庆时时开奖最快直播 北京pk赛车软件 大乐透走势图表图 二人好友斗地主可以吗 黑龙江时时图表 黄金pk10计划软件手机版 牛牛看4张牌抢庄 福建时时号码表 北京快车pk10直播视频 免费快三计划软件app 吉林省今天快3走势图