作为一个刚起步的ioser, 一开始对闭包这玩意真的理解无能,在开发中磕磕碰碰地用着,加上平时阅读到的一些资料,写一篇博客记录梳理一下,也算自己的查漏补缺了。
先来看一下官方定义:
Closures are self-contained blocks of functionality that can be passed around and used in your code.
闭包是自包含的代码块,可以在代码中传递和使用。
也就是说, 你可以把闭包赋值给一个变量,在变量的传递中, 其他的函数可以调用闭包并且执行其中的代码,这时候的闭包就和普通函数没啥区别。
我们来写一个闭包:
let birthday = {
print ("happy birthday")
}
birthday()
这就是最简单的闭包,我们给常量“birthday” assign 了一个代码块, 也就是一个闭包,闭包的内容是打印一句生日快乐。 然后我们像调用函数一样调用了闭包。 或者,你也可以这样写:
let birthday = {
print ("happy birthday")
}()
这样闭包会自动调用执行。
现在“birthday” 的类型,和闭包的类型,是() -> () ,也就是说,我们在调用birthday的时候,不用传递任何初始值给它,这个闭包也不会return任何东西。
让我们来加点料:
let birthday:(String) -> () = { name in
print("Happy birthday, \(name)!")
}
birthday("豆国华")
输出为: Happy birthday, 豆国华!
我们给birthday指定了类型,它是:(String)-> () , 当然闭包的类型也是: (String)-> ()
也就是说,当我们调用birthday 时, 我们需要给它传递一个String类型的值, 但闭包中不会返回任何的东西,所以它的返回还是()
我们看到上面这个闭包中,我们新命名了一个name ,这是我们自定义的闭包第一个参数,在 in 后, 是我们要执行的内容。 实际上我们可以用通配符代替name,省略 in :
let birthday:(String) -> () = {
print("Happy birthday, \($0)!")
}
这里的$0 代表闭包中的第一个参数。这样会简洁一些。但阅读性不是太好。
下面我们来聊聊闭包的类型,这对闭包的理解很重要:
每一个闭包都有自己的类型,这一点和变量,常量这些玩意都一样。
我们上面写的闭包,完整地写法如下:
let birthday:(String) -> () = { (name:String) -> () in
···
}
它的对应名字为:
let closureName:(parameter types) -> return type = { (parameter name:parameter type) -> return type in
···
}
birthday 是闭包的名称。 第一个 (String) -> () 是闭包的类型, 它表示: 闭包接收一个字符串,没有返回类型。 第二个(name:String)->()是相同的闭包类型,除了它将闭包的第一个参数命名为name。 它是闭包表达式的一部分。
当然,闭包可以像函数一样具有返回值,如:
(Int, Int) -> Double // 接收两个Int , 返回 一个Double 类型 的值 () -> Int //不接收任何值,返回一个Int类型的值。
箭头左边是闭包的输入值, 箭头右边是闭包的输出值。
当然, 闭包中也可以使用可选类型,任何输入参数类型和返回值都可以是可选类型。比如: (String?, Int)-> Int?
或者整个闭包都为可选: let birthday: ((String) -> ())? = {}
这代表birthday是nil或者包含一个闭包。
闭包中的类型推断
我们知道Swift 是一门强类型语言,它有很强的类型推断能力,比如 :
let age = 24
我们不用指定类型,Swift 也会推断出这是Int型。所以我们在闭包中,可以省略表达式的很多内容,Swift会为我们作推断。我们来看一个例子:
let names = ["zhaoxiaoxiong", "pengcheng", "douguohua" , "chenke"]
let sortNames = names.sorted(by: <)
print(sortNames)
// 打印结果为: ["chenke", "douguohua", "pengcheng", "zhaoxiaoxiong"]
sorted() 是Swift为我们提供的排序方法,它接收一个闭包。 但我们只给了它一个 “<” ,你可能看不太懂,别急,我们往下看。
其实我们可以写个完整复杂版的:
names.sorted(by: { (name1: String, name2: String) -> Bool in
return name1 < name2
})
这样是不是好理解多了,我们给by:传递了一个闭包, 闭包中有两个String类型参数,它返回一个bool类型。 闭包的类型为: (String, String)-> Bool
在Swift的类型推断帮助下,我们可以简化代码:
names.sorted(by: { name1, name2 in return name1 < name2 })
因为return是单表达式,我们可以再简化:
names.sorted(by: { name1, name2 in name1 < name2 })
基于此,我们可以用通配符进行再次简化:
names.sorted(by: { $0 < $1 })
闭包还有一个语言特性,叫做尾随闭包: 如果闭包是函数的最后一个(或唯一)参数,则可以将闭包写在函数的括号之外,并且忽略参数标签。 基于此,我们可以再简化代码:
names.sorted{ $0 < $1 } // sorted()函数只有一个参数by: ,我们完全可以省略它,去掉小括号,把闭包写在函数后面。
或者回到我们最一开始的写法:
names.sorted(by: <)
这个写法是因为: 我们使用小于运算符<作为闭包。在Swift中,运算符是顶级函数。它的类型是(lhs:(),rhs:())-> Bool,正好适合sorted(by :)的类型。
类型推断会帮助我们省略很多繁琐的代码,更简洁高效,比如我们写一个计算平方的闭包:
let squared = { $0 * $0 }
当我们需要用到时:
squared(15) // 225
但要把握好度,毕竟我们要兼顾代码的可阅读性。满屏都是类型推断的闭包,下一个接手项目的程序员分分钟想砍人的说。在实际的开发中,尾随闭包用的会比较多。
闭包的值捕获
官方说: 在Swift中,闭包从其周围的范围捕获变量和常量。
这是什么意思呢?其实没什么玄乎的。
我们知道,每个变量,常量,函数,闭包都有它的作用域,作用域决定了你能在什么范围之内访问属性,函数,闭包。 如果这些变量或者函数闭包超出访问范围,你是访问不到的。
比如:
我们写一个类:
class Room {
var person = "chenke"
var globalHourInClass = 2
func relax() -> String{
var stuff = "listen music"
var scopeHourInMethod = 3
return stuff
}
func relaxTime( hours: Int) -> Int {
let baseHour = 1
let todayHour = {
return baseHour + hours
}
return todayHour()
}
}
在类中定义的属性 person 是全局类范围的一部分。 我们可以在该类中的任何位置设置并获取属性的值。
在函数中定义的变量 stuff 是局部函数范围的一部分。 我们只可以在该函数中的任何位置设置并获取属性的值。
我们如果在另一个类中访问person 或者在Room 类中 relax方法外访问 stuff, 都超出了访问范围,会报错。
让我们看看闭包是怎么进行值捕获的:
我们来实例化Room 类
var room = Room()
print(room.relaxTime(hours: 3))
控制台输出为:
4
可以看到,在方法relaxTime中,闭包todayHour 捕获了 baseHour 的值,虽然闭包内没有对baseHour进行定义,但是闭包的值捕获可以让它使用这个值。那它能随便捕获值吗? 答案是否定的,比如我们在relax方法里有一个scopeHourInMethod ,闭包todayHour就无法捕获到它的值。但对于全局范围内的globalHourInClass ,闭包可以通过self.globalHourInClass来使用。
强引用和捕获列表
我们在这篇文章里对iOS的内存管理不多展开篇幅,因为我也没有太搞明白目前哈哈哈哈。这里放一个官网的link小伙伴们可以自己去读: docs.swift.org/swift-book/…
我们暂时回到闭包上,先简单了解以下:
当闭包捕获一个值时,它会自动创建对该值的强引用。这会导致系统不会自动释放对象,造成内存泄漏,APP可能就crash 了。
我们如何解决它呢?
我们可以使用捕获列表来打破强大的参考周期。就像可以将类的属性标记为弱一样,也可以将闭包中捕获的值标记为弱或无主引用。
看个例子:
class Database {
var data = 0
}
var database = Database()
database.data = 110
let calculate = { multiplier in
return database.data * multiplier
}
let result = calculate(2)
print(result)
我们定义了一个database class , 然后实例化它赋给data一个数值110。 我们在闭包calculate中直接进行对database进行值捕获,然后传给闭包一个 multipler把result 打印出来,在我们运行的时候,APP 会crash。
这就是因为值捕获后的强引用导致了系统没有及时释放database对象,造成了内存泄漏。
我们使用捕获列表来解决这个问题:
class Database {
var data = 0
}
let database = Database()
database.data = 110
let calculate = { [weak database] multiplier in
return database!.data * multiplier
}
let result = calculate(2)
print(result) //220
捕获列表是用逗号分隔的变量名列表,前缀为弱或无名,并括在方括号中。 like:
[weak self]
[unowned navigationController]
[unowned self, weak database]
我们使用捕获列表来指定需要将特定捕获值引用为weak或unowned。 weak和unowned都会打破强引用循环,使得闭包不会保留捕获的对象。
它们的含义如下:
weak关键字表示捕获的值可以为nil
unowned关键字表示捕获的值永远不会为零
当闭包和捕获的值始终相互引用,并且始终同时解除分配时,通常使用unown。一个例子是viewController中的[unown self],其中闭包永远不会超过viewController。
当捕获的值在某些时候变为nil时,通常使用weak。当闭包的生存期超过了其创建时所用的上下文时(例如,在完成冗长的任务之前已释放的viewController),可能会发生这种情况。这样,捕获的值是可选的。
网络请求中的Completion Handlers
我们经常在网络请求中使用闭包,闭包能更好地帮我们处理异步的回调。
比如:
let task = session.dataTask(with: "http://example.com/api", completionHandler: { data, response, error in
// Do something...
})
这是我们最常见的session.dataTask方法,我们用completionHandler 来处理请求完成后的工作。