05-Swift閉包(Closures)

* 閉包 是自包含的函數代碼塊,可以在代碼中被傳遞和使用。swift中的閉包和Objective-C中的代碼塊(block)以及其他一些編程語言中的匿名函數類型。
?
閉包可以捕獲和存儲其所在上下文中任意常量和變量的引用。這所謂的閉合并包裹這些常量和變量,即是 閉包 *。Swift中會自動處理在捕獲過程中涉及到的所有內存操作。

函數分全局和嵌套函數,而函數其實是特殊的閉包,閉包采取如下三種形式之一:
  • 全局函數是一個有名字,但不會捕獲任何值的閉包;
  • 嵌套函數是一個有名字,并可以捕獲其封閉函數區域內的值的閉包;
  • 閉包表達式是一個利用輕量級語法所寫的,可以捕獲其上下文中變量或常量值的匿名閉包;
    (即全局函數、嵌套函數也都是閉包!)
Swift的閉包表達式的擁有簡潔風格,并鼓勵在一下常見場景中進行語法優化:
  • 利用上下文推斷參數和返回值類型;
  • 隱式返回單表達式閉包,即單表達式閉包可以省略return關鍵字;
  • 參數名稱縮寫;
  • 尾隨閉包語法;




一、閉包表達式


* 嵌套函數 是一個在復雜函數中方便進行命名和定義自包含代碼模塊的方式。 閉包表達式 *是一種利用簡潔語法構建內聯閉包的方式。

  • sort方法。Swift標準庫中提供了名為sort的方法,會根據所提供的用于排序的閉包函數,將已知類型數組中的值進行排序。排序完成后,sort(_:)方法會返回一個與原數組大小相同,包含同類型且元素已是排序好的新數組,而原數組不會被sort(_:)方法修改。

sort方法的使用:sort(_:)方法接受一個閉包,該閉包需要傳入與數組元素類型相同的兩個值,并返回一個布爾類型的值,這是用于表明當排序結束后傳入的第一個參數是在第二個參數的前面還是后面。如果第一個參數值出現在第二參數值前面,排序閉包需要返回true,否則返回false

// 數組對應: 張三、李四、王五、趙六、田七
let names = ["zhansan","lisi","wangwu","zhaoliu","tianqi"];
// 排序方法
func mySort(str1:String, str2:String) -> Bool {
    // 即str1是在str2的前面,返回true
    return str1 > str2;
}
// 調用sort方法排序
var sortNames = names.sort(mySort);
print("排序前:\(names)");
print("排序后:\(sortNames)");
輸出結果:
排序前:["zhansan", "lisi", "wangwu", "zhaoliu", "tianqi"]
排序后:["zhaoliu", "zhansan", "wangwu", "tianqi", "lisi"]
  • 閉包表達式語法。在這種可以使用常量、變量和inout類型作為參數,不能提供默認值。也可以在參數列表的最后使用可變參數。另外,元組也可以作為參數和返回值:
// 上面mySort(_:_:)函數可以寫成閉包表達式
let names = ["zhansan","lisi","wangwu","zhaoliu","tianqi"];
let sortNames = names.sort({ (str1:String, str2:String) -> Bool in
    return str1 > str2;
});
// 代碼簡單,改寫為一行
// let sortNames = names.sort({ (str1:String, str2:String) -> Bool in return str1 > str2; });
print("排序前:\(names)");
print("排序后:\(sortNames)");
輸出結果:
排序前:["zhansan", "lisi", "wangwu", "zhaoliu", "tianqi"]
排序后:["zhaoliu", "zhansan", "wangwu", "tianqi", "lisi"]

閉包表達式語法一般形式
{ (parameters) -> returnType in
statements
}
// 注意1: 參數和返回值類型都是在大括號內,而不是大括號外;
// 注意2: 閉包中代碼部分由關鍵字in引入,即是in表示閉包的參數和返回值類型定義已經完成,閉包的代碼模塊即將開始;

  • 根據上下文推斷類型。在上面排序中使用到sort(_:)方法,而該方法其參數必須是(String, String) -> Bool類型,也就意味(String, String)Bool并不需要作為閉包表達式定義的一部分。因為類型都可以被正確推斷:
// 即省略參數類型和返回值類型
let sortNames = names.sort({ str1, str2 in return str1 > str2 });
  • 單表達式閉包隱式返回。即單行表達式閉包可以通過省略return關鍵字來隱式返回單行表達式的結果:
// 即省略return關鍵字
let sortNames = names.sort({ str1, str2 in str1 > str2 });
  • 參數名稱縮寫。Swift自動為內聯閉包提供了參數名稱縮寫功能,你可以用$0$1$2來順序調用閉包的參數,以此類推。另外,類型是可以通過上下文推斷,所以in關鍵字也同樣可以省略:
// 即參數縮寫
let sortNames = names.sort({ $0 > $1});
  • 運算符函數。swift的String類型定義了關于大于號>的字符串實現,其作為函數接收兩個String類型的參數并返回Bool類型的值,因此上面排序就還可以寫的更簡短(簡短的讓你抓狂,別懷疑,絕對是可以運行的,也是絕對沒問題的):
// 運算符函數的運用
let sortNames = names.sort(>);
// 注: >即從大到小,若是換為<即是從小到大


二、尾隨閉包


  • 若閉包表達式很長作為最后一個參數傳遞給參數,可以使用* 尾隨閉包 *來增強函數的可讀性:
// 參數縮寫
//let sortNames = names.sort({ $0 > $1});
// 尾隨閉包
//let sortNames = names.sort(){ $0 > $1 };
// 尾隨閉包也可以將()省略
let sortNames = names.sort { $0 > $1 };
  • 當閉包非常長以至于不能在一行中進行書寫時,尾隨閉包就非常有用:
// 將Int類型數組[16, 58, 510]轉為包含對應String類型值的數組["OneSix", "FiveEight", "FiveOneZero"]
// 數值位和英文版名字相映射的字典
let digitNames = [
    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
// 需要轉換的數組
let  numbers = [16, 58, 510];
// 省略括號、閉包尾隨
let str = numbers.map {
    //  參數為number,返回值類型String
    (var number) -> String in
    // 具體代碼區域
    var output = "";
    while number > 0 {  // 取出對應位數的數值
        // 從個位開始,并找到對應英文,最后拼接
        output = digitNames[number % 10]! + output;
        // 從個位開始截去
        number = number / 10;
    }
    return output;
}
print(numbers);
print(str);
輸出結果:
[16, 58, 510]
["OneSix", "FiveEight", "FiveOneZero"]

Array類型中map(_:)方法: 獲取一個閉包表達式作為其唯一參數。該閉包會為數組中的每一個元素調用一次,并返回該元素所映射的值,而具體的映射方式和返回值類型由閉包來指定。


三、捕獲值


閉包可以在其被定義的上下文中* 捕獲 常量或變量。即定義這些常量和變量的原作用域已經不存在,閉包仍然可以在閉包的代碼區域中引用和修改這些值。
?swift中,可以捕獲值的閉包其最簡單形式就是
嵌套函數 ,即是定義在其他函數的函數體內的函數, 嵌套函數 *可以捕獲其外部函數所有的參數以及定義的常量和變量。

/** 實現計步功能*/
// 返回類型: (Int) -> Int
func makeRunningTotal() -> (Int) -> Int {
    var runningTotal = 0
    
    // 嵌套函數,特殊閉包,參數amount是一個增量
    func addFunc(amount:Int) -> Int {
        // 捕獲runningTotal
        runningTotal += amount
        return runningTotal
    }
    
    // 注意: 不要寫成addFunc(),這是函數調用,而不是返回函數
    // 即makeRunningTotal將addFunc作為閉包返回
    return addFunc;
}
// 李明
let liming = makeRunningTotal();
print("李明-總步數: \(liming(10))");
print("李明-總步數: \(liming(20))");
print("李明-總步數: \(liming(30))");
// 張三
let zhagnsan = makeRunningTotal();
print("張三-總步數: \(zhagnsan(50))");
print("張三-總步數: \(zhagnsan(100))");
print("張三-總步數: \(zhagnsan(200))");
輸出結果:
李明-總步數: 10
李明-總步數: 30
李明-總步數: 60
張三-總步數: 50
張三-總步數: 150
張三-總步數: 350

單獨看嵌套函數,會發現函數體中使用的變量runningTotal不是在函數體內部定義的;addFunc()函數就是外圍函數中捕獲了runningTotal的引用;捕獲引用保證runningTotal在調用完addFunc()后不會消失,并保證在下一次執行addFunc()函數的時候,runningTotal依舊存在;
func addFunc(amount:Int) -> Int {
runningTotal += amount;
return runningTotal;
}


四、閉包是引用類型


閉包是* 引用類型 *,在上述例子中,limingRunningTotalzhagnsanRunningTotal是常量,但這些常量是指向閉包,仍然還是可以增加其捕獲的變量值。
?無論是將閉包賦值給一個常量還是變量,實際都是將常量或變量的值設置為對用閉包的引用。


五、非逃逸閉包


當一個閉包作為參數傳入到一個函數中,但這個閉包在函數返回之后才被執行,該閉包稱為從函數中* 逃逸 *。
?當你在定義函數,其參數是閉包,在參數之前使用@noescape標注,即表明該閉包不允許"逃逸"出這個函數。而這個@noescape閉包標注,就是是告訴編譯器這個閉包的生命周期是在它所在函數體中的。
?而在默認情況,即沒有加上@noescape標注的,閉包是可以"逃逸"出函數的。

  • 非逃逸閉包:
/** 下載數據的函數 - 非逃逸閉包
 參數接收一個閉包
 @noescape即表明該閉包是屬于"非逃逸閉包"
 */
func downloadData1 (@noescape closure:(Void) -> Void) {
    // 閉包closure的調用,就是該閉包closure只有在downloadData1函數中執行調用
    print("即將開始下載...");
    closure();
    print("已經開始下載...");
}
// 調用下載函數
downloadData1() {
    print("正在下載QQ...")
    sleep(2);   // 延時操作,延時2秒,用于演示正在下載QQ
};
/** 效果
先打印"即將開始下載...",
再打印"正在下載QQ...",
2秒過后,
最后才打印"已經開始下載..."
即表示閉包是在downloadData1函數中執行的;
*/
輸出結果:
即將開始下載...
正在下載QQ...
已經開始下載...
  • 逃逸閉包:
// 具體操作處理者
// 處理者這里是一個數組,即要處理的事件數組
var downLoadHandler:[(Void)->(Void)] = [];
/** 下載數據的函數 - 逃逸閉包
 參數接收一個閉包
 沒有@noescape,即表明該閉包是屬于"逃逸閉包"
 */
func downloadData2 (closure:(Void) -> Void) {
    // 將具體操作添加到處理者,而不需要管什么時候處理
    // 只添加,但閉包是沒有執行的!!!
    // 閉包closure不是在downloadData2中執行,即是"逃逸閉包"
    print("即將添加到處理者");
    downLoadHandler.append(closure);  
    print("已經添加到處理者");
}
// 添加下載QQ操作
downloadData2() {
    print("正在下載QQ...");
    sleep(2);   // 延時操作,用于演示正在下載QQ
};
// 添加下載xcode操作
downloadData2 {
    print("正在下載xcode...");
    sleep(2); // 延時操作,用于演示正在下載xcode
}
// 處理者downLoadHandler,處理事件
for (index,closure) in downLoadHandler.enumerate() {
    // 具體處理事件
    print("正在處理事件:\(index)");
    closure();
    print("處理完成事件:\(index)");
}
輸出結果:
即將添加到處理者  
已經添加到處理者  // 即是添加"下載QQ"閉包
即將添加到處理者
已經添加到處理者  // 即是添加"下載xcode"閉包
正在處理事件:0    // 即是"下載QQ"閉包的調用
正在下載QQ...
處理完成事件:0
正在處理事件:1    // 即是"下載xcode"閉包的調用
正在下載xcode...
處理完成事件:1

注: 關于* 逃逸閉包 ,更多都會用于異步操作相關的封裝。即在異步操作開始后就立即返回,而 逃逸閉包 *是在異步操作結束之后才會被調用。


六、自動閉包


* 自動閉包 *是一種自動創建的閉包,用于包裝傳遞給函數作為參數的表達式,而這種閉包不接受任何參數,當它被調用后,會返回包裝在其中表達式的字。

  • 自動閉包語法可以將一段具體操作封裝成一個變量或常量,方便使用操作:
// 將問候操作以閉包形式賦值給一個常量
let speakClosures = {
    print("hello world!");
    print("hello swift!");
}
// 當在需要問候的時候,只需要調用speakClosures即可
speakClosures();
// 作為參數傳遞
func testFunc(closures:()->()) {
    // 調用
    closures();
}
testFunc(speakClosures);
輸出結果:
hello world!
hello swift!
hello world!
hello swift!


注:xcode7.3環境

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 86.復合 Cases 共享相同代碼塊的多個switch 分支 分支可以合并, 寫在分支后用逗號分開。如果任何模式...
    無灃閱讀 1,433評論 1 5
  • 閉包是自包含的函數代碼塊,可以在代碼中被傳遞和使用。Swift 中的閉包與 C 和 Objective-C 中的代...
    窮人家的孩紙閱讀 1,748評論 1 5
  • Swift 中的閉包是自包含的函數代碼塊,可以在代碼中被傳遞和使用。類似于OC中的Block以及其他函數的匿名函數...
    喬克_叔叔閱讀 529評論 1 3
  • 閉包是功能性自包含模塊,可以在代碼中被傳遞和使用。Swift中的閉包與 C 和 Objective-C中的 blo...
    AirZilong閱讀 353評論 0 2
  • 綠玉呵紅艷, 名喚美人蕉。 斑斕勝百花, 凄清比蘭草。 細雨只增媚, 烈日總不凋。 微風裊裊過, 嫣然對我笑。 注...
    詩夢劍客閱讀 257評論 0 0