iOS – Swift中闭包的花式写法以及常见问题总结

在学习OC的时候,总会被Block的用法搞蒙。国外大佬甚至做了一个 “fucking block syntax”的网站来提示Block的用法,如今切换到Swift之后,闭包(Closure)的各种写法再次搞蒙,本文整理Swift中闭包的各种转换,体会Swift的“优雅”之处。


文章案例来自: iOS Caff社区, 该社区有优质的Swift学习资源,欢迎大家加入

正文

本文主要以 sorted(by:)方法实现排序功能,该方法接收一个自定义的排序规则方法来实现对目标集合的排序。

然后通过各种约束去简化闭包,接下来你将看到这样一个变化过程。

/// 待排序数组
let names = ["C", "A", "E", "B", "D"]

/// 1. 外部实现排序方法
func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)

/// 2. 闭包表达式
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

/// 3. 通过上下文推测类型
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )


/// 4. 单一闭包表达式隐式返回
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

/// 5. 缩写参数名
reversedNames = names.sorted(by: { $0 > $1 } )

/// 6. 运算符方法
reversedNames = names.sorted(by: >)

/// 7. 尾随闭包
reversedNames = names.sorted() { $0 > $1 }
/// 8. 甚至。。。
reversedNames = names.sorted { $0 > $1 }

接下来我们逐个分析一下每个闭包,第1个是常规写法,我们不去考虑。

2. 闭包表达式

/// 2. 闭包表达式
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

闭包的基本语法如下:

{ (参数列表) -> 返回类型 in
    // ... // 
}

闭包表达式主体部分开始于关键词 in, 也就是代表闭包的入参和返回值已经声明结束,主体将要开始。

3. 通过上下文推测类型

/// 3. 通过上下文推测类型
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

在本案例中,sorted(by:)由一个 [String] 数组调用, 所以闭包的类型一定是 (String, String) -> Bool 类型,所以可以把入参类型和返回值类型省略

4. 单一闭包表达式隐式返回

/// 4. 单一闭包表达式隐式返回
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

单一表达式 的意思是只有一句表达式, 这里也就是 in 后面只有一句 return s1 > s2, 因为 s1 > s2 明确返回一个Bool值,符合闭包的返回类型,所以可以将 return 也省略。

5. 缩写参数名

/// 5. 缩写参数名
reversedNames = names.sorted(by: { $0 > $1 } )

Swift自动为内联闭包提供参数名缩写写法,可以使用$0, $1, $2等代替参数, 接上上面所说的,可以省略参数类型和返回类型,现在也可以省略参数名s1, s2, 同时in也可以省略,就是现在的写法: $0 > $1

6. 运算符方法

/// 6. 运算符方法
reversedNames = names.sorted(by: >)

此方法用到的是Swift支持运算符重载的功能,此时 > 是一个方法名称,大体类似下面的样子:

func > (_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}

7. 尾随闭包

/// 7. 尾随闭包
reversedNames = names.sorted() { $0 > $1 }
/// 8. 甚至。。。
reversedNames = names.sorted { $0 > $1 }

当闭包作为函数的最后一个参数传入函数时,可以使用尾随闭包,通常是闭包写在函数括号的调用之后。 使用尾随闭包可以不用填写函数入参为闭包那部分的参数,意思就是,本来我们的闭包都需要写在 sorted(by:)by: 后面,现在可以省略 by: ,甚至可以省略 (), 如上述7和8的写法。

这个写法在Xcode的自动补全中非常常见,就如我第一次写Swift的网络请求时,需要传入一个 success: 闭包和 failure: 闭包,然而当自动补全之后,发现 failure 闭包一直在 () 后面,一直以为是我自己出了问题。

如图所示,尾随闭包只算最后一个,图中红色标记为 (), success闭包在()内,而failure闭包是尾随闭包,同时忽略了failure:


Swift函数式编程

通过上述理解,可以发现Swift有些地方写起来很方便,例如遍历一个数组,或者筛选一个数组中的偶数等

let array: [Int] = [1, 2, 3, 4, 5]

/// 让数组中的每个数组翻倍
let a = array.map { $0 * 2 }
print(a)    // [2, 4, 6, 8, 10]

/// 找出数组中的偶数
let b = array.filter { $0 % 2 == 0 }
print(b)    // [2, 4]

通过上述对闭包各种写法的分析,很容易能理解上述闭包的意思,如果按照原始的写法,我们可能还需要去写其他的函数去分析,或者使用 for 去遍历判断。使用闭包让代码变得更优雅。


闭包值捕获

在学OC的Block的时候,对外部变量的使用就有很多学问,比如在Block内部无法修改外部变量,需要加 __block 才可以修改。

// 报错,Variable is not assignable (missing __block type specifier)
int a = 10;
    
void(^block)(void) = ^{
    NSLog(@"%d", a);
    a += 1;
};
    
block();


// 加上 __block 后可以正常运行
__block int a = 10;
    
void(^block)(void) = ^{
    NSLog(@"%d", a);
    a += 1;
};
    
block();

还有block的对外部变量的捕获是截断式的('截断式'是我自己想的一个词,欢迎更正), 也就是说,在定义block的时候,就在block内部copy了一个a的值,例如下面:

int a = 10;
    
void(^block)(void) = ^{
    NSLog(@"%d", a);
};
    
a = 15;
    
block();

此时打印的 a = 10, 而不是15。

而在Swift的闭包里面会简单一点,闭包可以 捕获 它所定义的上下文环境中的常量和变量。在闭包体内可以使用和修改这些常量和变量的值, 即使这些常量、变量的作用域已经不存在了。

重复上述三种情况

var a: Int = 10

var test = {
    print("闭包捕获到的a = \(a)")
    
    a = 15
    print("在闭包内部修改后的a = \(a)")
}

a = 12

test()
print("经过闭包修改后a = \(a)")

在这里我们可以发现,闭包可以方便的控制外部变量。


循环引用, 显式使用self等

在闭包中可以使用 [weak self] 的方式来解决循环引用的问题,例如

loadData { [weak self] (dataString) -> () in
            
    //在闭包中使用self将都是弱引用
    print("\(dataString) \(self?.view)")
}     

同时在闭包中,必须显式使用self,例如在一个下载图片闭包中,下载完成需要设置图片到imageView:

let imgView = UIImageView()

downImage() { (image) -> () in 
    // 图片下载完成后
    imgView.image = image
}

此时会报错

// Implicit use of 'self' in closure; use 'self.' to make capture semantics explicit

也就是说在闭包中必须显式使用self, 即 imgView.image = image 需要改成 self.imgView.image = image

查了一下资料,在Swift中存在一种闭包叫逃逸闭包,可以理解为,将传入的闭包先保存, 可能以后才会调用。 这个时候如果闭包中调用了当前对象的某个属性,例如上述中的imgView, 需要考虑到self是否还存在。

这跟Block外部使用 __weak 后,内部还需要使用 __block 类似,需要保证self还存在。