如何評價王垠的《Swift 語言的設計錯誤》?

Swift 語言的設計錯誤


這篇文章有些地方我認同,更多地方是不認同。

Swift 的對象,可以分為 struct 類型和 class 類型。struct 類型就是值類型,class 類型就是引用類型。

假如 array 是引用類型。這種語法應該是對的

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

但假如 array 是值類型,上面語法就是錯的。Swift 也確實是這樣區分的。

王垠的意見是,array 這種大數據結構,不應該採用值類型。而應該使用引用類型,而將 array 分拆成可變和不可變兩個類。因為值類型的複製代價很大。

但他似乎忽略了一點,Swift 語言的 array 和 dictionay, string 這些常用的數據結構,雖然是值類型。但它在實現時,採用了寫時複製。平常將 array 作為函數參數,並不會引起複制。

對於同樣的問題。王垠採用 A 方案解決,保留引用類型,而將其分拆成可變不可變兩個類。 而 Swift 採用了 B 方案解決,切換到值類型,使用寫時複製。

Swift 的方案沒有問題。

另外「swift,Rust 和 C# 的 value type 製造的更多是麻煩,而沒有帶來實在的性能優勢。「

這句話,我不能認同,在 iOS 開發,涉及到大量的點(CGPoint), 矩形(CGRect) 等基礎對象,假如這些對象也採用引用類型,而在堆中分配,是不能忍受的。使用 Objective-C 的時候,當需要存儲大量點的時候,為了性能考慮,也經常會切換到使用 C++, 用 std:vector 存儲。而不是用 NSArray 將點放到 NSValue 中。

Swift 繼續使用 Objective-C 的引用計數,並沒有一個 GC, 這種值類型和引用類型的區分,多個選擇,我覺得是好事。Objective-C 以前版本是增加過 GC 的,後來實踐發現 GC 的版本會慢,就將 GC 去掉了。現在 Swift 沒有 GC, 採用 ARC 管理內存。

---------

回答中,我最開始說 Swift 1.0 中 array 是引用類型,Swift 2.0 才切換到值類型。查了一下資料,似乎 Swift 的 array 從一開始,就是值類型。為避免誤導,特定指出,也將之前可能出錯的語句刪掉了。


你就說王老師用過的什麼東西沒毛病……


文章的重點在最後一句啊。

數組什麼的說得挺有道理,不過他對value type的看法倒是很符合他沒有太多實際工程經驗的背景,沒有被GC坑過的人有這樣的觀點很正常,畢竟50行代碼GC也沒機會幹什麼。


很遺憾,這篇文章從頭到尾都在胡扯。王垠既不知道swift編譯器的內部實現,也不知道swift的歷史進程。

swift在設計之初就對class和struct兩種類型進行了明確區分,let和var對於class而言只保證引用不可變,對於struct應保證其內部也是不可變的。在swift3.0中,還計劃繼續將諸如NSDate和NSData等類作為struct引入swift標準庫,以確保在使用let或var的時候字面意思與其實際意義一致(即看起來應該是值類型的表現也應當是值類型)

對於堆棧使用的問題此文更是一派胡言,swift的值類型都採取了copy on write策略,並不會因為簡單的賦值操作影響性能,而struct內部實現實質上是放在堆上,基於對開發者不透明的一種特殊引用計數管理其生命周期。這也引出了另外一個話題,就是在目前版本的swift下能夠利用這點構造基於struct的循環引用,譬如http://www.cocoawithlove.com/blog/2016/03/27/on-delete.html中所介紹的情況。如果真的要噴的話,這種混亂的引用計數實現才是噴點,而不是王垠那篇文章中那樣基於猜測的亂噴。


昨天早上(美西時間)醒來寫的答案不知所云,我自己都看不懂:

Swift 團隊對此的考慮更像是深思熟慮的結果。比如說,最近通過的 SE-0069 Mutability and Foundation Value Types 已經欽定了 Core Lib 用值類型表示不可變數據、引用類型表示可變數據。考慮到他們官方推薦給大數據結構寫時複製,恐怕王垠的提議,通過不同的 class 表示同一類型的可變與不可變版本,早在內部被拒絕了,改成用 struct 與 class 表示這一區別。

不過我重新組織一下語言:

一、Swift 現有的數組實現不太需要關心性能問題,因為採用了寫時複製(copy on write)語義,幾個高票答案在這一方面已經描述地很好了。同時,Swift 官方文檔也推薦用戶在自己實現比較大的數據結構的時候實現寫時複製語義,原因正如王垠所說,直接複製開銷過大。

二、在 Swift 里,如果一個 let 變數的類型是一個類,該變數雖然不可以指向一個新的對象,但卻可以調用該對象的任意方法,而這些方法可能會改變對象本身的內容。大家可以在 Playground 里運行這個例子:

class SomeClass {
var value: Int
init(value: Int) {
self.value = value
}
}

let ref = SomeClass(value: 3)
// 3
print(ref.value)
ref.value = 4
// 4
print(ref.value)

(這段內容即將失效,本答案寫於 WWDC 2016 前一個星期)

套用 @米斯特菜 所提到的「對稱性」一詞,Swift 上現有的 Cocoa 介面缺少一個 symmetry between mutable data types and immutable ones。很多 Cocoa 的類型都是 class 而不是 struct 或者 enum,比如說 NSBundle,你在幾個月內都沒有辦法表示一個 immutable 的 NSBundle。這也正是王垠在他博文里提到的問題:以前的 Cocoa,是有 NSArray 和 NSMutableArray 的區別的。

不過正如我原始答案所提到的,剛剛通過的 SE-0069 Mutability and Foundation Value Types 致力於解決這個問題。根據這個提議,標準庫里的很多類型將成對出現,比如說

  • AffineTransform 與 NSAffineTransform

  • CharacterSet 與 NSCharacterSet

  • Date 與 NSDate

  • DateComponents 與 NSDateComponents

其中沒有 NS 前綴的將是值類型(value type),是一個 struct 而不是 class,而對應的有 NS 前綴的將是引用類型(reference type),是一個 class 而不是一個 struct。雖然我很想吐槽用 NS 這個前綴表示區分,但至少在標準庫層面,這玩意得到了一個統一。

本提議的狀態是被 Swift 3 所接受,所以幾個月之後就應該可以用了,而 WWDC 應該也會有更多的信息出現。值得注意的是,Apple 為了真正將 Swift 搬到 Linux,決定把 GUI 無關的 Cocoa 的很多類全部放入一個叫做 Core Library 的東西(與 standard library 作區分),比如我上面舉的這個類型。SE-0069 調整的也正是 Core Library。

不過或許有一個問題還沒有解決,NSArray 是不是也要變成一個引用類型呢?難道以後我們還要繼續用 NSMutableArray?從這個角度來看,雖然王垠推理全錯,但還是提了一個好問題呢(光速逃)。


依稀記得許多年前看過一篇文章,講的大概是copy-on-write在多核的新環境下已經落後、低性能、沒人用了的事情,所以Delphi要跪。沒想到Swift也(略

果然能用的語言來來回回都是這麼幾樣東西,普通程序員已經有幾十年沒有被迫接觸過新東西了。

=============

王垠寫了篇新文章來澄清(Java 有值類型嗎?),大概說的就是「我口中的值類型和引用類型跟你們口中的不一樣,你們要按照我說的來討論」。當然這篇文章不看「王垠的術語」和「大家的術語」的區別的話,講的還是很有道理的。


放在C++里,就是要實現const 引用不能調用非const成員函數。

也就是禁止const Class b = a;b.set(X);這樣的語句。

但是Swift裡面let的語義是這樣:Class const b = a;//我故意把跟Class寫一起了,因為Swift裡面沒有顯性的引用,或者說都是引用

這就尷尬了,因為被禁止的只是b=c這種語句,而b.set還是隨便調用。這跟我預想不一樣啊,怎麼辦呢?

於是蘋果大神想了一種方法,對於某些類,它們比較常用,也經常用於存常量,就讓他們作為值來傳遞,比如數組,把上面的Class整個替換成Array,寫下Array b = a;的時候b整個就是一個常量,不能調用任何非const方法([]=可以看作set(index, value)方法),當然b=c也是不允許的。

目的達到論了,似乎挺和諧。

但是這麼做引來一些問題。

一是王神所說,數組這麼大,靠值傳遞會引發嚴重的效率問題。這個Swift採取的解決方案是copy-on-write,簡稱cow(母牛)。線程安全與多核並行問題暫且不論,在C++裡面,cow是非常蛋疼的,因為[]無法區分左右(可以參考下map裡面的[]操作,不管是get還是set都會給你insert),導致c = str[5]這樣的表達式也能觸發cow。事實上string的複製效率非常高,一般都會用memcpy實現,並且如果我不想複製直接用const string 就行了,需要複製的時候早晚也要複製,還能自己覺得早還是晚,cow完全是自作多情的做法。然而某些C++編譯器似乎一意孤行的使用這項在VS2003時代就放棄了的「技術」,果然C++是裝逼樂園。

TMD扯遠了,一黑就收不住。總之cow是蛋疼的東西,沒有任何效率優化,卻增加了程序員的思考負擔。王神在博客中用的一個詞非常好——削足適履。

第二個問題是把Array當值傳遞使得語義不那麼統一。類型分為兩類:不可變類型,包括C中的int, double, Java/Python中的字元串,他們除了用賦值之外沒有任何改變自身值的方法;可以改變的類型,包括各種自定義類,數組,它們可以通過某種操作改變自身,比如set方法,比如[]=的操作。如果前者按值傳遞,後者按引用傳遞,那麼這種傳遞方式可以看成所有對象都是引用傳遞(賦值可以看成改變引用,參見Java 有值類型嗎?)。無端的讓數組值傳遞,破壞了上述統一性,也使得數組從「可改變對象」中割裂,為什麼偏偏是數組,其他類對象怎麼不這麼搞?

PS:C的傳遞方式非常統一,都是值傳遞;C++的分為引用和值兩種,不過都是顯式聲明的,也無可厚非;C#就不那麼統一了,既有值又有引用,光看var b = a不知道是怎麼傳的。

問題之三還是王所說,value type並沒有帶來多少效率的提升,卻大大增加麻煩。在這裡不得不黑一波Rust,這貨號稱比C++安全……個屁!用了各種關鍵字,各種符號,各種庫,就是為了實現C++裡面的各類智能指針,同時各種指針本身的問題一個也沒避免,租借move之後只能有一個在用(unique),RC不能循環引用(shared),非線程安全,再加上unsafe(raw pointer),根本就是比C++沒有一丁點長進,完全是一廂情願,倒也有mozilla的作風。

回到問題。有人可能會問,既然你說這麼做不好,那應該怎麼做?我的看法,一是引入新關鍵字,如C++的const,C#的readonly,禁止一切改變類成員的行為(數組元素可以看成數組類的成員);二是像Java/Python/C(89)那樣,放著不管,不提供保護措施,你愛改就改出了問題自己負責。兩種方法都能保證語義的統一性,前者稍微複雜,能滿足強迫症需求;後者更加簡潔,代價是幾乎為0的出錯幾率。

iOS9變卡不是沒有原因的。



補充:從值類型,引用類型,可變性說開去 - zhenghui zhou的文章 - 知乎專欄

再次贊同一下王垠,前面@黃兢成的答案已經解釋的比較清楚了。但我是不贊同為了解決什麼個問題,就搞出很多限制或者付出額外的不必要的代價。

很多人模糊的意識到這跟對象的分配方式有關係,比如王垠提到了帶有GC的語言的做法,黃兢成也解釋了在堆中分配的額外開銷,不能都用GC,而是(有選擇的)進行引用計數。

除了從語義上解釋這個問題,另一方面swift這種無奈的選擇實際上是源自c++起就引入的一個深坑,似乎到目前為止都沒有引起人們的注意,最新的語言要麼完全採用GC方案,要麼就去兼容C++的做法並適當去增強它。或者是在語法層面上搞出一堆限制(Rust)。本質上在於C++對delete操作做出了明確的固定,這是一個錯誤的設計,也引發了智能指針設計上帶來的系列問題。

這個問題下,很多人拿實現說話,這似乎有點搞笑了。僅僅是實現方法里用了很多的trick來彌補設計缺陷可能引起的性能缺陷,這就算是設計正確嗎?一邊說寫複製,一邊說避免了寫的開銷,這不是自相矛盾嗎?再回到根本上,值類型表達不可寫,這是個什麼鬼,雖然我明白那個目的,但明顯在設計上是為了目的而目的,引入不必要的概念,增加了負擔。


。。。。我昨晚想到一種情況,嚇得睡不著,它是這樣的:

step1、我們有一個數組a,有一個閉包lambda

step2、把它傳遞給某個函數f,函數的形參b獲得了數組a的值;

step3、f對b進行了某種查詢;

step4、然後lambda把數組a給改了;

step5、然後直到返回,f動也沒動b。

還請諸位大神開示,怎麼用cow保證數組a不被莫名其妙地複製一次


什麼叫「拋磚引玉」,我確實真切的見識到了。。。

自嘆不如


Swift 中 let 和 var 的語義,我也覺得確實難於理解。如果 let 作用於 Value Type 時能使其本身不可變而不僅僅是 Reference 不可變,就超出了 Type 跟著 Value 走的心智模型。你拿到一個東西,它的值是否可變,不在這個值的類型信息上,而是在這個 Name Binding 使用了 let/var上,Immutable 跟 Mutable 類型的介面往往也會設計得不一樣,比如 Immutable Array append 方法返回值跟 Mutable Array 的 append 方法返回值保持一樣合理嗎?你會發現,同一個數據類型,它的方法因為let/var的區別語義完全變掉了。再一個,如果可變性是 Array 類型本身的信息,那就可以輕鬆理解 ImmutableArray of MutableArray 這樣的類型,我不知道用了 let/var 還怎麼去解釋這些。

不過,我覺得在討論語義時,不應該將具體實現牽扯進來(比如 C++ 中按值傳遞按引用傳遞,反彙編說傳了內存地址來解釋,就顯得不得要領)。這麼大歲數還要扯分配在堆上還是分配在棧上,這是王垠這篇文章讓人大跌眼鏡的地方。


頂王垠, 不管王垠文章裡面有多少錯誤, 有句話說得很對: 語言是給開發者用的, 不是給編譯器專家用的. 你 blabla 搞這麼語義, 這門語言的用戶體驗一坨屎一樣. 扯那麼實現有用嗎?


畢竟是編譯原理沒考及格的水平。

編譯原理考試第一題。對於業務邏輯狗來說,語義是strict, immutable的語言才是最好的。可是業界因為immutable更慢佔用內存更多等偏見不願意使用immutable的語言。業務邏輯狗有苦說不出啊。你學習了編譯原理,現在是時候寫一個編譯器,駁斥各種不實的說法了。Tips: MLton通過whole-program analysis把高階函數拍扁成一階函數,你也可以用類似的方法,自動找出可以改成mutable而不影響語義的代碼,並對其進行轉換。


Swift的篇幅少點,說明還是設計得不錯。

我發現王老師總拿Java的觀點來看待其它語言,這是病得治。王老師無非就是認為值類型每次賦值必定發生複製,開銷很大,而引用類型可以選擇手動顯式複製,或者僅僅將引用指向相同實例。但是值類型不意味著「數據一定放棧上」,它是根據對象賦值時的行為而不是儲存位置來定義。有一類值類型底層實現上把數據放堆上,它就可以實現「寫時複製」,避免不必要的開銷。典型的例子就是C++的std::vector或者std::string。如果王老師要噴Swift.Array,把C++的容器也一起噴了吧。但是我從來沒看過王老師噴C++,明明實現機制和類型都是一樣的,說明還是欺軟怕硬啊。

不過有一點王老師說的還是對的,就是對於imutable,值類型和引用類型無所謂,或者說這種二分法不適用於它們。比如Java的String和JS的string在行為上完全一致,但是JS標準中的string就是值類型。


王垠所討論的是容器和元素關係的問題,他在另一篇評論Rust的文章里也討論了相關內容。

我在他文章里看的真真切切,表達的很清楚,並且不無道理。

容器和元素要麼都可變,要麼都不可變,不能聲明一個容器不可變而容器內的元素可變,他是想說明這個問題。這是一個亘古已有的爭論,王垠所支持的設計的確更符合邏輯。

王垠不是神也會犯錯,但是他受過國內外三所頂尖大學的專業訓練是PL領域的專家,相比之下國內大部分程序員的層次遠不及王垠的水平,所以在探討他的 "錯誤" 之前應當更嚴謹些。現代科學離普通人已經十分遙遠,分工高度細化且專業,即使是專業的程序員在PL這麼精深的學科面前也只是普通人。

所以有關swift內部實現是怎麼做的,他要討論的並不是這個。嚴重反對抖機靈和變相嘲諷的答案。

有關王垠的特立獨行,有的人可能十分反感,然而我覺得他是一位敢說真話的人,並且具有很好的批判精神,因此刺中了一些人心中的痛點,搞得他們很不舒服。一些人對大牛的話爛熟於心,過度崇拜奉若圭臬。比如knuth說過的"過早的優化是萬惡之源",C++之父就在他的TC++PL中評論過,knuth雖然說的有道理,但一些人"學的實在是太好了"(話不說透,自己領悟)。

所以,學著點吧,對於一個比自己強很多倍的人還是虛心學習為上,不要跟人瞎起鬨,因為事實就在那裡,無論同不同意,它就在那。


我只能說

王博士程序寫的太少了,才無法體會這個設計的必要性和簡潔性,語言是給開發者開發使用的,開發者的用戶體驗才是更加重要的,而不是什麼編譯器專家或者語言設計專家

其他:最大的問題就是以偏概全,抓住一個問題(不知道算不算問題,就姑且按其觀點算吧),然後極大的貶低一個新生知名語言(大概是為了突出自己牛B?)

恩,iPhone面世的時候,如果按這種標準來挑毛病,估計是百無是處吧?


沒有任何其它現代語言(Java,C#,……)把 array 作為 value type。

王垠這是在黑 php 不是現代語言嗎?


Swift,從精通到入門。

不知樓上有多少「從一開始就關注,躍躍欲試希望早日用在自己項目中,最終慶幸沒有使用」的 Swift 用戶。


文章的核心主題:

把 array 作為 value type,使得每一次對 array 變數的賦值或者參數傳遞,都必須進行拷貝。你沒法讓兩個變數指向同一個 array,也就是說 array 不再能被共享。

這違反了程序員對於數組這種大型結構的心理模型,他們不再能清晰方便的對 array 進行思考。由於 array 會被不經意的自動拷貝,很容易犯錯誤。

SO上的提問已具體說明問題:Swift: Pass array by reference?

王垠談論的語義上的問題,而不是具體實現問題。


推薦閱讀:

王垠公開未簽署的秘密離職協議,是否違法?
如何看待王垠的博文《我和權威的故事》?
如何評價王垠的博文《中國式母親》?
如何評價王垠新博文《經驗和洞察力》?
聽說王垠加入了微軟,是否屬實?

TAG:王垠人物 | 如何看待評價X | Swift語言 |