关于OC中的Block,Swift中的闭包,C++11中的lambda表达式等匿名函数详解

时间:2021-12-09 18:53:55

Hello,大家好啊!逗比老师又来给大家逗比啦!今天咱们逗比的内容,就来围绕一个比较棘手的问题——匿名函数。我有一个朋友在学习做iOS开发,他最近就在被这个Block缠绕弥漫,感觉云里雾里,所以希望我来详细讲解一下相关的内容。相信不止他一个,遇到此类问题的人一定不占少数,所以,今天逗比老师就来给大家分享一下这个部分我个人的详细见解。

我们先把视野拉回到C语言中。在C语言中定义一个函数,相信是一件非常容易的事情了,比如说,我使用一个函数传入两个整数,并返回两个整数中较大的一个:

int max(const int num1, const int num2) {
return num1 > num2 ? num1 : num2;
}
这个非常简单,但是,如果我要求写一个这样的函数呢:写一个函数,传入一个无符号整型n和一段代码block,函数将block这段代码重复执行n次。这个功能如果实现起来倒也不是什么难事,关键困难的部分是在于,如何将一个代码块传入一个函数中?如果熟悉JavaScript的朋友应该很容易解决,如果我们在JavaScript中来解决相同的问题,则应该这样来写:
function repeat(n, block) {    for(var index = 0; index < n; index++) {        block()    }}
之所以能这么写,是因为JavaScript是弱类型语言,所以我们不需要指定block的类型,因此,我们可以把block当做一个函数来使用,比如以下方式调用:
function repeat(n, block) {    for (var index = 0; index < n; index++) {block()    }}function test() {    print("Hello!")}repeat(5, test)
这样会在控制台打印出5行Hello!,我们看到,这种问题的解决思路就是,用一个函数来保存代码,然后把函数当做另一个函数的参数传入其中,然后在函数体中调用参数函数。在C语言中,我们也可以使用类似的方法,但是由于C语言是强类型语言,我们需要指定参数的类型,来表示我们将要传入一个什么样的函数,我们把能够实现这种功能的变量,叫做指向函数的指针,请看以下示例:
#include <stdio.h>void repeat(const unsigned n, void (*const block)()) { // 第二个参数传入一个指向函数的指针    for (int index = 0; index < n; index ++) {        block();    }}void test() { // 用一个函数来保存代码    printf("Hello!\n");}int main(int argc, const char * argv[]) {    repeat(5, test); // 注意这里直接传入函数名,后面没有小括号(因为如果写上小括号就表示把函数的返回值传入而不是把函数本身传入)    return 0;}
着重点在于repeat函数的第二个参数上,
void (* const block)()
这个表示声明了一个指针,名为block,并且用const修饰表示一旦被初始化则不能修改其值,这个指针指向一个返回值为void,无形参的函数,当然,你也可以给一个有参数有返回值的函数来做测试,比如我们规定repeat函数的第二个参数是一个传入两个整数并返回一个整数的函数,在函数体内执行n次,并把每一次的返回值的和返回出来(说起来太拗口了,直接看例子吧)
#include <stdio.h>int repeat(const unsigned n, int (*const block)(int, int)) {    int result = 0;    for (int index = 0; index < n; index ++) {        result += block(index, 1);    }    return result;}int test(const int num1, const int num2) {    return num1 * (num1 + num2);}int main(int argc, const char * argv[]) {    int r = repeat(5, test);    printf("%d\n", r);    return 0;}
repeat函数中,每一次循环时都把index和1传入test()函数,然后把得到的返回给加给result,最后返回result的值。

函数指针的用法并不难,如果你在这里还存在问题的话,可以去好好补一补关于C指针的语法方面的知识。由于这里只是作为抛砖引玉,我就不再做更多的介绍了。

接下来我们考虑这样一个问题,尽管我们通过函数指针可以解决往函数中传入代码块的问题,但是,新的问题出现在了我们这个代码块上,我们现在是把代码块保存在了test()这个函数中,试想如果你有100个代码块需要保存,那你是不是就要写100个函数来保存代码?如果每段代码都很长呢?如果其中的很多代码也就仅仅使用了一次或者两次呢?通过函数来保存代码块的做法显然有些浪费成本了。因为我们知道,C语言的函数是全局性的,其生命周期是从定义到程序结束的,换句话说,在程序运行过程中,这些代码块会一直占用着资源,即使它只被调用过很少次甚至没有被调用过。那么,有没有办法能让代码块不放在专门的函数里,而是像变量那样在生命周期结束时自动被释放呢?很遗憾,在C语言中这个问题是否定的,没有这样一种结构。因此,由C语言拓展出的很多语言为了弥补这一不足而创造了相应的数据类型,也就是我们今天的主角——匿名函数。

那么,究竟什么是匿名函数呢?我们还是先来看一下JavaScript中的函数定义吧(如果你不懂JS得话,完全没关系,下面的代码你绝对看得懂)

function func1() {
// 标准定义函数的方式
}

var func2 = function() {
// 匿名函数定义方式
print("Hello!")
}

func2()
前一种定义函数的模式没什么说的,很普通,来看一下后一种,我们先定义了一个变量叫func2,然后直接把一个函数赋值给了它,而且我们注意到,赋值符号的左边表示变量名,右边表示值,我们来看这个值,它是一个函数,但是却没有名字,这就相当于把1赋值给a一样,1是个值,它也没有名字。那么这种函数的表示方式就称作匿名函数。这里需要注意的是,我们是把一个匿名函数赋值给了一个变量func2,而不是说这个函数的名字是func2,这里一定要区分开,变量是变量,函数是函数,这是两码事。

既然有了匿名函数这种东西的存在,我们再写刚才那个例子就不需要专门的把代码块保存到一个函数里了,我们可以把它保存到一个变量中,甚至可以直接传给一个函数,例如:

function repeat(n, block) {
for (var index = 0; index < n; index++) {
block();
}
}

repeat(5, function() {
print("Hello!")
})
注意看,我在调用repeat函数的时候,第二个参数我并不是传入了哪个函数或变量,而是直接传入了一个匿名函数,其实这里的道理就相当于,假如你有一个变量a,值为2,然后你传到参数里的时候,你写func(a)和写func(1)是同样的效果,只不过后者不再需要专门的存储空间来存储这个1。

在Objective-C中,就是使用了Block代码块来实现匿名函数的功能:

@import Cocoa;

void repeat(const unsigned n, void (^block)()) {
for (int index = 0; index < n; index++) {
block();
}
}

int repeat2(const unsigned n, int (^block2)(int, int)) {
int result = 0;
for (int index = 0; index < n; index++) {
result += block2(index, 1);
}
return result;
}

int main(int argc, const char * argv[]) {
repeat(5, ^{
printf("Hello!\n");
});

int r = repeat2(5, ^int(int num1, int num2) {
return num1 * (num1 + num2);
});
printf("%d\n", r);

return 0;
}
可以看到,这里的写法和C语言中函数指针的写法基本上没什么区别,只不过使用Block的话,就可以在调用函数时直接传入一个匿名函数,而不需要再单独的将代码保存到一个函数中了。

由于OC只是C语言的一个简单扩充,因此在匿名函数这里其实也并没有什么特别的东西,Swift语言作为OC的继承人,它使用了闭包来代替OC中的Block来实现匿名函数的功能,其实说实在的,闭包同样没有什么特别的东西,只是语法上略有不同罢了:

import Foundation

func repeatFunc(n: UInt, block: () -> Void) {
for _ in 0..<n {
block()
}
}

func repeatFunc2(n: UInt, block2: (Int, Int) -> Int) -> Int {
var result = 0
for index in 0..<n {
result += block2(Int(index), 1)
}
return result
}

func test() {
print("Hi!")
}

repeatFunc(5) { // 当函数的最后一个参数为闭包时,可以将闭包内容写在括号外面,并省略外部参数名
print("Hello!")
}

/* 上面的函数等价于下面的写法:
repeatFunc(5, block: {
print("Hello!")
})

*/

repeatFunc(5, block: test) // 也可以直接把函数当做闭包传进来

let r = repeatFunc2(5) { (num1, num2) -> Int in
return num1 * (num1 + num2)
}
print(r)
唯一要说Swift闭包比OC的Block强的地方就在于,Swift函数可以当做闭包来使用,而C语言函数却不能够当做OC的Block来使用,除此之外基本上没有区别。如果你对Cocoa库比较熟悉的话就应该能够发现,OC版本中的Block在Swift版本中全部使用了闭包来代替。

与OC和Swift不同,C++11中增加了一种更加强大的匿名函数类型,我们称之为lambda表达式,之所以说它更加强大,是因为它不能能实现上述的功能,还可以实现一些Block和闭包不能够实现的功能,在此之前,我们还是先来看一下刚才那个举烂了的例子,用C++怎么来书写:

#include <iostream>

void repeat(const unsigned n, void (*const block)()) {
for (int index = 0; index < n; index++) {
block();
}
}

int repeate2(const unsigned n, int (*const block2)(int, int)) {
int result = 0;
for (int index = 0; index < n; index++) {
result += block2(index, 1);
}
return result;
}

int main(int argc, const char * argv[]) {
repeat(5, []() {
std::cout << "Hello!" << std::endl;
});

int r = repeate2(5, [](int num1, int num2) -> int {
return num1 * (num1 + num2);
});
std::cout << r << std::endl;

return 0;
}
这个看完我想你都笑了,没错,lambda表达式可以当做函数指针来使用,把一个lambda表达式传给一个相应的函数指针,完全没有问题!而且我想你也应该看到lambda表达式的书写形式了,有人说这个叫括号开会,哈哈,差不多,一个中括号,一个小括号再跟一个大括号,大括号中就是函数体的内容了,而小括号中就是形参,小括号后面还可以跟一个箭头表示返回值(省略也完全没问题,因为如果你函数体中有return语句,编译器会自动推断lambda表达式的返回值类型)。不过,那个中括号是啥呢?这里就是lambda表达式强大的地方所在了。

lambda表达式的第一个中括号,叫做捕获列表,简单的来说,如果你想在函数体中操作函数体之外的变量,又不想(或不方便)通过形参来传递,那么你就可以使用这个捕获列表来进行传递,使用起来非常方便,不过需要注意的是,如果你一旦使用了捕获列表,你就不能够将这个lambda表达式作为函数指针来传递,而只能当做STL中的一个模板类的实例来使用(说着还是太拗口,看例子,看例子)

#include <iostream>
#include <functional> // 注意要包含STL中的functional头文件

void func(const unsigned n, std::function<void()> block) { // 注意这里的形参则不能够使用函数指针,而必须使用std::function,类型为<返回值(参数1,参数2,…)>
for (int index = 0; index < n; index++) {
block();
}
}

int main(int argc, const char * argv[]) {
int val = 0;
func(5, [&val]() { // 捕获了一个val变量的引用
val += 1; // 每调用一次该lambda表达式,就把val加1
});
std::cout << val << std::endl; // var的值为5

return 0;
}
lambda表达式如果用到成员函数中,还可以在捕获列表中传入&, *, this等,会有更大更方便的用途,由于今天的重点在于匿名函数,所以这里不再详细讲解。

好啦!说了这么多,不知道我有没有把匿名函数这个概念讲解清楚。你也应该发现了,同样的一个功能,其实无论使用什么语言其实差别都不大。语言仅仅是工具,而作为程序员,我们一定要学会其中的思想,把知识和具体形式抽离开来,才能做到举一反三。如果你有兴趣,你还可以去看看python的lambda匿名函数,还有lua的匿名函数,C#的匿名函数,等等等等,道理都是相通的。好啦!今天就逗比到这里。谢谢!