使用func和closure加工資料(一)

weixin_34075551發表於2017-08-22

使用func和closure加工資料(一)

[TOC]

函式的返回值以及靈活多變的引數

作為開始,我們就簡單的快速學習一下Swift中函式的基本要素,這將是我們接下來所有內容的基礎。

一個簡單的函式

一個簡單的函式看上去是這個樣子的:

func printName() {
    print("My name is Mars")
}

其中:

  • func是定義函式的關鍵字,後面是函式名;
  • ()中是可選的引數列表,既然是最簡單的函式,自然我們可以讓它留空;
  • ()後面,是函式的返回值,同樣,簡單起見,我們也沒有定義返回值
  • {}中是函式要封裝的邏輯,其實,在這裡,我們呼叫print,也是一個函式,只不過,它是一個定義在標準庫中的函式,並且帶有一個引數罷了

向函式傳遞引數

我們定義一個計算兩個整數的乘機:

func mul(m: Int, n: Int) {
    print(m*n)
}

然後我們通過下面這樣來使用mul:

mul(m: 2, n: 3) // 6

為引數設定預設值

我們在宣告函式時,經常會遇到處理引數的預設值問題。它可以用來約束函式的預設行為,或者簡化大多數時候都會傳遞的值。例如:

func mul(_ m: Int, of n: Int = 1) {
    print(m*n)
}

當我們使用的時候:

mul(2) //2

擁有預設值的函式引數必須從右向左依次排列,有預設值的引數不能出現在無預設值的引數的左邊。

定義可變長引數

接下來,如果我們要計算不確定個數引數的乘積該怎麼辦呢?Swift還允許我們通過下面的方式,定義可變長度的引數列表:

func mul(_ numbers: Int ... ) {
    let arrayMul = number.reduct(1, *)
    print("mul: \(arrayMul)")
}

在上面的例子中,我們用numbers:Int ...的形式,表示函式可以接受的Int引數的個數是可變的。實際上,numbers的型別,是一個Array<Int>,因此,為了計算乘積,我們直接使用Array型別的reduce方法就好了。
定義好以後,我們可以這樣呼叫它:

mul(2, 3, 4, 5, 6, 7) //5040

定義inout引數

Swift裡,函式的引數有一個性質:預設情況下,引數是隻讀的,這也就意味著:

  • 你不能再函式內部修改引數值;
  • 你也不能通過函式引數對外返回值;
func mul(result: Int, _ number: Int ...) {
    result = numbers.reduce(1, *) //!!! Error Here !!!
    print("mul: \(arrayMul)")
}

在上面的實現裡,函式的引數預設是個常量,因此編譯器會提示你不能再函式內部對常量賦值。然後再來第二條:如果我們希望引數可以被修改,並且把修改過的結果返回給傳遞進來的引數,該怎麼辦呢?

其實,很簡單,我們需要用inout關鍵字修飾一下引數的型別就ok了!明確告訴Swift編譯器我們要修改這個引數的值:

func mul(result: inout Int, _ number: Int ...) {
    result = numbers.reduce(1, *)
    print(mul: \(result))
}

然後就可以這樣使用mul

var result = 0
mul(result: &result,2,3,4,5,6,7)
result //5040

對於inout型別的引數,我們在呼叫的時候,也需要在引數前明確使用&.這樣,mul執行結束後,就可以看到result的值變成了5040

通過函式返回內容

當然,通過引數來獲取返回值只能算函式的某種副作用,更"正統"的做法應該是把返回值放在函式的定義裡,像這樣:

func mul(_ number: Int ...) -> Int {
    return numbers.reduce(1, *)
}

我們通過->Type的方式,在引數列表後面定義返回值。然後,就可以用mul的返回值來定義變數了:

let result = mul(2,3,4,5,6,7) // 5040

函式和Closure真的是不同的型別麼?

提起closure,如果你有過其他程式語言的經歷,你可能會立即聯想起一些類似的事物,例如:匿名函式、或者可以捕獲變數的一對{}等等。但實際上,我們很容易搞混兩個概念:Closure expression和Closure。他們究竟是什麼呢?我們先從closure expression開始。

理解Closure Expression

簡單來說,closure expression就是函式的一種簡寫形式。例如,對於下面這個計算引數平方的引數:

func square(n: Int) -> Int {
    return n*n
}

我們也可以這樣來定義:

let squareExpression = { (n: Int) -> Int in
    return n*n
}

在呼叫的時候是完全相同的:

square(2) // 4
squareExpression(2) // 4

並且它們都可以當做函式的引數來使用:

let numbers = [1, 2, 3, 4, 5]
numbers.map(square) // [1, 4, 9 ,16, 25]
numbers.map(squareExpressions) // [1, 4, 9 ,16, 25]

我們在這個例子裡,用於定於squareExpressions{}就叫做closure expression,它只是把函式引數、返回值以及實現統統寫在了一個{}裡。
沒錯,此時的{}以及squareExpressions並不能叫做closure,它只是一個closure expression。那麼,為什麼要有兩種不同的方式來定義函式呢?最直接的理由就是,為了寫起來更簡單。Closure expression可以再定義它的上下文裡,被不斷的簡化,讓程式碼儘可能的呈現出最自然的語義形態。

例如,當我們把一個完成的closure expression定義在map引數裡,是這樣的:

numbers.map ({ (n: Int) -> Int in 
    return n * n
})

首先Swift可以根據numbers的型別,自動推匯出map中的函式引數以及返回值的型別,因此,我們可以在closure expression中去掉它:

numbers.map ({ n in return n * n })

其次,如果closure expression中只有一條語句,Swift可以自動把這個語句的值作為整個expression的值返回,因此,我們還可以去掉return關鍵字:

numbers.map ({ n in n * n })

第三,如果你覺得在closure expression中為引數起名字是個意義不大的事情,我們還可以使用Swift內建的$0/$1/$2/$3這樣的形式作為closure expression的引數替代符,這樣,我們連引數宣告和in關鍵字也可以省略了:

numbers.map ({ $0 * $0 })

第四,如果函式型別的引數在引數列表的最後一個,我們還可以把closure expression寫在()外面,讓它和其它普通引數更明顯的區分開:

numbers.map(){ $0 * $0 }

最後,如果函式只有一個函式型別的引數,我們甚至可以再呼叫的時候,去掉():

numbers.map { $0 * $0 }

看到這裡,就應該知道當我們把closure expression用在它的上下文裡,究竟有多方便了,相比一開始的定義,或者單獨定義一個函式,然後傳遞給它,都好太多。但事情至此還沒結束,相比這樣:

numbers.sorted(by: {$0 > $1}) // [5,4,3,2,1]

closure expression 還有一種更簡單的形式:

numbers.sorted(by: > ) // [5,4,3,2,1]

這是因為,numbers.sorted(by:)的函式引數是這樣的:(Int ,Int) -> Bool,而Swift為Int型別定義的>操作符也正好和這個型別相同,這樣,我們就可以直接把操作符傳遞給它,本質上,這和我們傳遞函式名是一樣的。

另外,除了寫起來更簡單之外,closure expression還有一個副作用,就是預設情況下,我們無法忽略它的引數,編譯器會對這種情況報錯。看個例子,如果我們要得到一個包含了10個隨機數的Array,最簡單的方法,就是一個CountableRange呼叫map方法:

(0 ... 9).map { arc4random() } // Error in Swift

這樣看似很好,但是由於map的函式引數預設是帶有一個引數的,在我們的例子裡,表示range中的每個值,因此,如果我們在整個closure expression裡都沒有使用這個引數,Swift編譯器就會提示我們錯誤。

我們不能預設忽略closure expression中的引數,如果堅持如此,我們必須用_明確表明這個意圖:

(0 ... 9).map { _ in arc4random() } 

這也算是Swift為了型別和程式碼安全,利用編譯器,為我們提供的一層保障。以上,就是和closure expression有關的內容,如你看到的一樣,它就是函式的另外一種在上下文中更簡單的寫法和用func定義的函式沒有任何區別。

究竟什麼是closure

如果我們翻翻Wikipedia,就能找到下面的定義:a closure is a record storing a function together with an environment。
說的通俗一點,一個函式加上它捕獲的變數一起,才算一個closure。來看個例子:

func makeCounter() -> () -> Int {
    var value = 0
    return { 
        value += 1
        return value
    }
}

makeCounter()返回一個函式,用來返回它被呼叫的次數。然後,我們分別定義兩個計數器,並各自呼叫幾次:

let counter1 = makeCounter()
let counter2 = makeCounter()

(0...2).forEach { _ in print(counter1())} // 1 2 3
(0...5).forEach { _ in print(counter2())} // 1 2 3 4 5 6

這樣,三次呼叫counter1()會在控制檯列印"123",6次呼叫會列印“123456”。這說明什麼呢?

首先,儘管從makeCounter返回後,value已經離開了它的作用域,但我們多次呼叫counter1counter2時,value的值還是各自進行了累加。這就說明,makeCounter返回的函式,捕獲了makeCounter的內部變數value。
其次,counter1counter2分別有其各自捕獲的value,也就是其各自的上下文環境,他們並不共享任何內容。

理解了closure的含義之後,我們就知道了,closure expression和closure並不是一回事兒。然後,捕獲變數是{}的專利麼?實際上也不是,函式也可以捕獲變數。

函式同樣可以是一個Closure

還是之前makeCounter的例子,我們把返回的closure expression改成一個local function:

func makeCounter() -> () -> Int {
    var value = 0
    return increase() -> Int {
        value += 1
        return value
    }
    return increase
}

然後你就會發現,之前counter1counter2的例子的執行結果,和之前是一樣的:

(0...2).forEach { _ in print(counter1()) } // 1 2 3
(0...5).forEach { _ in print(counter2()) } // 1 2 3 4 5 6

所以,捕獲變數這種行為,實際上,跟用{}定義函式也沒關係。

相關文章