Swift5 闭包 Closures 详解

389 阅读7分钟

作为一个刚起步的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 来处理请求完成后的工作。