閉包 C++汽煮、Java冻记、Kotlin

Wikipedia關(guān)于閉包的定義:
In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function together with an environment. The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created. Unlike a plain function, a closure allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.
簡單說睡毒,閉包是能夠訪問外部環(huán)境中自由變量的函數(shù)。

軟件中一等公民

在閉包的定義中出現(xiàn)了first-class Function.在軟件領(lǐng)域中檩赢,一等公民(first-class citizen)是什么吕嘀?Wikipedia中關(guān)于first-class citizen的定義:In programming language design, a first-class citizen (also type, object, entity, or value) in a given programming language is an entity which supports all the operations generally available to other entities. These operations typically include being passed as an argument, returned from a function, modified, and assigned to a variable. 一等公民可以作為參數(shù)傳遞违寞、作為函數(shù)的返回值、可修改偶房、可賦值給變量趁曼。 e.g.在C語言中,數(shù)組就不是一等公民棕洋,如果數(shù)組被作為參數(shù)傳遞挡闰,其傳遞的只是該數(shù)組的首地址,而其數(shù)組長度會被丟棄掰盘。

Function
語言 一等公民 備注
C++ 部分 C++11前雖然支持函數(shù)指針摄悯,但其不可被修改,故函數(shù)非一等公民愧捕。C++11的lambda表達式為一等公民
Java 部分 Java8前函數(shù)非一等公民奢驯,Java8的lambda表達式為一等公民
Kotlin Kotlin函數(shù)為一等公民

由表可知,閉包在三種語言的支持情況次绘,C++從C++11開始支持閉包瘪阁,Java從Java8開始支持閉包,Kotlin由于函數(shù)為一等公民天然支持閉包邮偎。

閉包

C++閉包

C++實現(xiàn)閉包通常有三種方式管跺,分別為lambda表達式、重載operator運算符和std::bind方式禾进。

lambda表達式

C++11開始引入了lambda表達式豁跑,形式如下

[capture] (parameters) mutable ->return-type {statement}

[capture]:捕獲列表。=為值傳遞泻云,&為引用傳遞艇拍,也可傳遞變量名或變量引用。
(parameters):參數(shù)列表壶愤。無入?yún)r可省略淑倾。
mutable:可選修飾符。如果加上修飾符征椒,對值傳遞的捕獲變量在lambda表達式內(nèi)部也可以修改其值娇哆,但是不影響外部被捕獲的值。
當標明mutable修飾符時勃救,參數(shù)列表即便無參數(shù)也不可省略碍讨。
->return-type: 函數(shù)的返回值類型。當返回值可被推斷出時可省略蒙秒。
{statement}:函數(shù)體勃黍。

Tips:在lambda表達式中,注意值捕獲和引用捕獲及mutable使用與否的區(qū)別晕讲。關(guān)于lambda表達式的內(nèi)容不再展開覆获。

傳遞方式 mutable lambda函數(shù)體 外部影響
值傳遞 lambda體內(nèi)不能修改該值 變量維持不變
值傳遞 lambda體內(nèi)可以修改該值 外部變量維持不變马澈,內(nèi)部該變量會被累積變化
引用傳遞 - lambda體內(nèi)可以修改該值 外部變量變化
auto foo(int a)
{
    int b = 0;
    return [=](int c) mutable -> int
    {
        ++b;
        std::cout << "b: " << b << std::endl;
        return a + b + c;
    };
}
    auto f = foo(5);
    std::cout << "f(10): " << f(10) << endl;
    std::cout << "f(10): " << f(10) << endl;

個人最喜歡C++lambda表達式捕獲方式,它對值傳遞弄息、引用傳遞是由程序員自己指定痊班,清晰明了,在值傳遞時摹量,不論是否添加mutable涤伐,被捕獲的外部變量的值不會被lambda表達式的調(diào)用而受影響。在而引用捕獲時缨称,lambda表達式的內(nèi)部邏輯會影響被捕獲變量本身凝果。在外部變量捕獲方面C++與Java與Kotlin的方式不同。

重載operator運算符
class foo{
 public:
    foo(int a) : a(a){}
    auto operator()(int b){
        return a + b;
    }
 private:
    int a;
};
    auto f = foo(5);
    std::cout << "f(10): " << f(10) << endl;
    std::cout << "f(10): " << f(10) << endl;
std::bind
auto foo(int a, int b){
    return a + b;
}
    int a = 10;
    auto f = std::bind(foo, a, std::placeholders::_1);
    std::cout << "f(10): " << f(10) << endl;
    a = 20;
    std::cout << "f(10): " << f(10) << endl;
Java閉包

Java的閉包可以通過內(nèi)部類和lambda表達式實現(xiàn)睦尽。

interface Add {
    int add();
}

public class Foo {
    int a;

    public Foo(int a) {
        super();
        this.a = a;
    }

    public int calc_innerclass(int c) {
        int b = 20;
        return new Add() {
            @Override
            public int add() {
                ++a;
                // ++b;
                // ++c;
                System.out.println("a = " + a + ", b = " + b);
                return a + b + c;
            }
        }.add();
    }

    public int calc_lambda(int c) {
        int b = 20;
        Add add = () -> {
            ++a;
            // ++b;
            // ++c;
            System.out.println("a = " + a + ", b = " + b);
            return a + b + c;
        };
        return add.add();
    }

    public static void main(String[] args) {
        Foo foo = new Foo(10);
        System.out.println(foo.calc_innerclass(10));
        System.out.println(foo.calc_lambda(10));
        System.out.println(foo.a);
    }
}

以上代碼中放開任何一處注釋就會出現(xiàn)編譯錯誤器净。“Local variable defined in an enclosing scope must be final or effectively final”, 定義在封閉范圍內(nèi)的局部變量必須是不可變或?qū)嶋H上不可變的当凡。 對于內(nèi)部類或lambda表達式掌动,引用的變量可分為兩類:外部類成員變量和外部局部變量。通過上面的代碼可以發(fā)現(xiàn)宁玫,外部類變量可以不是final的,而外部局部變量必須實際上是final的柑晒。為什么引入這個約束呢欧瘪?其實這與Java編譯器的實現(xiàn)方式有關(guān)系,下面為上面代碼中的匿名內(nèi)部類的反編譯代碼:

class Foo$1 implements Add {
    Foo$1(Foo var1, int var2, int var3) {
        this.this$0 = var1;
        this.val$b = var2;
        this.val$c = var3;
    }

    public int add() {
        ++this.this$0.a;
        System.out.println("a = " + this.this$0.a + ", b = " + this.val$b);
        return this.this$0.a + this.val$b + this.val$c;
    }
}

通過反編譯代碼可見匙赞,首先分析匿名內(nèi)部類的構(gòu)造函數(shù)佛掖。構(gòu)造函數(shù)的第一個入?yún)⑹峭獠款惖膖his指針,因此可以通過this指針對外部類變量進行修改涌庭。構(gòu)造函數(shù)的第二和第三個入?yún)⒍际峭獠康木植孔兞拷姹唬⑶矣捎贘ava參數(shù)傳遞的性質(zhì)(基本類型傳遞的是值的拷貝,對象類型傳遞的是對象引用的拷貝)坐榆,無論對基本類型還是對象類型拴魄,都不會發(fā)生變化原值。因此為了避免了概念的混淆席镀,Java引入這條約束匹中。簡單的說,Java是值捕獲的豪诲。而lambda表達式作為一類公民本可以實現(xiàn)引用捕獲顶捷,但仍然沿用值捕獲方式。

Kotlin閉包

Kotlin作為一門現(xiàn)代語言屎篱,集C++與Java設(shè)計思想優(yōu)點之大成服赎,語言簡潔葵蒂、表達力強,易于構(gòu)建DSL重虑、空安全践付、與Java、JavaScript的轉(zhuǎn)換嚎尤、支持Gradle編寫等等優(yōu)點荔仁,未來可期。Kotlin由于支持函數(shù)式編程芽死、lambda表達式自然支持閉包乏梁。

class Foo(var a: Int) {
    fun calc(c: Int): () -> Int {
        var b = 20;
        return {
            ++a;
            ++b;
            println("a = " + a + ", b = " + b);
            a + b + c;
        };
    }
}

fun main() {
    var foo = Foo(10).calc(20);
    println(foo());
    println(foo());
}

通過代碼可以看到,與Java不同关贵,Kotlin不但可以修改外部類變量a遇骑,同時也可以修改外部的局部變量b。那為什么有這種差異呢揖曾,同樣給出反編譯的代碼:

public final class Foo {
    private int a;

    @NotNull
   public final Function0<Integer> calc(int c) {
      IntRef b = new IntRef();
      b.element = 20;
      return (Function0)(new 1(this, b, c));
   }

    public final int getA() {
        return this.a;
    }

    public final void setA(int var1) {
        this.a = var1;
    }

    public Foo(int a) {
        this.a = a;
    }
}

final class Foo$calc$1 extends Lambda implements Function0<Integer> {
    public final int invoke() {
        Foo var10000 = this.this$0;
        this.this$0.setA(this.this$0.getA() + 1);
        var10000.getA();
        IntRef var3 = this.$b;
        ++this.$b.element;
        int var4 = var3.element;
        String var1 = "a = " + this.this$0.getA() + ", b = " + this.$b.element;
        boolean var2 = false;
        System.out.println(var1);
        return this.this$0.getA() + this.$b.element + this.$c;
    }

    Foo$calc$1(Foo var1, IntRef var2, int var3) {
        super(0);
        this.this$0 = var1;
        this.$b = var2;
        this.$c = var3;
    }
}

通過反編譯的代碼看落萎,外部變量b被封裝在IntRef里。而非Java中的int類型炭剪。IntRef只是把int封裝在一個類中练链。而反匯編結(jié)果里也存在一個內(nèi)部類,這個內(nèi)部類實現(xiàn)了foo里的calc中的lambda表達式部分奴拦,同樣可以看到Foo$calc$1構(gòu)造函數(shù)的三個入?yún)⒚焦模谝粋€入?yún)⑹峭獠款惖膖his指針,第二個變量為外部作用域的局部變量错妖,其被封裝在IntRef類型中绿鸣,因此可以修改其值。簡單地說潮模,Kotlin是引用捕獲。

通過分析可見痴施,閉包在不同語言的表現(xiàn)不同:
1擎厢、C++11 lambda表達式可以指定捕獲方式為值捕獲或引用捕獲。
2晾剖、Java的lambda表達式和內(nèi)部類是值捕獲锉矢,對外部類變量可以讀寫,但外部局部變量必須實際是final的齿尽。
3沽损、Kotlin的lambda表達式為引用捕獲。

WalkeR-ZG

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末循头,一起剝皮案震驚了整個濱河市绵估,隨后出現(xiàn)的幾起案子炎疆,更是在濱河造成了極大的恐慌,老刑警劉巖国裳,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件形入,死亡現(xiàn)場離奇詭異,居然都是意外死亡缝左,警方通過查閱死者的電腦和手機亿遂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渺杉,“玉大人蛇数,你說我怎么就攤上這事∈窃剑” “怎么了耳舅?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長倚评。 經(jīng)常有香客問我浦徊,道長,這世上最難降的妖魔是什么天梧? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任盔性,我火速辦了婚禮,結(jié)果婚禮上呢岗,老公的妹妹穿的比我還像新娘纯出。我一直安慰自己,他們只是感情好敷燎,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著箩言,像睡著了一般硬贯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上陨收,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天饭豹,我揣著相機與錄音,去河邊找鬼务漩。 笑死拄衰,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的饵骨。 我是一名探鬼主播翘悉,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼居触!你這毒婦竟也來了妖混?” 一聲冷哼從身側(cè)響起老赤,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎制市,沒想到半個月后抬旺,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡祥楣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年开财,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片误褪。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡责鳍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出振坚,到底是詐尸還是另有隱情薇搁,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布渡八,位于F島的核電站啃洋,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏屎鳍。R本人自食惡果不足惜宏娄,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望逮壁。 院中可真熱鬧孵坚,春花似錦、人聲如沸窥淆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽忧饭。三九已至扛伍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間词裤,已是汗流浹背刺洒。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吼砂,地道東北人逆航。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像渔肩,于是被迫代替她去往敵國和親因俐。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容