第五章 Functions and Closures - 函數和閉包方法

函數是現代編程最重要的基礎之一。他們允許你將要執行的特定的任務邏輯封裝在一個單元裏,可實現封裝重用。封裝也可以是執行抽象層的封裝,允許你和你的團隊成員將其作爲一個獨立的“黑盒”而不用操心內部具體實現細節。

Swift支持全局的函數和方法,函數關聯的類和其他的類型等。Swift還支持閉包,類似於匿名函數的表達式。

在這一章中,你將進一步探索函數,他們的語法以及基本類型等。你將看到輸入輸出參數對類和結構參數的影響。你還會看到Swift的方法和函數參數的命名一直受Object-C的影響。最後,你將瞭解到簡潔且靈活的閉包表達式的語法,這也是爲什麼Swift會被稱爲是一種函數式語言的原因。

Functions - 函數

在這一章中,你將繼續探索Swift函數的強大和多功能性。但在你暈頭轉向前,先來做點簡單的小的例子。

Your first function - 你的第一個函數

生成一個新的playground並添加代碼如下:

import Foundation 
let a = 3.0, b = 4.0

let c = sqrt(a * a + b * b) 
print(c) 

你還能認出這是你在學校數學課裏的哪個數學公式嗎~~~~,勾三股四弦五,只能幫你這麼多了。。。
這裏寫圖片描述

你在playground中實現了一個函數,允許你在代碼的最後使用開平方來計算代碼。你定義了兩個常數a和b,然後用勾股定理來計算c。控制檯輸出5.0.

Swift中沒有自帶計算數值平方的函數,所以我們自己來寫一個,更新playground。

import Foundation 
func square(number: Double) -> Double { 
    return number * number; 
} 

let a = 3.0, b = 4.0let c = sqrt(square(a) + square(b)) 
print(c) 

修改成這樣後,輸出的值依然是5.0。

上面的代碼定義了一個叫square的函數,傳入一個Double的參數並返回一個Double。

在Swift中,用關鍵詞func來定義一個函數,後面跟函數的名字。函數可以包含有多個不同類型的輸入參數但只能返回一種類型的數據。Swift的函數定義在全局範圍,與此相反的是方法通常被定義在一個聲明的類型範圍裏,比如一個類和結構。

除了語法的聲明有些不同外,Swift和絕大多數的現代語言如Object-C都是相似的。讓我們來試試一些新東西!

Functions are first-class - 函數是最棒的類

更新代碼如下

func square(number: Double) -> Double {
    return number * number 
}


let operation = square 
let a = 3.0, b = 4.0let c = sqrt(operation(a) + operation(b)) 
print(c) 

再次運行輸出結果依然是5

毫無疑問,你肯定已經注意到了代碼上某些有趣的事了,你定義了一個新的常數operation並將square作爲值賦值給他。後面的代碼用的也是operation而不是square了。

這是Swift語言的一個非常重要的特徵。函數也可以看做是一個類,這意味着你可以將他分配給變量或常量,作爲傳遞的參數或者是從其他函數返回的值。你可以使用採用了函數式編程的非常強大的編程技術,在第七章中還會深入的講解函數式編程。

你要記住Swift是一種強類型語言,每個變量和常數都有一個類型和差不多相似的函數。比如下面這行代碼沒有聲明常數的類型,所以編譯器必須要自己推斷類型。

let operation = square

但是square是作爲什麼類型被推斷的呢?更新代碼:

let operation:(Double) -> Double = square 

代碼正常運行。

square和operation都是(Double)->Double類型。這個函數類型說明了參數的類型和返回的類型(包括了參數的個數和類型)。這通常被稱爲函數的簽名。和任何的函數一樣,同樣的簽名都被認爲是具有相同的類型。

技巧:當函數被作爲另一個函數的參數時可能十分的難於理解,比如下面這種情況:
func doMath(operation:(Double)-> Double) -> Double { … }

如果你發現你已經被繞暈了的話可以用別名來試試。允許你給一個非常複雜的類型賦予一個別名:
typealias OperationType = (Double) -> Double
func doMath(operation:OperationType) -> Double { … }

Function syntax - 函數語法

正如你看到的那樣,Swift的函數語法非常的簡單。包含了函數的關鍵字func以及緊隨其後的函數名,括號裏的輸入參數,最後箭頭後面的返回類型。

這還有幾個簡單的例子:

你可以定義沒有參數的函數,比如,創建一個用於生產隨機數的函數:

func generateRandomNumber() -> Double { ... } 

函數中有多個參數時可用逗號分隔開:

func padString(string: String, length: Int) -> String { ... } 

函數沒有返回值或者返回的是void的,則稍有些許的有趣,在playground最後添加代碼:

func logDouble(number:Double) -> Void { 
    print(String(format: "%.2f", number)) 
} 
logDouble(c) 

這創建了一個叫logDouble的函數,傳入一個Double的參數並輸入一個控制了小數位數爲兩位的字符串。然後調用這個函數並傳入c。控制檯輸出:5.00

注意:這個代碼用了NSString中的初始化方法initWithFormat。用Object-C的方法來彌補Swift中對字符串類型的初始化。

你可能注意到Void並不像Object-C那樣是個關鍵詞,而是一種類型。如果你打開Swift的頭文件,你能發現Void是個()的別名:

typealias Void = () 

這和你使用沒有元素的元組方法是一樣的,你當然可以直接用(),雖然其實並沒什麼卵用。

技巧:你可以在xcode中按着common鍵鼠標點擊對應的類型查看其在Swift的定義。

爲了好玩,修改下函數,讓其更緊湊些:

func logDouble(number:Double) -> () { 
    print(String(format: "%.2f", number)) 
} 

再編譯看看,是不是結果仍然一樣。你甚至可以在方法中給將參數賦值給一個變量,雖然會有警告,且毛用沒有。

還有個更簡潔的定義方式。你可以完全去掉箭頭和返回值,更新代碼:

func logDouble(number:Double) { 
    print(String(format: "%.2f", number)) 
} 

蘋果設計的Swift是傾向於簡潔緊湊的,允許你放棄掉不必要的結構和語法~~

然而需要提一下的是,當你拋棄了箭頭和void時,你的logDouble類型依然是(Double)->()。

爲了驗證這一點,你可以更新你的代碼並明確的定義一個有類型的變量。

func logDouble(number:Double) { 
    print(String(format: "%.2f", number)) 
}


var logger: (Double) -> () = logDouble 
logger(c) 

代碼執行的情況和原來一模一樣,然而一旦你省略了類型的聲明中的返回值,如下:

var logger: (Double) = logDouble 

然後編譯器提示錯誤,需要你進行類型轉換。因爲(Double)是一個元組而不是一個函數。

現在你已經知道了函數的基本的內容,是時候去發現些更加有意思的東西了。現在是該瞭解泛型的時間了。

Overloading and generics - 重載和泛型

在一個新的playground上添加如下代碼:

func checkAreEqual(value: Int, expected: Int, message: String) {
    if expected != value {
    print(message)
    }
}

上面這個函數檢查一個給定的整數是否和另一個期望的整數相等。如果兩個值不相等,則函數輸出一條信息在控制檯。你可以用這個格式的方法用來驗證或者作爲前置判斷條件。

在函數下面加幾行代碼測試下:

var input = 3
checkAreEqual(input, expected: 2, message: "An input value of '2' was expected")
input = 47
checkAreEqual(input, expected: 47, message: "An input value of '47' was expected")

第一個輸出信息,第二個通過驗證,控制檯輸出:

An input value of ‘2’ was expected

當前的函數只有少量的限定詞所以只能用來檢查你的整數是否匹配你期望的整數。如果你想要檢查字符串string呢?或者是Double,或其他的類型呢?

好在,Swift允許你使用重載。重載即是用了相同的函數名字,但他們的參數類型,或者參數的個數是不同的。

在playground後面添加代碼:

func checkAreEqual(value: String, expected: String, message: String) {
    if expected != value {
        print(message)
    }
}


var inString = "cat"
checkAreEqual(inString, expected: "dog", message: "An input value of 'dog' was expected”)

控制檯輸出:

An input value of ‘dog’ was expected

通過重載checkAreEqual函數,你可以像判斷整數值那樣判斷字符串是否相同了。

這個並不是一個可以自由擴展的解決方案。每一個新的類型都需要一個單獨的函數,下面還有一種更好的方式。

刪除checkAreEqusl的函數方法,替換爲下面的內容:

func checkAreEqual<T: Equatable>(value: T, expected: T, message: String) { 
if expected != value {
    print(message) 
    } 
} 

正如你期待的那樣,輸出沒有變化。

新添加的checkAreEqual是通用的,前兩個指定參數用佔位符T來表示。當函數被調用時,T會自動推斷類型。另外,編譯器也在下面兩種情況下檢查約束。

1.傳遞到此函數時的第一個和第二個參數必須是相同類型,因爲兩個參數都用了相同的類型T。

2.必須採用Equatable協議類型。這個約束利用了!=運算符,是實現checkAreEqual的必須條件。

爲了驗證先上面的條件我們試試用混合類型調用此函數,試試刪除掉Equatable約束看看會怎麼樣,會有什麼錯誤提示?

想了解更多和泛型相關的類型,請看第四章。

提示:Swift的類型推斷非常強大。如果你將47和48.67分配給常量,且讓編譯器自己推斷類型,則會分別選擇Int和Double。所以很明顯編譯器會提示錯誤,第一個是Int,第二個是Double,無法執行。

In-out variables - 輸入輸出變量

所有到目前爲止你寫的大量的包含有輸入參數和返回值的函數,都不會影響到任何參數的狀態。但是如果你想要一個可以修改傳遞過來的參數的函數呢?這叫做函數的副作用,且行爲必須要顯示的聲明,下面來舉個例子!

打開一個新的playground,添加如下代碼:

import Foundation 
func square(number: Double) { 
    number = number * number 
} 
var a = 2.0 
square(a) 
print(a) 

上面的代碼定義了一個簡單的函數square。將傳遞來的值修改爲原來的平方。

明顯發現編譯器報錯:

Cannot assign to ‘let’ value number.

這個錯誤提示告訴了你有關函數的一個非常重要的信息,默認情況下他們的參數都是常數。換句話說,他們的行爲和用let關鍵字定義的常數是一樣的。

編譯器明確的指出這樣的操作是禁止的。

提示:大多數的其他主流語言(Object-C,jav,c#等)都允許你在函數類修改函數的參數。然而,這些改變也僅僅是改變函數參數值在本函數內的本地副本,而不是調用者傳入的實際變量。Swift是明確禁止修改函數參數的,因爲他有一個非常好的常數概念,讓你出現異常結果的可能性變得更小。

你可以修改函數參數的默認定義,先定義函數參數爲變量inout就可以讓編譯器通過了,調傳參數的時候前綴一個&,使用如下:

fun square(inout number: Double) {
    number = number * number
}
var a = 2.0
square(&a)
print(a)

輸入如預料的是4

在許多的其他語言,如Object-C中,在使用的參數前用一個&表明你傳遞的是值的引用,所以函數可以改值。

儘管palyground上的輸入輸出參數是Double類型,但事實是你可也以指定任何類型的參數作爲輸入輸出,包括類和結構。

技巧:用in-out可以修改值,但是儘量少用,很容易造成調用時的混亂

Classes and structures as function parameters - 類和結構做函數的參數

在前面的內容中,你已經看到了用Int,Double,String作爲函數參數時被拷貝一份用於使用,也看到了用inout關鍵字來修改傳入的參數。

Swift在處理類上面有些不同,看下面的例子:

新建一個playground,並添加如下代碼:

class Person {
    var age = 34,
    name = "Colin"
    func growOlder() {
        self.age+=1
    }
}

func celebrateBirthday(cumpleanero: Person) {
        print("Happy Birthday \(cumpleanero.name)")
    cumpleanero.growOlder()
}
let person = Person()
celebrateBirthday(person)
print(person.age)

在playground定義了一個叫Person的類,裏面包含了age,name兩個屬性,還有一個用於讓年齡疊加的函數。 celebrateBirthday()是一個普通的函數,可以打印一條慶祝信息,併疊加對象的年齡。

控制檯輸出:

Happy Birthday Colin
35

你可以看到調用celebrateBirthday成功的讓年齡增加了一歲。然而是怎麼實現的呢?

當你將一個對象的實例作爲一個參數傳遞給一個函數時,Swift傳遞的是這個類的引用。在你的playground上,person和cumpleanero都是指向同一個Person的實例。

將Person修改爲結構看看情況如何

struct Person {

    var age = 34, name = "Colin" 
        mutating func growOlder() { 
            self.age+=1 
    } 
} 

注意當你將聲明class改爲struct時,你還需要將growOlder()用mutating聲明。任何結構中新的函數要改變他的屬性狀態值都需要用到mutating這個關鍵字。

更新代碼如下

func celebrateBirthday(inout cumpleanero: Person) {

    print("Happy Birthday \(cumpleanero.name)")
    cumpleanero.growOlder()
}
var person = Person()
celebrateBirthday(&person)
print(person.age)

控制檯輸入:

Happy Birthday Colin
35

結構的使用和Int,Double之類的相同,當要修改結構中的值時必須要要用inout關鍵字

上面的代碼你做了一連串的改變。首先person現在是常數而不是變量。你可以改變一個被分配給常數的類的屬性,但不可以修改一個被分配給常數的一個結構,因爲結構是值類型,要想修改就要用inout進行傳遞。第二處,正如前面的,你用&聲明將結構對象作爲參數傳遞。

正如你看到的,函數在處理類和結構作爲參數時操作是完全不一樣的。

注意:回憶下能想起數組和字典都是值類型的結構。如果你想要修改他們的值都必須先將他們標記爲輸入輸出參數。

Variadics - 可變參數長度

在這一部分,你將瞭解到可變長度參數。你可以使用變量的參數個數給函數。

你用一個省略號表示一個參數類型的個數是可變的,如下所示:

func longestWord(words: String...) -> String?

此函數接收一個字符串列表並返回一個最長的字符串或者nil。是時候創建一個新的playground了,此時不練練更待何時!

func longestWord(words: String...) -> String? {
    var currentLongest: String?
    for word in words {
        if currentLongest != nil {
            if word.characters.count > currentLongest!.characters.count {
                currentLongest = word
            }
        } else {
            currentLongest = word
        }
    }
    return currentLongest
}

可變參數words變成在函數內的一個常數數組,允許你使用for-in 控制結構來遍歷他的內容。這個算法的實現相當簡單:函數遍歷每個word,比較目前最長的字符串。

在函數下添加調用代碼試試:

let long = longestWord("chick", "fish", "cat", "elephant") 
print(long) 

控制檯輸出

Optional(“elephant”)

輸出表明,long是一個包含有“elephant”的可選類型值,嘗試用不同的數量值的參數進行測試。

你看了第七章“Functional Programming”了嗎?如果看過你可能會知道如何讓這個函數更加簡潔:

func longestWord(words: String...) -> String? {
        return words.reduce(String?()) {
    (longest, word) in
    longest == nil || word.characters.count > longest!.characters.count
        ? word : longest
    }
}

不用擔心這個函數看不懂,後面的章節會詳細介紹。是不是簡單了不少!你可能不會經常使用到可變參數,但是當需要用時,這就是一個非常棒的解決方案。

External parameter names - 外部參數名

目前爲止你已經寫了很多兩個有着多個參數的函數了,比如:

checkAreEqual("cat", "dog", "Incorrect input") 

有時候很難確認每個參數在上下文對應的作用。在上面的代碼中,你沒那麼容易知道哪個是輸入值,哪個是期望值。

外部參數名可以解決這個問題,在新的playground上試試:

func checkAreEqual(value val: String, expected exp: String, message msg: String) { 
        if exp != val { 
            print(msg) 
    } 
}

這個函數的實現和前面是非常相似的。不過這一次中每個參數都有了兩個名字。比如第一個參數有value,val兩個名字。第一個名字是用來在外部調用時展示的外部參數名,這第二個名字是在函數內部使用的名字。

調用checkAreEqual時可以看到外部名稱:

checkAreEqual(value: "cat", expected: "dog", message: "Incorrect input") 

在定義checkAreEqual時提供了外部參數名,則你在調用這個函數的時候必須使用這個名字。你可以看到引入這個有多先進-他避免了每個參數在使用時造成的任何模糊不清的歧義。

當你使用了命名參數時,你的參數需要按照正確的函數順序填寫。儘管代碼很清晰明確,但如果不按照正確的參數名傳值,依然無法通過編譯。

checkAreEqual(expected: "dog", value: "cat", message: "Incorrect input") 

爲了保持代碼的簡潔性,推薦你儘量少使用外部參數名。只有需要用到外部參數名來解決歧義時來使用比較合適。舉一些例子看看:

下面是一個將字符串轉變爲日期的函數:

dateFromString("2014-03-14")

很明顯該函數只接受一個單一的參數,所以再增加一個外部參數無異於畫蛇添足。

與此相反,下面的函數獲取一個cell上的座標:

convertCellAt(42, 13) 

一個參數是行,一個參數是列,但是哪個是行哪個是列呢?這時很明顯就需要外部參數了。

convertCellAt(column: 42, row: 13) 

Methods - 方法

Swift是一種面向對象的語言,因此,你寫的應用的邏輯在方法中而不是在全局函數中。方法是與類型(如類,結構,枚舉)相關聯的一種特殊的函數。在本節中,你會發現一些在Swift中的一些特殊行爲是受Object-C語法的大影響的。

關於方法和函數有兩種理解方式:1.沒有區別 2。函數是全局的,方法是類或結構裏面的

Instance methods - 實例方法

實例方法將一個函數與一個特定類型的一個實例進行了關聯。你可以在類,結構和枚舉中定義一個和全局函數完全相同語法的方法。在一個新的playground中演練下:

class Cell: CustomStringConvertible { 
    private(set) var row = 0 
    private(set) var column = 0 
    func move(x: Int, y: Int) { 
        row += y 
        column += x } 
    func moveByX(x: Int) { 
        column += x 
    } 
    func moveByY(y: Int) { 
        column += y 
    } 
var description: String { 
    get { 
        return "Cell [row=\(row), col=\(column)]" } 
    } 
} 

該類定義了一個包含有個行,列屬性,一些可用於修改屬性的方法,一個描述語句。並遵守CustomStringConvertible協議。

添加下面的代碼執行這個類。

var cell = Cell() 
cell.moveByX(4) 
print(cell.description) 

這個是Cell的一個實例,用了moveByX修改座標並打印結果,控制檯輸出:

Cell [row=0, col=4]

這沒什麼奇怪的地方,繼續更新代碼:

var cell = Cell() 
cell.moveByX(4) 
cell.move(4, y: 7) 
print(cell.description)

注意,在上面的代碼中使用了move方法。你的第二個參數包含了名字。如果你刪除了y:參數的前綴,編譯器報錯。

Swift方法和函數共享一個相同的內部和外部參數名概念相同。然而,他們的默認行爲有點區別。一個函數除非你顯示的提供了外部名稱,否則所有的參數都無外部名稱。但在方法中,第一個參數無外部名字,後續的參數默認使用和內部一樣的外部名稱。

當然你也可以通過自己命名外部名稱自由的調整外部名稱,操作方式和前面的完全相同。此外,你可以添加一個下劃線表示刪除默認的外部名稱。

嘗試修改下代碼看看效果:

func move(x: Int, _ y: Int) { 
    row += y 
    column += x
} 

現在方法的調用可以不使用外部參數了cell.move(4, 7)

雖然你可以修改默認的參數命名方式,但是我還是建議你不要用。蘋果的api從Object-C開始就採用了這個標準,和moveBy,moveByY一樣方法名指定了第一個參數的名稱。

另外一個函數和方法屌炸天的功能是提供了給參數賦一個默認值的能力。更新move代碼,代用0作爲x和y的默認參數值

func move(x: Int = 0, y: Int = 0) { 
    row += y 
    column += x 
} 

相應的更新調用的代碼

var cell = Cell()
cell.move(4, y: 7)
cell.move(2)
cell.move()
cell.move(y: 3)
print(cell.description)

從上面可以看出有了默認值後,可以不填參數,當只填一個參數時默認的代表第一個對應的參數,其他的參數需要寫外部參數名。

Methods are first-class, too - 方法也是一個優秀的類

早在前面的章節中,你就發現在Swift中函數可以當類用。你可以在函數中分配變量或常量並將值傳遞給其他的函數。方法也是一個類。

實踐出真知,操練下:

var cell = Cell()
var instanceFunc = cell.moveByY 
instanceFunc(34) 
print(cell.description) 

上面的代碼生成了一個Cell的實例,然後分配moveByY實例方法給一個變量,然後通過這個變量的引用調用這個方法,控制檯輸出:

Cell [row=0, col=34]

你已經通過函數了解了實例方法這個概念,還有另一個可能比較有趣的功能,更新代碼:

var cell = Cell()
var moveFunc = Cell.moveByY
moveFunc(cell)(34)
print(cell.description)

這一次,你是通過類方法來獲取的moveByY方法而不是通過一個實例賦值。

當你第一次綁定一個對象的實例時返回對應實例下的函數,相當於在你早些時候的代碼中就初始化了函數。

如果你對“當前函數”完全不瞭解也沒事。等你看了第七章“函數式編程”再來理解吧!

Closures - 閉包

閉包,和函數方法一樣,都可以調用代碼塊,傳遞等。但是和函數方法不一樣的是,閉包是匿名的,而且有“捕捉”的能力將值存在在定義他們的範圍。在接下來的幾節中,你會瞭解到閉包有多強。

Closure expressions as anonymous functions - 閉包函數就像一個匿名函數

在Swift的api中大量的函數和方法都使用了這個閉包。舉個例子如下:
func sort(isOrderedBefore: (T, T) -> Bool) -> [T]
(Swift3.0已修改,自行在xcode中查看)

這種方法有一個參數:isOrderedBefore.這個參數本身就是一個函數,她接受兩個T型參數(有這個數組的類型定義)並返回這兩個值的相對順序的布爾值。使用排序方法返回一個有序數組。

讓我們看看這個方法,創建一個playground添加如下代碼:

let animals = ["fish", "cat", "chicken", "dog"] 
func isBefore(one: String, two: String) -> Bool { 
    return one > two 
} 
let sortedStrings = animals.sort(isBefore) 
print(sortedStrings) 

上面生成了一個常數數組animals和一個用來決定兩個字符串相對順序的函數isBefore。Swift定義了一個大於操作符用來比較字符串,使isBefore的實現很簡單。

函數定義後,通過引用isBefore,創建一個排序後的數組:

[fish, dog, chicken, cat] 

簡而言之,你的playground告訴排序數組應該基於一項值是否大於另一項。唯一真正需要知道的排序內容如下:

one > two

在這種情況下,聲明一個單獨的isBefore函數用於排序顯然有些多餘。在接下來的幾個步驟中,你可以去掉一些不必要的結構來創建一個更加簡潔的實現。接下來的實現是非常神奇的,當然你在playground中的代碼也是受益於這種語言的特性。讓我們來去掉無用的函數。

更新代碼

let animals = ["fish", "cat", "chicken", "dog"] 
let sortedStrings = animals.sort({ (one: String, two: String) -> Bool in 
    return one > two 
}) 
print(sortedStrings) 

和剛剛一樣,輸出一個排序後的數組。

你使用了一個閉包函數而不是通過一個函數的引用來進行處理。在這例子中,你可以考慮用一個匿名函數閉包來實現。閉包的語法邏輯和剛剛的isBefore是一樣的,只是沒有額外的函數聲明。isBefore的代碼直接放在了sorted中。

你當前的閉包函數的語法和普通的函數語法非常的像。一個參數列表,一個箭頭,一個返回類型,in關鍵字緊隨在閉包的實現後面。

然而,一切有意思的事情纔剛開始。你已經知道編譯器可以根據上下文推斷變量和常量的類型。所以其實在閉包中編譯器也可以執行類型的方式。

在接下來的幾個例子中,你會逐漸從閉包函數中刪除代碼,直到達到最簡形式。每一次的改變,你的控制檯輸出都不會改變。

首先,參數的類型可以先刪除了:

let sortedStrings = animals.sort({ (one, two) -> Bool inreturn one > two 
}) 

編譯器能從sort中獲取的數據推斷參數的類型。

這個表達式只有一個語句塊,作爲一個結果,return關鍵字有些多餘,所以可以刪除:

let sortedStrings = animals.sort({ 
    (one, two) -> Bool inone > two 
}) 

這個閉包函數的返回值也可以從sort需要的類型中推斷,所以可以刪除返回類型:

let sortedStrings = animals.sort({ 
    (one, two) in 
    one > two })

雖然參數旁邊的括號沒幾個字符,但畢竟是佔用了幾個字符,刪掉,刪掉,通通刪掉。

let sortedStrings = animals.sort({ 
    one, two in 
    one > two })

你已經幾乎刪除了50%的非空白字符,但這纔剛剛開始!

後面這一步有點拽,你可以刪除掉閉包函數中的參數聲明,完全通過輸入參數分配給本地常量:

let sortedStrings = animals.sort({ $0 > $1 }) 

正如你上面看到的,如果你不提供局部變量的參數名稱,Swift提供了編號進行對應。這是閉包函數中一個非常好用且簡單的參數集。

如果一個閉包作爲最後一個參數傳遞給函數或方法,你可以將閉包寫在括號外,使用所謂的後閉包。let sortedStrings = animals.sort() { $0 > $1 }

最後你再刪除掉空的圓括號,最簡結構如下:

let sortedStrings = animals.sort { $0 > $1 } 

通過我的計算,你現在的閉包表達式至少比你一開始緊湊了3倍!理論分析結構已經是最緊湊了,但還不是最最緊湊的代碼形式。sort需要的參數類型(String,String)-> Bool.大於操作符已經完全表明了這個關係,所以還可以直接說明:

let sortedStrings = animals.sort(>) 

還記得你在這一節開始時定義的isBefore函數嗎。你可以直接將閉包函數分配給一個變量:

var isBefore = {

    (one: String, two: String) -> Bool in  return one > two 
}


let sortedStrings = animals.sort(isBefore) 

因爲編譯器需要推斷isBefore表達式的類型,所以,你不能夠刪除參數的類型,如果刪除了,編譯器則無法推斷isBefore變量類型。編譯器會報錯:

var isBefore = {
(one, two) -> Bool in 
return one > two 
}

let sortedStrings = animals.sort(isBefore)

編譯器無法根據你使用isBefore變量的方式來判斷他的類型,直接將閉包傳遞給sorted:在sort的內部操作中編譯器可以做更多的類型推斷。

Capturing values - 捕捉值

閉包最強的地方之一是可以從他周圍的內容環境中捕獲常量和變量。閉包可以使用一些原來的環境已經被破壞了的值!通過一個例子來理解這個概念是最簡單的。

新的playground中添加代碼:

typealias StateMachineType = () -> Int 

typealias 定義了一個叫StateMachineType的函數類型。這個函數每次調用都返回一個Int。狀態用intergers來表示,在每個調用中狀態都在一個週期裏循環。如,一個狀態機有三個狀態週期:
0, 1, 2, 0, 1, 2, 0, 1, 2, …

接下來添加一個函數用來創建一個基於週期狀態數的狀態機:

func makeStateMachine(maxState: Int) -> StateMachineType { 

return { 
    currentState+=1if currentState > maxState { 
    currentState = 0 
} 
    return currentState 
    } 
} 

在詳細瞭解這個函數內容前,先檢查下是否能運行正常。添加一個測試如下:

let tristate = makeStateMachine(2) 
print(tristate()) 
print(tristate()) 
print(tristate()) 
print(tristate()) 
print(tristate()) 

控制檯輸出:

1
2
0
1
2

看上去很棒!接着在代碼下面繼續添加一個狀態機:

let bistate = makeStateMachine(1) 
print(bistate());
print(bistate()); 
print(bistate()); 
print(bistate()); 

不出所料的,輸出值在0和1之間循環

1
0
1
0

你已經確認了這個函數在創建一個狀態值時需要一個基數,現在來看看是怎麼具體實現的,看代碼

func makeStateMachine(maxState: Int) -> StateMachineType { 
    var currentState: Int = 0return { 
        currentState+=1if currentState > maxState { 
        currentState = 0 
    } 
    return currentState 
    } 
} 

makeStateMachine的第一行定義了一個本地變量currentState,用來保存在當前結構中的狀態值。第二行返回一個狀態機本身狀態的閉合表達式。這一行你省略了閉合表達式的結構()->Int,因爲編譯器能夠從這種封閉的函數中推斷出需要的類型。類型推斷是非常聰明的。完整寫法如下:
這裏寫圖片描述

再更仔細的看看閉包的表達式,他利用了封閉的makeStateMachine函數的局部變量currentState。這是比較一個有趣的地方!

currentState是makeStateMachine中的本地變量,你希望這個函數在返回時即註銷。

let tristate = makeStateMachine(2)
// currentState

變量在這個時候被註銷了嗎?!

print(tristate()) 

然而狀態機正常工作,可以從makeStateMachine返回的閉包表達式中瞭解到currentState一定是還存在且有效的。

你當前在playground上的代碼就是一個很好的用於示範值捕獲的練習。因爲閉合表達式利用了currentState變量,所以他可以在使用它的上下文週期中繼續使用currentState。

此外你無須擔心捕捉值的內存管理。Swift在捕獲值應當銷燬的時候自動處理。

很多現在的CocoaAPIs都是用的代理來處理異步執行的代碼。比如,location的位置改變通知CLLocationManager通過CLLocationManagerDelegate協議聲明處理。閉包的優秀表現,無疑會讓蘋果引入更多閉包的api,當你異步調用的時候調用一個閉包而不是一個委託對象。當這一切真的都發生的時候,你會發現捕獲值是多重要的一個功能代碼。

Memory leaks and capture lists - 內存泄漏和捕獲列表

我相信你已經發現了閉包的強大之處,你應該會在你的代碼中大量使用!不過在你這樣做之前,還有一個重要的課題需要學習:如何避免因爲使用閉包造成的內存泄漏。讓我們來看一個簡單例子:

Swift的playground不是一個用來了解內存管理的好地方。在playground的代碼是反覆運行的,所以,對象的釋放不能預測。在這最後一小節,你需要創建一個完整的app。

打開Xcode選擇File/New/Project… 並生成一個Single View Application命名爲MemoryLeakTest。在這個項目中快速生成一個Person.swift文件,添加代碼如下:

class Person {let name: String

    private var actionClosure: (() -> ())! 
        init(name: String) { 
            self.name = name 
            actionClosure = {
              println("I am \(self.name)") 
            }   
         } 
    func performAction() { 
        actionClosure() 
    } 
    deinit {

        print("\(name) is being deinitialized") 
    } 
} 

Person類非常簡單,有一個不變的常數屬性name,通過初始化設置。還有一個deinit在這個類被銷燬時打印一條消息。

這個類還有一個performaAction方法用來打印name。你已經通過actionClosure的函數屬性實現了。

是時候來用一下這個類了。

打開ViewController.swift,更新viewDidLoad代碼如下:

override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        let person = Person(name: "bob")
        person.performAction()
    }

簡單的創建了一個Person的實例,並分配了一個屬性值同時調用了performAction. 常數person是ViewDidLoad的本地值,所以,當這個方法退出時,person也應該被註銷。爲了能直接顯示結果,你還希望deinitializer被調用,表明Person的實例已經被註銷。

現在你可以測試下這個理論了。運行程序,監視控制檯的輸出:

I am bob

這證實了你的Person實例創建成功,且功能完全正常,但在deinit中打印語句呢?爲什麼這個方法沒有執行?

答案很簡單:在你的代碼裏有內存泄漏!如果你想仔細檢查內存問題,你可以配置下你的應用檢查內存的分配情況。這說明在ViewDidLoad退出後Person實例仍然在堆上存在。

閉包是引用類型,下面是對象和ViewDidLoad之間的關係圖:
這裏寫圖片描述

ViewController有個對本地常量Person實例的引用。Person有個加name常量屬性的引用,有個被分配爲私有的閉包函數actionClosure的引用。最後,這個閉包函數內部使用替換字符串的地方使用了self,造成了這個閉包函數對Person的引用。

當ViewDidLoad退出時,本地常量person被釋放,刪除ViewDidLoad和Person實例見的連接線。此時,你希望Person的實例的引用計算值應該下降爲0並自動被銷燬。不幸的是,這個期望並不會發生。因爲閉包函數的有對Person實例的引用,如下圖紅色線所示:
這裏寫圖片描述

這是一個典型的計數循環,在對象之間的循環引用造成的內存無法被釋放。

筆記:如果你是從C#,Java,JavaScript或者其他採用垃圾回收機制語言轉到Swift的,你可能會覺的有些奇怪。垃圾回收機制將引用追溯到已知的“根”,如果無法追溯到root根,則這個對象將從堆中釋放銷燬。用了這種技術,當檢測到遊離的循環引用時會在垃圾回收中移除。

Swift用的是自動引用計算。每次使用這個對象時都會增加一個對象引用的計數,當一個對象保留的計數爲0時,這個對象立即被銷燬。這解決了需要停止應用主線程來收集“垃圾”,但是你卻需要在使用代碼時避免出現循環引用。

這個問題並不是只在Swift中出現,在Object-C的代碼塊的block中也有這個問題。如果你熟悉Object-C的話,標準的解決方法是創建一個弱引用給self,然後創建一個強引用基於這個弱引用,在block的語句塊中使用這個強引用:

__weak typeof(self)weakSelf = self;
 [self.context performBlock:^{ 
    __strong typeof(weakSelf)strongSelf = weakSelf; 
// do something with strongSelf 
}]; 

呵呵!既醜又容易出錯!

還好,在解決這個同樣問題時Swift要簡單的多。打開Person.swift並在actionClosure的初始化代碼裏更新如下:

actionClosure = { 
    [unowned self] () -> () in 
    print("I am \(self.name)") 
} 

上述代碼定義了一個閉包的捕獲代碼,詳細的說明了這個閉包列表中常量和變量的所有權。在閉包使用前出現這個捕獲列表,並將其用放擴報包含變量列表。

在這種情況下,unowned self表明這個閉包函數沒有對自身的引用,不會增加引用計數。

編譯並運行程序,確認Person的實例現在有被成功釋放掉:

I am bob
bob is being deinitialized

很簡單不是嗎?

一般情況下你不太可能會寫一個Person類,讓他通過一個私有的閉包來實現方法,但你爲了出現這種循環引用所以使用了這樣的一個閉包。

循環引用一般在視圖控制器中經常出現。你經常需要異步的更新一個視圖控制器上的UI。如果你用閉包來實現更新,很有可能就會出現循環引用。如果你的視圖控制器一直不能釋放,說明出現了內存泄漏了!

Where to go from here? - 接着幹什麼?

在這一章中,你瞭解了下Swift中的函數和閉包,也學習了他的首要類first-class,語法和表達式。

最後你可能會發現,在大多數情況下,你的函數和閉包表達式是可以互換的,這是因爲函數和閉包是一樣的,函數是有名字的閉包而已。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章