Swift

來講講從今年 WWDC 之後,到目前為止練習 Swift 語言的心得。

想想其實蘋果推出一門新語言其實並不奇怪。雖然對許多人來說,是最近幾年才開始學 Objective-C,但是 Objective-C 算一算已經有三十多年的歷史,而三十年過去,自然不少人會看到這門語言的缺陷。

在八零到九零年代,Objective-C 最大的缺點就是慢—我們稍微了解一下 Objective-C 的 Runtime:Objective-C 其實是在 C 上面架構了簡單的一層, Objective-C 語法會在 compile time 時編譯成 C,每個 Objective-C 物件其實都是一個指向某個 C structure 的指標,物件的成員就是 structure 中的變數,至於物件的 method 則是會編譯成 C function,然後 runtime 中會有一個表格,管理一個 C 字串對應到 function pointer 的對應,我們對某個物件呼叫某個 method 的時候,就是用 method 的名稱(又稱為 selector)去尋找對應的 function 執行。

因為 Objective-C runtime 是這樣設計,於是這個語言就有一些有趣的特性,像是可以在不用繼承物件的狀況下,直接對某個 Class 新增 method—因為只要對這個「字串 key/function pointer」的表格繼續加東西就好—這叫做 category。另外也可以在 runtime 抽換某個 method 的實作,方法是改變某個 key 與某個 function pointer 的對應關係,把原本的 method 名稱指到別的位置上,這叫做 method swizzling。

但相對的,Objective-C 就沒有 overload,因為一個 method 的名稱就只能夠對應到一個 function pointer。而每呼叫一個 method,就要在這個表格中查詢一輪,在八零到九零年代的硬體上想來不太能讓人忍受,雖然我自己在那個時代還不知道什麼是 Objective-C 就是了。

在二十一世紀第一個十年,隨著硬體發展,執行速度慢慢地變成不是問題,而比較起來,Objective-C 應該還比其他架構在一些 VM 上的語言來得快,加上蘋果軟硬體都有,硬是可以在自己的硬體上壓榨出效能來,於是 Objective-C 這幾年有些不錯的光景,而蘋果開始想辦法繼續掌握 Compiler 技術,處理 Objective-C 的其他缺陷。

像是缺乏匿名函式的問題,蘋果就弄了 block 語法出來—雖然很多時候你會覺得 block 很兩光,像是你在使用 block 的時候往往還是要注意記憶體管理問題,先是你要知道 block 裡頭用到外面的變數都會 copy 一份,如果你想改動 block 外面的變數還要用 __block 語法,還要注意 self 不要不小心也被 copy 這類基本問題,如果你想拿 block 寫遞迴更是容易撞到牆;然後在不同狀況下,block 會被編譯成要在 heap 還是 stack 執行,而蘋果 compiler 又有點 bug,如果你把 block 放進 NSArray 中, Compiler 就可能會把該使用 stack 還是 heap 搞錯而造成 Bad Access 的錯誤。但總之,block 似乎堪用,這些年也看到不少基於 block 發展出的 framework 與高階用法。

Objective-C 另外一點讓人詬病的是 retain/release 這套半手動的記憶體管理方式,似乎四五年前開始寫 Objective-C 的人,大概都會在這邊卡關一陣子,一不小心就忘記 retain/release 要成對,retain 太多次會 memory leak,release 太多次會 bad access,新手還沒有寫幾行程式,就一直盯著 retain count 在看。

蘋果一開始做了一套 Garbage Collection,不過並不成功,我在 2010 年左右參與一個用 Objective-C GC 寫的案子,結果只要不在 main thread 中呼叫 NSDate 物件,memory leak 就狂洩不止。後來蘋果在 iOS 5 推出 ARC,改成在 compile time 時,透過靜態分析自動在該 release 的地方加上 release。

當然 ARC 也有 ARC 的問題,像是啟用 ARC 之後,就不能夠在 C structure 中放 Objective-C 物件,Objective-C 物件與 Core Foundation API 之間又要使用大量難懂的 bridge 語法,而有時 compiler 會把 release 放在不該放的地方。不過,最近幾年新寫的 Objective-C 專案,大概也都採用 ARC 了。

蘋果這些年還有一些比較次要的演進,像是 Xcode 4.4 之後的新 Literal 語法,寫了 property 不用另外寫 synthesize,compiler 會自動補上成員變數等。而有了這些,我們還是會覺得,Objective-C 不是一門安全的語言。

Objective-C 語言中,可以將任何物件 cast 成 id 型態,也就是說,不管這個物件屬於哪一個 Class,我們都可以指宣告成是一個物件的指標。

在某些狀況下這點很好用,像是 Objective-C 的 delegate pattern 就是在這個基礎上發展出來的—所謂的 delegate 用一般的說法來說,就是某個物件遇到某個狀況時並不打算自己處理,而是交給另一個物件負責;用我自己的語言,則是某個物件上有很多 callback,在別的語言中,可能會指定一堆 callback function、event handler 處理,在 Objective-C 中則習慣把所有的 callback 送到單一物件上,這個物件實作所有 callback 時需要的 method,這個物件屬於哪種 Class 不重要,重要的是有這些 method 可以呼叫。這樣的物件叫做 delegate,因為屬於哪個 Class 不重要,所以我們會 cast 成 id,而某個 delegate 應該要實作的 method 的集合,叫做 protocol,所以 delegate 就會是實作了哪些 protocol 的 id。

但另一方面,可以把物件 cast 成 id 很危險。在 Objective-C 中,所有的 Collection 物件,像是 NSArray、NSDictionary、NSSet…都不管集合裡頭每個物件的型別,只要可以 cast 成 id,都可以插入到 Collection 物件中,當你拿到一個 NSArray,完全無法預期 array 裡頭到底包含了哪些東西,當你拿出 array 裡頭的物件呼叫 method 時,就可能出現找不到 selector 的錯誤。比方說,你呼叫了某個 RESTful API,以為某個 key 應該會對應到一個 NSString 字串物件,結果你拿到了 NSNull,程式就跳出 exception 而 crash 了。

每個 Objective-C 物件的變數,不管宣告成什麼 Class,或是 cast 成 id,都可以指向 nil。在大部分狀況下把變數宣告成 nil 相當安全,因為對 nil 呼叫任何 Objective-C method,就只是沒有作用而已;如果是手動管理記憶體,我們甚至會認為把某個已經釋放的物件的變數指向 nil 是個好習慣,因為對 nil 呼叫 release 不會有事,但如果沒指向 nil,再一次呼叫 release,就會出現 bad access 錯誤。但,如果我們想對 Mutable 的 Collection 物件插入 nil,例如呼叫 NSMutableArray 的 addObject: ,對 NSMutableDictionary 呼叫 setObject:forKey:,傳入 nil 會造成 crash。

而說到底 Objective-C 底下還是 C,所以所有 C 的危險操作,把指標指到 C Array 長度外面什麼的,在 Objective-C 統統都可以做。

拉拉雜雜說到這裡,從 WWDC Keynote 那天拿到 beta 版本的 Xcode,開始學 Swift 一個月以來,到目前為止我的印象是—Swift 這個語言要解決的看來不是如何讓新手更快進入 iOS/Mac OS X 開發,因為很多地方看起來比 Objective-C 更加困難,並且假設你應該有 C/Objective-C 的基礎;雖然你第一眼看過去,可能會覺得語法像是某些 scripting language,但是實際寫起來你感受到的大概不是自由揮灑,而是很多地方限制重重。Swift 其實是個頂麻煩的強型別語言,所關心的,看來是如何讓開發者寫出更安全的程式。

  • Objective-C 的 Collection 可以任意傳入 id,Swift 導入了 Generics。
  • Objective-C 的變數可以任意指向 nil,Swift 於是有 optional 這項設計。
  • Swift 可以使用指標,但是 Swift 又將指標包裝了一層。

以下會簡單說明一下這些特性。不過,由於 Swift 畢竟還是個剛面世不久的語言,到目前為止,還有一堆讓人很想撞牆的問題…。

Generics

在蘋果自己的電子書中,主要強調 Generics 這項用一個抽象型別對應到多種可能的型別的設計,可以用在像 swap 這樣的 function 上:當我們想要交換兩個東西的時候,重要的是用來互相交換的兩者應該屬於同一個型別,但不需要一定得是整數或字串等某種特定的型別。而我自己的看法是,我們可以約束像是 Array 或 Dictionary 等 Collection 當中每一個物件都屬於同一個型別,不像 Objective-C 裡頭都是 id,會更為重要。

說到 swap。Swift 所提供的 swap function 看來也可以用來交換一個 Array 當中不同位置的東西,可是呢,如果你在一個多層的 Array 裡頭使用 swap,就會出現不可預期的結果。

1

而即使我們可以使用 Generics 要求 Collection 當中都要是同一種型別的物件,但很多時候 Collection 裡頭會有什麼東西,還是不可預期。

Swift 仍然有像 Objective-C 裡頭 id 的東西,叫做 AnyObejct,而如果你在 Swift 中 import 了 Foundation Framework 的話,又有 NSObject 可以用。所以,如果你宣告一個多維的 Array,如果你不沒有特別宣告這個 Array 到底是什麼型別的話,你原本以為應該是 [AnyObject],但很有可能就變成了 [NSObejct] 或 NSArray—這樣很討厭,像 [AnyObject] 或 [NSObject] 是 mutable 的,可以改變 Array 裡頭的內容,但 NSArray 是 immutable 的,宣告一行之後,到底是 mutable 還是 immutable 都搞不清楚。

2

不過就這方面來說,Xcode 6 beta 3 之後相較於前兩個 beta 版本,至少有一些改進。Xcode 6 beta 與 beta 2 裡頭如果宣告多維的 Array 會直接 crash。

Optional

Swift 語言在語法就嚴格限制變數是否可以指向 nil。簡單來說,一個變數是否可以指向 nil,必須先宣告成是 optional,例如我有一個數字叫做 i,但是 i 需要可以指向 nil,我就不能夠寫成 var i:Int,而是再加上個問號,寫成 var i:Int?。

3

一個 optional 的變數會被包裝在一個結構當中,這個結構有兩種狀態:Some 或 nil,如果我把上面剛剛那個變數 i 指向 10,i 現在不是直接指向 10,而是 10 被包裝(wrap)在 Some 裡頭的 [Some 10],如果我們要從 [Some 10] 裡頭取出 10,則需要一步 unwrap,unwrap 有幾種方式,可以用一個變數 bind 上去,或是可以用一個驚嘆號強迫 unwrap 取值。

規則雖然很簡單,但是當你實際寫過一次之後,就會發現 wrap/unwrap 的時機多得不得了,你手上沒有什麼確實的數字,但是就直覺上,你覺得寫 wrap/unwrap 的次數,大概也跟你在沒有ARC 之前寫 retain/release 的次數差不多。我覺得我強迫自己寫 Swift 一個半月下來,大概習慣了 wrap/unwrap 的語法—這種記得什麼跟什麼要成對這種事情,最後大概不是靠記憶學,要用身體學,讓手指習慣曾經打過一個 optional 之後就要用 unwrap—但學習過程中還是遇到了幾個障礙。但最強烈的感受還是,我們不是告別了 retain/release,怎麼又來了一個要成對的東西?

遇到的障礙中,首先是,在一行程式當中,其實就會有好幾個地方可能出現 optional—一個該指向某種型別的物件的變數可能會指向 nil,而這個物件的某個 method 可能回傳 nil,結果這一行程式裡頭就出現了一大堆問號驚嘆號。剛開始寫,會完全不知道自己在寫什麼,如果再搭配一個 as 關鍵字,可以讓程式更加意味不明。

4

練習到這邊,我曾經試著用比較暴力的方式,試試看舉凡遇到了 optional 的場合,統統都用驚嘆號強迫取值。結果是,這樣寫程式非常危險,因為一個變數指向 nil 後,強迫取出 nil 然後呼叫一些 method,會跳出 nil 不能執行這些 method 的 Exception 出來。說到 Exception 也很妙,Swift 可以呼叫 Cocoa Framework,Cocoa Framework 中有 NSException 物件,但是 Swift 卻沒有 try…catch 語法。

比較安全的寫法還是另外用一個變數 bind 到原本的 optional 變數,照著蘋果電子書裡頭的範例,我們的變數 i 是 optional 的時候,可以寫一個「if let ii = i」,如果這個判斷式成立,代表我們順利的從 i 裡取出 Some 當中的值,可以安全地使用 ii。但這樣寫,又會有另外一個困擾:命名。

我大概還知道一開始想用的變數該如何命名,但是一個「為了從某個 optional 變數取值而出現的新變數」,你實在想不出來該用什麼名字,使用 i 到一半會出現一個意味不明的 ii。在國外的討論中,現在似乎認為用原本的變數名稱做 binding 會是比較好的方式,但其實寫一個「if let i = i」,乍看之下,如果不了解這一層邏輯,那還是很意味不明。

5

我在寫 Swift 第一天最撞牆的地方,是在寫一個 optional 的 bool。bool 平常會有 true 跟 false 兩種狀態,而如果是 bool?,就會有 [Some true]、[Some false] 與 nil 三種狀態。假如我現在有個變數叫做 b,型別是 bool,當我寫了「b = false」,然後寫一個判斷式「if b {}」,這樣會因為 b 是 false 而不會走進去;但如果 b 的型別是 bool?,將 b 設成 false 後,會是 [Some false],「if b {}」就會是成立的。

6

指標操作

Swift 本身沒有指標語法,但是可以操作指標,或這麼說—如果我們要使用 Cocoa 或 Cocoa Touch Framework 裡頭的東西,就一定會遇到指標,而基本上不呼叫這些 Framework 等於是沒辦法寫個 Mac 或 iOS 應用程式出來。而你會在這邊遇到的麻煩呢,就是 Swift 語言本身還在改來改去。

大概是在 Xcode 6 beta 2 推出的時候,蘋果推出了第二本 Swift 電子書,裡頭講如何使用 Swift 語言呼叫 Cocoa Framework;裡頭提到,Swift 在沒有指標語法的狀況下,會將幾種不同的 C 指標,包裝成 CConstVoidPointer、CMutableVoidPointer、CConstPointer、CMutablePointer…等等 structure,另外還定義了幾種雙指標,我們透過這些 structure 操作指標—當你一開始寫,完全搞不清楚到底應該要用哪一種。

7

接著,在後面幾版的 beta 中,這幾個 struct 又全部被拿掉了,變成只剩下 UnsafePointer 這個 structure—要趕上每個 beta 版本之間的變化,心臟實在要很強才行。像到了 beta 4 的時候,怎麼又突然跑出了 public、private 這些 access control 關鍵字出來…。

C 語言的 Array 是連續的記憶體位置,所以在寫 C 語言的時候,我們經常透過移動指標,讀取整個 Array 的內容;Swift 沒有指標語法,也沒有移動指標這件事情,作法是透過原本建立的 UnsafePointer,再建立出一個 UnsafeArray—蘋果的電子書居然完全沒有講這件事情耶。

總之,如果你想寫一個「把一段資料裡頭每個 byte 都做一次 xor」這種最簡單的加密,寫出來大概是這樣:

8

我們一開始會覺得 Swift 語法比較簡潔,但看來,只要遇到指標操作,顯然不是這樣。

而現在這邊還有一個大問題—有一些蘋果原本的 API 需要傳入 C Function Pointer 做 callback,尤其是 Core Audio API 裡頭有一大堆這種東西。目前 Swift 裡頭有 CFunctionPointer,但是你還是完全不知道怎麼建立 CFunctionPointer,而目前也找不到有什麼方法可以把 Swift function 轉成 CFunctionPointer—哪要怎樣用 Swift 呼叫這些 API 呢?

該怎麼說呢?

學 Swift 的時候,一開始會讓你有種看到其他語言的親切感,你在 C++、Java 看過 Generics,宣告 function 回傳型別的地方看起來像 Haskell,然後人家說跟 Swift 最接近的語言是 Mozilla Foundation 的 Rust,所以你又去稍微瞄了一下這些語言。

然後是興奮—除了 class 之外,無論是 struct 或 enum 都可以增加 method,還可以直接用 extension 擴充;可以發明自己的 operator,operator 可以 overload,Swift 的匿名函式 closure 比起 Objective-C 的 block 好上好幾倍,你好久以前就好想要這些東西。

但接下來又是強烈的挫折感。像前面說的,在 optional 這個地方就先撞上個一堵牆,在前面幾個 beta 版中,隨隨便便寫一段 code 都會當—Array 裡頭還有 Array,當,寫一個變數是 Optional 的 Generics,把型別設成 T?,當。幾天下來你能想到的都是 WTF 這幾個字,看到跟指標有關的部份,就更 WTF 了。就像過去蘋果推出的技術,看起來沒擺個一年還是先別用,你回想起前幾年傻呼呼的真的拿蘋果的 GC 寫了一個糟糕的專案這件事。

可是蘋果的東西就是這樣,等到雷都被踩得差不多的時候,突然間就所有人都在用了,所以既然把這個平台當做是自己吃飯的工具,有些雷這個時候還是非得來踩一踩不可。這個階段看來還不能夠拿來寫產品 code,但至少還是可以拿來寫一些自己要用的小工具。

我非常懷疑可以在沒有 C/Objective-C 基礎上,可以直接學 Swift。就像上面講的,Swift 的使用 structure 包裝 C 指標,那麼還是得要有 C 的基礎才行,Swift 裡頭還是有 calloc、free 這些東西,而某些雙指標則又是沿自 Objective-C 裡頭的記憶體管理的慣例。

說到慣例,Swift 這個語言還有個問題,就是還沒有一份整理好的 Swift Design Pattern。雖然像是 Singleton、Delegate、KVO 應該都不會改變,但,應該怎麼寫?

在 Objective-C 裡頭,delegate 應該要是 weak 的,但是 Swift 不允許一個型態只宣告成某個 protocol 變數是 weak,而看看蘋果自己的 API,也沒有把 delegate 設成 weak。Singleton 又要怎麼寫呢?現在大多數人都按照 Mike Ash 的文章,使用 dispatch_once 實作 singleton,但據說 Swift 裡頭用 let 關鍵字宣告的變數不但是唯讀,也做過了一次 dispatch_once,那公認的標準實作應該怎麼做呢?有公認的標準實作嗎?

One thought on “Swift

  1. Pingback: Swift 2.2 – Thinking more…

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.