Swift 闭包下 | 七日打卡

1,007 阅读5分钟

点赞评论,感觉有用的朋友可以关注笔者公众号 iOS 成长指北,持续更新

本文是 《Swift 100 Days》系列的的第 7 天, Swift 100 Days 是笔者记录自己 Swift 学习的记录,欢迎各位指正。

闭包的类型推断

这之前的例子中,我们不但举例了类型推断时的的闭包的使用,同时也列举了具体的类型。这是为了锻炼我们确定闭包类型的能力。

Swift 可以通过当前代码的上下文,推断出当前闭包的类型。

Swift 是一种强类型编程语言。这意味着每个值都有一个类型,即使它是推断出来的!不要把类型推断误认为“此值没有类型”。一个值总是有一个类型,你只是没有显式地声明它。类型推断会导致理解类型时产生混淆。确保你总是知道你所定义的正确的类型,依赖类型推断的前提是能过掌握类型

笔者在使用 Swift 的闭包时,一直很惊讶于某些表达式竟然可以如此的简短。

我们常用的数组排序功能,我们可以这么使用

let names = ["Zaphod", "Slartibartfast", "Trillian", "Ford", "Arthur", "Marvin"]
let sortedNames = names.sorted(by: <)

数组的实例方法 sorted(by:) 在官方文档中如下定义

func sorted(by areInIncreasingOrder: (Iterator.Element, Iterator.Element) throws -> Bool) rethrows -> [Iterator.Element]

这里我们忽略关键字 throwsrethrows ,留着以后再说。

sorted(by:) 中定义了一个闭包。在之前的学习中,我们知道一个完整的闭包应该这样使用

names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 < s2
})

然后对于有返回值的闭包,我们可以忽略返回值,数组是一个字符串数组,所以我们对传参可以不需要

let sortedNames2 = names.sorted(by: { s1, s2 in
    return s1 < s2
})

然后我们可以使用 $0$1 来代替定义好的闭包入参

names.sorted(by: { $0 < $1 } )

接着,这个方法只有一个入参是闭包,那么可以用尾随闭包语法来进行修改

names.sorted { $0 < $1 }

从方法的定义上我们知道,这个闭包的返回值是一个 Bool 值。在Swift 中 String 类型定义了关于比较操作符(> < ==)的字符串实现,其作为一个函数接受两个 String 类型的参数并返回 Bool 类型的值。最终我们可以简单传递个比较操作符

在Swift中,操作符是顶级函数。它的类型是 (lhs:(), rhs:()) -> Bool,这完全符合 sorted(by:) 的类型!

names.sorted(by: <)

学会如何判断闭包的类型,十分有用。对于很多尾随闭包来说,他们的写法比较简短。有时候在看到这些简短的表达式时,能够逆推出闭包的具体类型,对一个 Swift 开发者来说十分必要。

自动闭包

我们比不是所有的闭包都当做尾随闭包来使用。在前面的例子中,我们也介绍了将闭包作为函数非最后一个参数的例子。用 @autoclosure 关键字标记的闭包称为自动闭包@autoclosure 关键字通过添加 {} 在表达式周围创建一个自动闭包。因此,在向函数传递闭包时可以省略大括号 {}

使用自动闭包的主要优点是在调用闭包时不需要将表达式包装在大括号 {}中。

在正常我们放置在非最后一个参数的闭包时,我们需要将表达式包装在大括号{}

func noAutoClosureFuction(closure:() -> (), msg: String) {
    print(msg)
    closure()
}

noAutoClosureFuction(closure: {
    print("print in closure")
}, msg: "print in function")

然后我们可以通过 @autoclosure 关键字来标记闭包,这样我们就可以省略大括号{}

func autoClosureFuction(closure:@autoclosure () -> (), msg: String) {
    print(msg)
    closure()
}

autoClosureFuction(closure: print("print in closure"), msg: "print in function")

注意:

我们无法向自动闭包传递参数

当我们给闭包增加一个返回时

func autoTailClosureFuction(msg: String, closure:@autoclosure () -> (String)) {
    print(msg)
    let closureString = closure()
    print(closureString)
}
autoTailClosureFuction(msg: "print in function", closure: "print in closure")

将闭包作为返回值

之前我们讨论许多关于将闭包作为参数的使用。在函数章节的时候我们说过,函数的返回值可以是任意类型。所以函数的返回值也可以是一个闭包。

func createAgeCheck(strict: Bool) -> (Int) -> Bool {
	if strict {
		return {
			if $0 <= 21 {
				return true
			} else {
				return false
			}
		}
	} else {
		return {
			if $0 <= 18 {
				return true
			} else {
				return false
			}
		}
	}
}
let ageCheck = createAgeCheck(strict: true)
let result = ageCheck(20)
print(result)

将闭包作为返回值的最大好处是当我们需要处理更多逻辑时。

例如上面的例子,当我们需要判断是否是严格意义上的青少年时,我们这么处理。如果是用其他方式来做,我们可能需要在代码中做一个条件语句判断,然后用另外两个函数处理这个年龄。而现在我们仅仅需要返回一个闭包方法,通过函数的传参strict 来判断返回那个闭包方法。有时候在处理复杂逻辑时,这种很好用。

值捕获

闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。

Swift 中,可以捕获值的闭包的最简单形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。

如果你在闭包中使用任何外部值,Swift 会捕获它们—将它们存储在闭包里,这样即使它们不再存在也可以修改。

func travel() -> (String) -> Void {
    var counter = 1
    return {
        print("\(counter). I'm going to \($0)")
        counter += 1
    }
}

result("London")
result("London")
result("London")

即使变量counter 是在函数 travel() 中创建的,它也会被闭包捕获,因此对于该闭包,它仍然是存在的。

作用域

Swift中的每个变量、函数和闭包都有一个作用域。作用域决定你可以在何处访问特定的变量、函数或闭包。如果一个变量、函数或闭包不在作用域中,你就不能访问它。作用域有时称为上下文

为了了解值捕获的具体意义,在这里我们稍微提一下作用域的功能。

我们一般认为一个{}为一个作用域,作用于是层层嵌套的。里层作用域可以捕获到外层作用域的属性或参数,但是外层作用与无法使用内层作用域的参数。一个 Swift 文件定义的属性或方法被认为是一个大的大括号{},虽然它没有。

闭包是引用类型

为什么闭包可以做到值捕获?详细可以阅读这篇文章 Closures Capture Semantics: Catch them all!

首先,当闭包捕获一个值时,它会自动创建对该值的强引用。这被称作强引用(strong reference cycle),会引起内存泄漏(memory leak)。

在使用值捕获时,我们需要格外注意内存安全。Swift 提供了一种优雅的方法来解决这个问题,称之为闭包捕获列表(closure capture list)。就像你可以将类的属性标记为弱引用(weak)一样,您也可以将闭包中捕获的值标记为弱引用(weak)或无主引用(unowned)。

捕获列表是一个以逗号分隔的变量名列表,前缀为weakunowned,并用方括号括起来。

定义捕获列表

如果闭包有参数列表和返回类型,把捕获列表放在它们前面:

let someClosure = {
    [unowned self, weak delegate = self.delegate]
    (index: Int, stringToProcess: String) -> String in
    // 这里是闭包的函数体
}

如果闭包没有指明参数列表或者返回类型,它们会通过上下文推断,那么可以把捕获列表和关键字 in 放在闭包最开始的地方:

let someClosure = {
    [unowned self, weak delegate = self.delegate] in
    // 这里是闭包的函数体
}
weakunowned

在定义捕获列表来避免循环引用时,需要合理使用关键字。以下是两个关键字的区别

  • weak关键字表示捕获的值可以变为 nil
  • unowned 关键字表示捕获的值永远不会变为 nil

注意在使用 weak 关键字修饰时,修饰的变量变成了可选类型。

你可以查看更多关于 Swift 内存方面的知识。Automatic Reference Counting

逃逸和非逃逸闭包

当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸

默认情况下,Swift 函数中作为参数的闭包都应该在函数返回之前被调用。即函数闭包为非逃逸闭包。

什么时候我们需要使用逃逸闭包呢?

  • 闭包作为参数来使用
  • 同步操作等待操作完成/完成,然后移动到下一个语句(从上到下的顺序)。而且,即使当前操作尚未完成,异步也会转移到下一个语句。
var closure = {
    print("closure called")
}

closure()

func testFunctionWithEscapingClosureVarable(myClosure:@escaping () -> Void) {
    print("function called")
    closure = myClosure
    myClosure()
    return
}
testFunctionWithEscapingClosureVarable {
    print("myClosure called")
}

closure()


func testFunctionWithEscapingClosureAsync(myClosure: @escaping () -> Void) {
    print("function called")
    DispatchQueue.main.async {
        myClosure()
    }
    return
}

testFunctionWithEscapingClosureAsync {
    print("closure called")
}

可以尝试把 @escaping 给去掉,看看会发生什么。

感谢你阅读本文! 🚀