第六章——函式(序)

bestswifter發表於2017-12-27

本文系閱讀閱讀原章節後總結概括得出。由於需要我進行一定的概括提煉,如有不當之處歡迎讀者斧正。如果你對內容有任何疑問,歡迎共同交流討論。

本章主要是介紹函式、閉包相關的基礎知識。如果你對此已有了解可以直接跳過

關於函式,主要有三個重要的概念:

  1. 函式也是一種型別,和IntString型別等類似,函式可以被賦值給變數,也可以作為引數傳入、傳出別的函式。
  2. 函式體內,不僅可以使用它的引數,還可以使用函式外部被截獲的變數
  3. 建立函式有兩種方法:用func關鍵字或閉包表示式{ }

接下來詳細解釋這三個概念:

1. 函式可以賦值給變數,也可以作為引數傳入、傳出別的函式

在Swift和很多現代語言中,函式也是“一等公民”。它可以被賦值給變數,也可以作為引數傳入、傳出別的函式。理解這一點很重要,這是函數語言程式設計的基礎,就像指標在C語言中那麼重要。我們通過一段程式碼來演示函式賦值給變數:

//定義了一個函式
func printInt(i: Int) {
print("you passed \(i)")
}

//函式賦值給變數,函式名後不用加括號
let funVar = printInt

//通過函式名呼叫函式,需要傳入引數
funVar(2)   //輸出結果:“you passed 2”
複製程式碼

函式也可以作為別的函式的引數來用:

// 這個函式的引數是一個 (Int) -> ()型別 的函式
func useFunction(funcParam: (Int) -> ()) {
funcParam(3)
}

//把之前的函式傳入,作為引數。輸出結果相同,都是:“you passed 3”
useFunction(funVar)
useFunction(printInt)
複製程式碼

掌握了這一點,我們就可以寫出很多有用的高階函式,這在第三章——“集合”中有所體現。

2. 函式體內可以使用它截獲的,位於函式外部的變數

我們先看一個例子,把函式作為另外一個函式的返回值:

// 返回的函式型別是:(Int) -> String
func returnFunc() -> (Int) -> String {
func innerFunc(i: Int) -> String {
return "you passed \(i) to the returned func"
}
return innerFunc
}

let myFnc = returnFunc()
print(myFnc(4))

//輸出結果:“you passed 4 to the returned func”
複製程式碼

如果函式引用了函式體外部的變數,我們說這個變數被函式“截獲”了。變數在超出自己的作用域後不會被釋放,因為還有這個函式引用著它。我們把剛剛的函式修改一下,新增一個count變數:

func returnFunc() -> (Int) -> () {
var count = 0
func innerFunc(i: Int) {
count += i
print("running total is now \(i)")
}
return innerFunc
}
複製程式碼

每次呼叫returnFunc函式都會獲得一個不同的函式:

let f = returnFunc()
f(3)	// 會輸出:“running total is now 3”
f(4)	// 會輸出:“running total is now 7”

let g = returnFunc()
g(2)	// 會輸出:“running total is now 2”
g(2)	// 會輸出:“running total is now 4”

f(2)	// 會輸出:“running total is now 9”
複製程式碼

你可以把截獲了變數的函式理解為一個類,這個類有一個方法(就是這個函式自身)和多個成員變數(被截獲的變數)

3. 閉包表示式的{ }語法

除了用func關鍵字建立函式之外,還可以用{ }語法建立函式。先看一個普通的函式:

func doubler(i: Int) -> Int { return i * 2 }
let a = [1, 2, 3, 4].map(doubler)  //a = [2, 4, 6, 8]
複製程式碼

等價的寫法是:

let doubler = { (i: Int) -> Int in return i * 3 }
let a = [1, 2, 3, 4].map(doubler)  //a = [3, 6, 9, 12]
複製程式碼

用閉包表示式定義的函式,可以理解為函式的“字面量”。就像1"hello"分別是IntString型別的字面量一樣。與用func關鍵字不同的是,用閉包表示式定義的函式是匿名的。如果想使用它,可以把它賦值給某個變數。

這裡我寫了return i * 3並非寫錯,而是表示新定義的doubler會替換之前的doubler,所以輸出結果中陣列的每個元素都是之前的三倍。這也說明了用func關鍵字和閉包表示式建立的函式是完全等價的。

閉包表示式的作用主要是在於簡化定義函式的方式。比如之前的double可以做如下簡化:

let a = [1, 2, 3, 4].map { $0 * 2 }  //a = [2, 4, 6, 8]
複製程式碼

如果使用閉包表示式,根本沒有必要建立doubler函式,就可以完成map。接下來我們以原來複雜的程式碼為例,一步一步詳細描述簡化過程:

let doubler = { (i: Int) -> Int in return i * 2 }
let a = [1, 2, 3, 4].map(doubler)  //a = [3, 6, 9, 12]
複製程式碼
  1. 函式作為引數傳入別的函式中時,沒有必要把這個函式先賦值給一個臨時變數,再把臨時變數傳入到別的函式中。我們可以直接把函式寫在別的函式中。簡化結果:
let a = [1, 2, 3, 4].map({ (i: Int) -> Int in return i * 2 })
複製程式碼
  1. 不必指定編譯器能根據上下文推匯出的型別。比如在這個例子中,陣列的元素是Int型別,所以傳入到map方法中的函式的引數型別也是Int,由於函式中進行了乘法運算,所以返回值型別必然也是Int。簡化結果:
let a = [1, 2, 3, 4].map({ i in return i * 2 })
複製程式碼
  1. 如果閉包內只有一條語句,那麼顯然返回值就是這條語句的值,return關鍵字就可以省略。簡化結果:
let a = [1, 2, 3, 4].map({ i in i * 2 })
複製程式碼
  1. 對於傳入的引數,可以不指定具體的名字。預設就是$0表示第一個引數,$1表示第二個引數……簡化結果:
let a = [1, 2, 3, 4].map({ $0 * 2 })
複製程式碼
  1. 如果作為引數的函式是別的函式的最後一個引數,它可以寫在函式的括號外面。這就是“尾閉包”語法。簡化結果:
let a = [1, 2, 3, 4].map() { $0 * 2 }
複製程式碼
  1. 因為map函式沒有別的引數,所以可以省略括號。簡化結果:
let a = [1, 2, 3, 4].map { $0 * 2 }
複製程式碼

這樣的寫法一開始可能不太熟悉,但是習慣之後就會發現它在不損害程式碼可讀性的前提下,極大的簡化了程式碼量。新手在簡化閉包表示式時,可能會遇到各種錯誤。這時候最好先寫出它的完整表示式,然後按照上述規則一步一步簡化,直到遇到錯誤為止。這時再好好考慮當時是哪裡犯了錯。

標註函式型別

Swift會自動推導閉包表示式的型別,但有時我們也需要顯式的標出。比如這個函式:

let isEven = { $0 % 2 == 0 }
複製程式碼

它會被自動推導為Int -> Bool型別的。如果我們需要人為指定它的型別,可以這麼寫:

let isEven = { (i: Int8) -> Bool in i % 2 == 0 }
複製程式碼

不過,在閉包內標註型別不是一個好方法,更優雅的解決方案是在閉包外標註型別:

var isEven: Int8 -> Bool = { $0 % 2 == 0 }
// 或者
let isEven = { $0 % 2 == 0 } as Int8 -> Bool
複製程式碼

最好的,也是比較複雜的解決方案是定義一個範型的isEven方法:

func isEven<T: IntegerType>(i: T) -> Bool {
return i % 2 == 0
}

let int8isEven: Int8 -> Bool = isEven
複製程式碼

總結

{ }閉包表示式和func關鍵字建立的都是函式,它們是等價的。而閉包是指截獲了外部變數的函式。因此,嚴格意義上來說閉包和閉包表示式是兩個不同的概念。閉包表示式就是函式,而閉包是可以截獲外部變數的函式。

相關文章