闭包

闭包可以捕获和存储其所在上下文中任意常量和变量的引用,被称为包裹常量和变量。
Swift 会为你管理在捕获过程中涉及到的所有内存操作。

全局和嵌套函数实际上也是特殊的闭包,闭包采用如下三种形式之一:

  • 全局函数是一个有名字但不会捕获任何值的闭包。
  • 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包。
  • 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包。

Swift 的闭包表达式拥有简洁的风格,并在常见场景中进行语法优化,主要优化如下:

  • 利用上下文推断参数和返回值类型
  • 隐式返回单表达式闭包,即单表达式闭包可以省略 return 关键字
  • 参数名称缩写
  • 尾随闭包语法

1. 闭包表达式

1.1 排序方法

  • Swift 标准库提供了名为 sorted(by:) 的方法,它会基于你提供的排序闭包表达式的判断结果对数组中的值(类型确定)进行排序。
  • 一旦它完成排序过程,sorted(by:) 方法会返回一个与旧数组类型大小相同类型的新数组,该数组的元素有着正确的排序顺序。
  • 原数组不会被 sorted(by:) 方法修改。
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
func backward(_ s1: String, _ s2: String) -> Bool {return s1 > s2
}
var reversedNames = names.sorted(by: backward)  // reversedNames 为 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
注意1. 如果第一个字符串(s1)大于第二个字符串(s2),backward(_:_:) 函数会返回 true,表示在新的数组中 s1 应该出现在 s2 前。2. 对于字符串中的字符来说,“大于”表示“按照字母顺序较晚出现”。这意味着字母 "B" 大于字母 "A" ,字符串 "Tom" 大于字符串 "Tim"。3. 该闭包将进行字母逆序排序,"Barry" 将会排在 "Alex" 之前

1.2 闭包表达式语法

一般形式:

{ (parameters) -> return type instatements
}
  • 闭包表达式参数 可以是 in-out 参数,但不能设定默认值。
  • 如果你命名了可变参数,也可以使用此可变参数。
  • 元组也可以作为参数和返回值。
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool inreturn s1 > s2
})
  • 闭包的函数体部分由关键字 in 引入。
  • 该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。

由于这个闭包的函数体部分如此短,以至于可以将其改写成一行代码:

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
该例中 sorted(by:) 方法的整体调用保持不变,一对圆括号仍然包裹住了方法的整个参数。然而,参数现在变成了内联闭包。

1.3 根据上下文推断类型

  • 因为排序闭包函数是作为 sorted(by:) 方法的参数传入的,Swift 可以推断其参数和返回值的类型。
  • sorted(by:) 方法被一个字符串数组调用,因此其参数必须是 (String, String) - > Bool 类型的函数。
  • 意味着 (String, String) 和 Bool 类型并不需要作为闭包表达式定义的一部分,因为所有的类型都可以被正确推断,返回箭头(->)和围绕在参数周围的括号也可以被省略:
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

1.4 单表达式闭包的隐式返回

单行表达式闭包可以通过省略 return 关键字来隐式返回单行表达式的结果:

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
注意:1. sorted(by:) 方法的参数类型明确了闭包必须返回一个 Bool 类型值。2. 因为闭包函数体只包含了一个单一表达式(s1 > s2),该表达式返回 Bool 类型值。3. 因此这里没有歧义,return 关键字可以省略。

1.5 参数名称缩写

Swift 自动为内联闭包提供了参数名称缩写功能。

  • 在闭包表达式中使用参数名称缩写,你可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。
  • in 关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:
reversedNames = names.sorted(by: { $0 > $1 } )
注意:$0 和 $1 表示闭包中第一个和第二个 String 类型的参数。

1.6 运算符方法

  • Swift 的 String 类型定义了关于大于号(>)的字符串实现,其作为一个函数接受两个 String 类型的参数并返回 Bool 类型的值。
  • 这正好与 sorted(by:) 方法的参数需要的函数类型相符合。
  • 因此,你可以简单地传递一个大于号,Swift 可以自动推断找到系统自带的那个字符串函数的实现:
reversedNames = names.sorted(by: >)

2. 尾随闭包

  • 尾随闭包是一个书写在函数圆括号之后的闭包表达式。
  • 函数支持将其作为最后一个参数调用。
  • 在使用尾随闭包时,你不用写出它的参数标签。
func someFunctionThatTakesAClosure(closure: () -> Void) {// 函数体部分
}// 以下是不使用尾随闭包进行函数调用
someFunctionThatTakesAClosure(closure: {// 闭包主体部分
})// 以下是使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {// 闭包主体部分
}
  • 字符串排序闭包可以作为尾随包的形式改写在 sorted(by:) 方法圆括号的外面:
reversedNames = names.sorted() { $0 > $1 }
  • 如果闭包表达式是函数或方法的唯一参数,则当你使用尾随闭包时,你甚至可以把 () 省略掉:
reversedNames = names.sorted { $0 > $1 }
  • Swift 的 Array 类型有一个 map(_:) 方法,这个方法获取一个闭包表达式作为其唯一参数。
  • 该闭包函数会为数组中的每一个元素调用一次,并返回该元素所映射的值。
  • 具体的映射方式和返回值类型由闭包来指定。
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 strings = numbers.map {(number) -> String invar number = numbervar output = ""repeat {output = digitNames[number % 10]! + outputnumber /= 10} while number > 0return output
}
// strings 常量被推断为字符串类型数组,即 [String]
// 其值为 ["OneSix", "FiveEight", "FiveOneZero"]

map(_ : ) 为数组中每一个元素调用了一次闭包表达式。
不需要指定闭包的输入参数 number 的类型,因为可以通过要映射的数组类型进行推断。
在该例中,局部变量 number 的值由闭包中的 number 参数获得,因此可以在闭包函数体内对其进行修改,(闭包或者函数的参数总是常量),闭包表达式指定了返回类型为 String,以表明存储映射值的新数组类型为 String。

注意1. 字典 digitNames 下标后跟着一个叹号(!),因为字典下标返回一个可选值(optional value),表明该键不存在时会查找失败。2. 在上例中,由于可以确定 number % 10 总是 digitNames 字典的有效下标,因此叹号可以用于强制解包(force-unwrap)存储在下标的可选类型的返回值中的 String 类型的值。

3. 值捕获

  • 闭包可以在其被定义的上下文中捕获常量或变量。
  • 即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
func makeIncrementer(forIncrement amount: Int) -> () -> Int {var runningTotal = 0func incrementer() -> Int {runningTotal += amountreturn runningTotal}return incrementer
}
  1. makeIncrementer 返回类型为 () -> Int。这意味着其返回的是一个函数,而非一个简单类型的值。
  2. 该函数在每次调用时不接受参数,只返回一个 Int 类型的值。
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()    // 返回的值为10
incrementByTen()    // 返回的值为20
incrementByTen()    // 返回的值为30let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()  // 返回的值为7

定义了一个叫做 incrementByTen 的常量
该常量指向一个每次调用会将其 runningTotal 变量增加 10 的 incrementer 函数

注意:1. 如果你将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员而捕获了该实例,你将在闭包和该实例间创建一个循环强引用。2. Swift 使用捕获列表来打破这种循环强引用。

4. 闭包是引用类型

  • incrementBySeven 和 incrementByTen 都是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量的值。这是因为函数和闭包都是引用类型。
  • 无论你将函数或闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的引用。
  • 上面的例子中,指向闭包的引用 incrementByTen 是一个常量,而并非闭包内容本身。
  • 这也意味着如果你将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包:
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()    // 返回的值为50

5. 逃逸闭包

  • 当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。
  • 当你定义接受闭包作为参数的函数时,你可以在参数名之前标注 @escaping,用来指明这个闭包是允许“逃逸”出这个函数的。
  • 一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {completionHandlers.append(completionHandler)
}
  1. someFunctionWithEscapingClosure(_:) 函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。
  2. 如果你不将这个参数标记为 @escaping,就会得到一个编译错误。
func someFunctionWithNonescapingClosure(closure: () -> Void) {closure()
}class SomeClass {var x = 10func doSomething() {someFunctionWithEscapingClosure { self.x = 100 }someFunctionWithNonescapingClosure { x = 200 }}
}let instance = SomeClass()
instance.doSomething()
print(instance.x)   // 打印出“200”completionHandlers.first?()
print(instance.x)   // 打印出“100”
  1. 将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 self。
  2. 传递到 someFunctionWithEscapingClosure(_:) 中的闭包是一个逃逸闭包,这意味着它需要显式地引用 self。
  3. 相对的,传递到 someFunctionWithNonescapingClosure(_:) 中的闭包是一个非逃逸闭包,这意味着它可以隐式引用 self。

6. 自动闭包

  • 自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。
  • 这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。
  • 这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。
  • 自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。
  • 延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)    // 打印出“5”let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)    // 打印出“5”print("Now serving \(customerProvider())!") // Prints "Now serving Chris!"
print(customersInLine.count)    // 打印出“4”

在闭包的代码中,customersInLine 的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。
如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。

注意:1. 注意,customerProvider 的类型不是 String,而是 () -> String。2. 一个没有参数且返回值为 String 的函数。
  • 将闭包作为参数传递给函数时,你能获得同样的延时求值行为。
//  serve(customer:) 函数接受一个返回顾客名字的显式的闭包。
func serve(customer customerProvider: () -> String) {print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// 打印出“Now serving Alex!”
  • 通过将参数标记为 @autoclosure 来接收一个自动闭包。
  • 可以将该函数当作接受 String 类型参数(而非闭包)的函数来调用。
  • customerProvider 参数将自动转化为一个闭包,因为该参数被标记了 @autoclosure 特性。
func serve(customer customerProvider: @autoclosure () -> String) {print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
注意:1. 过度使用 autoclosures 会让你的代码变得难以理解。2. 上下文和函数名应该能够清晰地表明求值是被延迟执行的。
  • 如果你想让一个自动闭包可以“逃逸”,则应该同时使用 @autoclosure 和 @escaping 属性。
// customersInLine i= ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))print("Collected \(customerProviders.count) closures.") // 打印“Collected 2 closures.”
for customerProvider in customerProviders {print("Now serving \(customerProvider())!")
}
// 打印“Now serving Barry!”
// 打印“Now serving Daniella!”
  1. collectCustomerProviders(_ : ) 函数并没有调用传入的 customerProvider 闭包,而是将闭包追加到了 customerProviders 数组中。
  2. 这个数组定义在函数作用域范围外,这意味着数组内的闭包能够在函数返回之后被调用。
  3. 因此,customerProvider 参数必须允许“逃逸”出函数作用域。

Swift-闭包,隐式返回、尾随闭包、值捕获、逃逸闭包、自动闭包相关推荐

  1. boost::safe_numerics模块实现隐式转换更改数据值的测试程序

    boost::safe_numerics模块实现隐式转换更改数据值的测试程序 实现功能 C++实现代码 实现功能 boost::safe_numerics模块实现隐式转换更改数据值的测试程序 C++实 ...

  2. swift可选隐式可选类型

    可选类型.隐式可选类型 在swift中,可选鲁迅个其实是一个,枚举类型,里面有None和Some两种类型.其实所谓的nil就是Optional.None,非nil就是Optional.Some,然后会 ...

  3. 33 个 JavaScript 核心概念系列(三): 显式 (名义) 与 隐式 (鸭子)类型转换

    原文地址:落明的博客 一. 前言 说实话,JavaScript 的类型转换是个相当头疼的问题,无论是对于初学者还是有经验的老司机.它的难处并不在于概念多难理解,而是情况多且杂,看似相同的情况结果却又出 ...

  4. Scala _09样例类(case classes)隐式转换

    样例类(case classes) 概念理解 使用了case关键字的类定义就是样例类(case classes),样例类是种特殊的类.实现了类构造参数的getter方法(构造参数默认被声明为val), ...

  5. html中隐式转换成数字,详解JS中的隐式类型转换

    JS中隐式类型转换 JS中的数据类型 JS中的数据类型分为两大类: 1.基本数据类型: 1.String 字符串 表示一段文本,例如:人的姓名.地址等 2.Number 数值 3.Boolean 布尔 ...

  6. ORACLE隐式类型转换

      隐式类型转换简介   通常ORACLE数据库存在显式类型转换(Explicit Datatype Conversion)和隐式类型转换(Implicit Datatype Conversion)两 ...

  7. C++类型转换:隐式转换和显式转换

    目录 隐式转换 为什么要进行隐式转换 C++隐式转换的原则 C++隐式转换发生条件 隐式转换的风险 禁止隐式转换 显式转换 dynamic_cast static_cast const_cast re ...

  8. Scala的隐式转换详解

    隐式转换是在Scala编译器进行类型匹配时,如果找不到合适的类型,那么隐式转换会让编译器在作用范围内自动推导出来合适的类型. 1.隐式值与隐式参数 隐式值是指在定义参数时前面加上implicit.隐式 ...

  9. 演练 构造函数的显示调用与隐式调用 c# 1613715344

    演练 构造函数的显示调用与隐式调用 c# 1613715344 父类 子类 入口 效果 自动调用了父类的带参构造方法 调用父类带参构造方法 调用了父类的带参构造 无参构造则不会被调用 代码改造分析原因 ...

最新文章

  1. 编写和调试Shader程序(1)
  2. linux文件系统逻辑,Linux系统——文件系统与LVM 逻辑
  3. 第十二周项目一-实现复数类中的运算符重载(2)
  4. 从 github 执行 git clone 一个大的项目时提示 error: RPC failed
  5. ASP.NET 用户控件自定义属性、方法、事件
  6. 转:VMware、微软等四种主要的网络IO虚拟化模型
  7. 如何使用Alert 组件
  8. Unity Using Interception and Policy Injection
  9. Docker镜像保存save、加载load
  10. SSH中 三大框架的各自的作用及好处
  11. 百胜中国于香港联交所主要上市;平安健康正式收购平安智慧医疗 | 美通企业日报...
  12. 华为网络技术大赛模拟题目
  13. 帆软报表更新到服务器控件展示不出来_帆软报表问题汇总
  14. java 支付宝 验证签名失败,支付宝支付错误返回ILLEGAL_SIGN,签名验证错误
  15. spy——[1220] SPY
  16. 经典网页设计:20个优秀的电子商务网站设计案例
  17. 入门级node+uni-app开发即时通讯聊天室(3)用户的搜索以及好友添加(二)
  18. js实现开平方 Math.sqrt
  19. B站粉丝计数器!基于microByte
  20. STM32毕业设计题目大全 选题推荐

热门文章

  1. Vsccode黄色波浪线
  2. 请用python帮写我写个抢票软件
  3. 各种排序算法比较(java)
  4. 天猫代运营靠谱不靠谱?
  5. linux查看分区blocks,查看ext3文件系统分区的superblock
  6. SDNUOJ 1025.马踏飞燕(DFS||BFS))
  7. 轻松利用frp搭建代理,socket5,端口映射,远程桌面控制
  8. 【微服务|openfeign】使用openfeign远程调用文件上传接口
  9. C语言中的级联if与switch
  10. 一束花送给MyLove:Fany~