Codementor Events

Swift Syntactic Sugar 偏方可恥但有用

Published May 25, 2017Last updated May 26, 2017
Swift Syntactic Sugar 偏方可恥但有用

Syntactic sugar is syntax within a programming language that is designed to make things easier to read or to express

Swift在設計上,有許多的syntactic sugar,也就是簡化過方便使用的語法。syntactic sugar對於初學者來講,有時候是syntactic poison,因為通常各種syntactic sugar寫起來都不是很直觀,甚至還常會有符號摻雜在裡面,每個語言的syntactic sugar也都不盡相同,如果不事先了解,讀code起來就會非常痛苦。但這不代表syntax sugar就會讓程式碼可讀性下降,相反地,syntactic sugar有點像是講同樣語言的程式設計師之間的行話,你會講某個語言,但是卻不會行話,也還是會讓人家覺得你是外國人;但只要你能理解這個領域的行話,你就可以更快進入狀況,也能寫出讓大家更快理解的程式出來。

以下就介紹幾個Swift的syntactic sugar,以及一些實用pattern:

Ignoring symbol “_”

一個底線在Swift代表的是”忽略”,通常被使用在不需要的變數上面。假設我們想要印出一個dictionary裡面的key跟value,我們會寫成如下:

for (key, value) in aDic {
  print("\(key)\(value)")
}

這樣可以簡單取用dictionary裡面的key跟value。但如果我們只需要key呢?

for (key, _ ) in aDic {
  print("\(key)")
}

我們可以在原本value該出現的地方,改成只有一個”_”,那個參數位置還是會被unwrap,但是因為我們需要它,所以我們就把它丟到”_”這個黑洞裡,就不用再另外找個變數把它裝起來了。
這個語法設計的蠻巧妙的,不是非常麻煩的寫法,但卻可以解決以前ObjC回傳值語法上的模糊性,也可以讓一些不被使用的參數直接隱藏,減少干擾並增加可讀性。
(當然在上面這個case,有個更棒的寫法是直接針對aDic.keys做迴圈,不過解釋”_”的概念應該是足夠的)

Argument Labels

我們在定義一個function時,通常會這樣寫:

func ambulate( startCity: String, endCity: String) {
  print("Tom walked from \(startCity) to \(endCity)")
}

在取用時,就會寫成這樣:

ambulate(startCity: "Taipei", endCity: "Tainan")`

但這樣的function呼叫,念起來不是很直觀,所以Swift提供了幫參數增加label的方式,寫法如下:

func walk(from startCity: String, to endCity: String) {
  print("Tom walked from \(startCity) to \(endCity)")
}

在呼叫的時候,就可以寫成:

walk(from: "Taipei", to: "Tainan")

代表Tom從台北走到台南,是不是相當直觀?讓我們為Tom鼓鼓掌!
再來,如果我們把參數別名,設定成代表省略的”_”,如下:

func walk(_ startCity: String, _ endCity: String) {
  print("Tom walked from \(startCity) to \(endCity)")
}

在取用函式的時候,就可以完全不用寫參數名稱:

walk("Taipei", "Tainan")

當然,這樣的寫法可讀性就不會太好,通常適用在不太會被重覆使用的function上,如果是比較泛用的function,就強烈建議要把參數名稱清楚地寫出來,以增加程式的可讀性。

Range Operator

這是一個基本語法,寫成如下:

a…b

中間是三個’.’,代表的是一個iterator從a到b(包含b),比方說 1…4 代表的就是 1, 2, 3, 4
另外,從a到b但不包含b的寫法如下:

a..<b

這常見於for loop的使用,若我們想針對array的每個元素進行訪問,同時又想要拿到index,可以寫成如下:

for i in 0..<arr.count {
  print("Index: \(i), element: \(arr[i])")
}

這樣既可以取index,也可以訪問原本array的元素。

PS. 當然,上面這種需求還有語意上更清楚的做法:

for (i, ele) in s.enumerated() {
  print("Index: \(i), element: \(ele)")
}

用”enumerated()”這個array的method,可以針對array中的每個元素都回傳index與元素本身,這樣的寫法好處就是index跟element都清楚地列在iterator的前面,較先前的寫法更一目瞭然。當然一切都還是看寫code當時的需求而定,沒有絕對的對錯喔。

Nil Coalescing

在unwrap optional時,通常我們會使用這樣的語法:

var op: String?

/* do something */

if let x = op {
  print(x)
}else {
  print("")
}

其中op是一個optional的變數,在經過一些計算之後,op可能有值也有可能為空,所以我們會用if let語法來unwrap它成為一個區域變數,來取用有值的x,或是取用預設值空字串。這樣的寫法很明確但是稍微有點繁瑣。所以Swift提供了一個unwrap運算子”??”,寫法如下:

let x = op ?? ""
print(x)

這邊代表的是,我想要把op unwrap成為區域變數x,如果op是空的,那就給它一個預設值空字串,這樣寫比上面那樣簡潔很多,在預設值是固定時相當有用。要注意的是,如果這些變數都是屬於你的模組,那把預設值寫在init裡會是更好的選擇,但如果變數是第三方,或是一個ObjC的模組,這樣的pattern就會顯得相當有用。

Closure

Closure這個概念在現代語言中佔有非常重要地位,而在Swift中,有許多的syntactic sugar讓closure寫起來更簡潔、更能夠自我描述,以下就介紹幾個跟closure有關的syntactic sugar:

The Trailing Closures

這是一個神奇的特性,如果一個function的最後一個參數是closure,那你可以不用把這個closure包在”()”括號裡面,下面是一個以closure當參數的function:

func findingNemo( gotcha: () -> Void ){
  gotcha()
}

一般呼叫方式:

findingNemo(gotcha: {
  print("I got Nemo!")
})

但因為這個closure是最後一個參數,所以你可以不用把它包在”()”裡面,也不用指定參數名稱。效果一樣但是更簡潔了。

findingNemo {
  print("I got Nemo!")
}

再讓我們看另外一個例子。

Swift針對array,提供了非常好用的map、reduce、filter等等function,以下是一個reduce的範例:

let s = [1, 2, 3, 4, 5]
s.reduce(0, { (result, e) -> Int in
  return result + e
}) // Result is 1+2+3+4+5

reduce的用途是,針對array中的每一個元素,都做某一種操作,並且把運算結果存下來,讓下一個元素運用。上面這段程式,就是把0當初始值,並且依序把array中的數字都加起來。在邏輯上是經過這樣的運算:

  1. result = 0 + 1
  2. result = result + 2
  3. result = result + 3
  4. result = result + 4
  5. result = result + 5

最後result等於這個array的總和15。

這樣的函式,很常把後面的那個closure寫成簡單的樣式:

s.reduce(0) { (result, e) -> Int in
  return result + e
}

然後其實回傳的型別是可以被省略的:

let a = [1, 2, 3, 4, 5]
a.reduce(0) { (result, e) in
  return result + e
}

看起來.....好像還是跟原本差不多XD
這樣的pattern好處是最後一個closure常被定義為callback,把它視覺上跟前面的獨立出來,會比較簡潔且直觀。

好我知道不太有說服力,下面再介紹一個救援。

Shorthand Argument Names

在Swift裡,傳進closure的參數,可以被依序用一些醜醜的預設參數名來取用,如果一個closure有兩個參數,那在這個closure之中,就可以用**$0$1**來依序代表這兩個參數,所以上面的程式可以改寫成這樣:

s.reduce(0) {
  return $0 + $1
}

就把輸入的參數整個拿掉了。如果一個closure裡面只有一行code,並且那一行的執行結果就是回傳值,那上面的code可以再進一步簡化成這樣:

'' s.reduce(0) { $0 + $1 }

讓我們再看看原本的code:

s.reduce(0, { (result, e) -> Int in
  return result + e
}) 

是不是差超多!?
這也很好地說明了各種語言的syntactic sugar就是個兩面刃,如果你對這語言不熟,或是不管可讀性全部都用這種炫技式寫法,可能下場就是沒人看得懂你的code。相反地,如果你熟悉這些特性,並且適時地利用它,那它會讓程式既簡傑可讀性又大幅上升。

以上就是今天的Syntactic sugar介紹,雖然簡單但是初學時還是被許多特別的語法搞混了,所以整理了一下,希望對想學Swift的人有一點用處。有錯也歡迎指正喔!

PS. 最近認真學習日文,能看到這行表示你毅力驚人,可以分享一下怎樣堅持把一個語言學好的技巧嗎?
PS 10. 下一篇會寫一些比較完整的UI元件,歡迎支持
PS 11. 研究wordpress的各種設定中,有心得也歡迎分享,也有考慮過把文章擺在medium,但跟我原本medium的文章調性完全不合....

Discover and read more posts from ShihTing Huang (Neo)
get started