7.闭包
闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的匿名函数比较相似。
闭包采取如下三种形式之一:
- 全局函数是一个有名字但不会捕获任何值的闭包;
- 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包;
- 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包;
闭包一般形式语法:
{ (parameters) -> returnType in statements }
OC中的闭包语法:^ 返回值类型 参数列表 表达式。示例如下:
int (^myBlock)(int) = ^int (int num){ return 3; };
1.闭包表达式
// TODO: 从排序说起 let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]; //定义一个数组 func backwards(s1: String, s2: String) -> Bool { return s1 > s2 } //定义排序规则函数 var reversed = names.sort(backwards) //将排序规则函数作为参数传给sort // TODO: 闭包语法实现 //【注意】: //1:在内联闭包表达式中,函数和返回值类型都写在大括号内,而不是大括号外 //2:闭包的函数体部分由关键字【in】引入。该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。 var reversed2 = names.sort({(s1: String, s2: String) -> Bool in return s1 > s2; }); // TODO: 进化1(根据上下文推断类型):因为排序闭包函数是作为sort(_:)方法的参数传入的,Swift 可以推断其参数和返回值的类型必须是(String, String) -> Bool类型的函数。这意味着(String, String)和Bool类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(->)和围绕在参数周围的括号也可以被省略: //【注意】: //1:实际在任何情况下,通过内联闭包表达式构造的闭包作为参数传递给函数或方法时,都可以推断出闭包的参数和返回值类型。 这意味着闭包作为函数或者方法的参数时,几乎不需要利用完整格式构造内联闭包。 //2:但是,如果完整格式的闭包能够提高代码的可读性,则可以采用完整格式的闭包。 var reversed3 = names.sort({s1, s2 in return s1 > s2}); // TODO: 进化2(单行表达式闭包隐式返回):单行表达式闭包可以通过省略return关键字来隐式返回单行表达式的结果 var reversed4 = names.sort({s1, s2 in s1 > s2}); // TODO: 进化3(参数名称缩写):Swift 自动为内联闭包提供了参数名称缩写功能,可以直接通过$0,$1,$2来顺序调用闭包的参数,以此类推。如果在闭包表达式中使用参数名称缩写,那么也可以在闭包参数列表中省略对其的定义,并且对应参数名称缩写的类型会通过函数类型进行推断。【in关键字也同样可以被省略】,因为此时闭包表达式完全由闭包函数体构成: var reversed5 = names.sort({$0 > $1}); // TODO: 进化4(运算符函数):Swift 的String类型定义了关于大于号(>)的字符串实现,其作为一个函数接受两个String类型的参数并返回Bool类型的值。而这正好与sort(_:)方法的参数需要的函数类型相符合。因此,可以简单地传递一个大于号,Swift 可以自动推断出想使用大于号的字符串函数实现: var resersed6 = names.sort(>);
2.尾随闭包
- 如果需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。
func someFunctionThatTakesAClosure(closure: () -> Void) { // 函数体部分 } // 以下是不使用尾随闭包进行函数调用 someFunctionThatTakesAClosure({ // 闭包主体部分 }) // 以下是使用尾随闭包进行函数调用 someFunctionThatTakesAClosure() { // 闭包主体部分 }
类比OC中的Block:
- (void)blockTest:(void(^)(NSString *str))block { block(@"Hello"); } [self blockTest:^(NSString *str) { NSLog(@"%@", str); }];
学习尾随闭包的基本知识后,我们继续对上节的排序进行优化:
// TODO: 进化5(尾随闭包): var reversed7 = names.sort(){$0 > $1}; // TODO: 进化6(尾随闭包):如果函数只需要闭包表达式一个参数,当使用尾随闭包时,甚至可以把()省略掉: var reversed8 = names.sort{$0 > $1}; // TODO: 再看一个示例:将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 string = numbers.map(){(number : Int) -> String in var output = ""; var temp = number; while temp > 0 { output = digitNames[temp % 10]! + output; temp /= 10; } return output; } //【说明】 //1.map(_:)为数组中每一个元素调用了闭包表达式; //2.闭包表达式在每次被调用的时候创建了一个叫做output的字符串并返回。 //3.通过尾随闭包语法,优雅地在函数后封装了闭包的具体功能,而不再需要将整个闭包包裹在map(_:)方法的括号内。
3.捕获值
- 闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
// TODO: 先看一个示例:incrementer()函数并没有任何参数,但是在函数体内访问了runningTotal和amount变量。这是因为它从外围函数捕获了runningTotal和amount变量的引用。捕获引用保证了runningTotal和amount变量在调用完makeIncrementer后不会消失,并且保证了在下一次执行incrementer函数时,runningTotal依旧存在。 func makeIncrementor(forIncrement amount: Int) -> () -> Int { var runningTotal = 0; func incrementor() -> Int { runningTotal += amount; return runningTotal; } return incrementor; } let incrementByTen = makeIncrementor(forIncrement: 10); incrementByTen(); //10 incrementByTen(); //20 incrementByTen(); //30 let incrementBySeven = makeIncrementor(forIncrement: 7); incrementBySeven(); //7 incrementByTen(); //40 incrementBySeven(); //14
4.闭包是引用类型
- 在上面的例子中,incrementBySeven和incrementByTen是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量的值。这是因为函数和闭包都是引用类型。
- 无论将函数或闭包赋值给一个常量还是变量,实际上都是将常量或变量的值设置为对应函数或闭包的引用。上面的例子中,指向闭包的引用incrementByTen是一个常量,而并非闭包内容本身。
5.非逃逸闭包
- 当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。当定义接受闭包作为参数的函数时,你可以在参数名之前标注@noescape,用来指明这个闭包是不允许“逃逸”出这个函数的。将闭包标注@noescape能使编译器知道这个闭包的生命周期,从而进行编译优化。
- 标记了 @noescape的闭包可以免去写引用self,普通的闭包使用self时都需要。因为它相当于一个同步的调用,不会产生循环引用。例如SnapKit的函数定义:
// TODO: 示例1: class ClosureA { var iTemp = 0; func methodA(@noescape closureTemp: (Void->Void)) { closureTemp(); } func methodB() { methodA {() -> Void in iTemp = 1; //一般的closure都是要self.iTemp = 1,@noescape则不需要 }; } } //【说明】:引用到外部变量的闭包是不能加@noescape标记的。 class ClosureB { var varibleA: (Void -> Void)!; func methodA(closureTemp: (Void->Void)) { self.varibleA = closureTemp; //引用到属性varibleA } } //上面的闭包closureTemp是不能加@noescape标记的。
6.自动闭包
- 自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够用一个普通的表达式来代替显式的闭包,从而省略闭包的花括号。
// TODO: 代码1: var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]; func serveCustomer(customerProvider: () -> String) { print("Now serving \(customerProvider())!"); } serveCustomer({customersInLine.removeAtIndex(0)}); //serveCustomer({(Void) -> String in // return customersInLine.removeAtIndex(0); //}); // TODO: 代码2 var customersInLine2 = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]; func serveCustomer2(@autoclosure customerProvider: () -> String) { print("Now serving \(customerProvider())!"); } serveCustomer2(customersInLine2.removeAtIndex(0)); //【说明】: //1.上面两段代码实现了同一个功能; //2.customerProvider参数自动转化为一个闭包,因为该参数被标记了@autoclosure特性。 //3.过度使用autoclosure会让代码变得难以理解,因此不太推荐应用。
无善无恶心之体,
有善有恶意之动,
知善知恶是良知,
为善去恶是格物。