關於OC中的Block,Swift中的閉包,C++11中的lambda表達式等匿名函數詳解

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#的匿名函數,等等等等,道理都是相通的。好啦!今天就逗比到這裏。謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章