闭包 C++、Java、Kotlin

Wikipedia关于闭包的定义:
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.
简单说,闭包是能够访问外部环境中自由变量的函数。

软件中一等公民

在闭包的定义中出现了first-class Function.在软件领域中,一等公民(first-class citizen)是什么?Wikipedia中关于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. 一等公民可以作为参数传递、作为函数的返回值、可修改、可赋值给变量。 e.g.在C语言中,数组就不是一等公民,如果数组被作为参数传递,其传递的只是该数组的首地址,而其数组长度会被丢弃。

Function
语言 一等公民 备注
C++ 部分 C++11前虽然支持函数指针,但其不可被修改,故函数非一等公民。C++11的lambda表达式为一等公民
Java 部分 Java8前函数非一等公民,Java8的lambda表达式为一等公民
Kotlin Kotlin函数为一等公民

由表可知,闭包在三种语言的支持情况,C++从C++11开始支持闭包,Java从Java8开始支持闭包,Kotlin由于函数为一等公民天然支持闭包。

闭包

C++闭包

C++实现闭包通常有三种方式,分别为lambda表达式、重载operator运算符和std::bind方式。

lambda表达式

C++11开始引入了lambda表达式,形式如下

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

[capture]:捕获列表。=为值传递,&为引用传递,也可传递变量名或变量引用。
(parameters):参数列表。无入参时可省略。
mutable:可选修饰符。如果加上修饰符,对值传递的捕获变量在lambda表达式内部也可以修改其值,但是不影响外部被捕获的值。
当标明mutable修饰符时,参数列表即便无参数也不可省略。
->return-type: 函数的返回值类型。当返回值可被推断出时可省略。
{statement}:函数体。

Tips:在lambda表达式中,注意值捕获和引用捕获及mutable使用与否的区别。关于lambda表达式的内容不再展开。

传递方式 mutable lambda函数体 外部影响
值传递 lambda体内不能修改该值 变量维持不变
值传递 lambda体内可以修改该值 外部变量维持不变,内部该变量会被累积变化
引用传递 - lambda体内可以修改该值 外部变量变化
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表达式的调用而受影响。在而引用捕获时,lambda表达式的内部逻辑会影响被捕获变量本身。在外部变量捕获方面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的闭包可以通过内部类和lambda表达式实现。

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);
    }
}

以上代码中放开任何一处注释就会出现编译错误。“Local variable defined in an enclosing scope must be final or effectively final”, 定义在封闭范围内的局部变量必须是不可变或实际上不可变的。 对于内部类或lambda表达式,引用的变量可分为两类:外部类成员变量和外部局部变量。通过上面的代码可以发现,外部类变量可以不是final的,而外部局部变量必须实际上是final的。为什么引入这个约束呢?其实这与Java编译器的实现方式有关系,下面为上面代码中的匿名内部类的反编译代码:

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;
    }
}

通过反编译代码可见,首先分析匿名内部类的构造函数。构造函数的第一个入参是外部类的this指针,因此可以通过this指针对外部类变量进行修改。构造函数的第二和第三个入参都是外部的局部变量,并且由于Java参数传递的性质(基本类型传递的是值的拷贝,对象类型传递的是对象引用的拷贝),无论对基本类型还是对象类型,都不会发生变化原值。因此为了避免了概念的混淆,Java引入这条约束。简单的说,Java是值捕获的。而lambda表达式作为一类公民本可以实现引用捕获,但仍然沿用值捕获方式。

Kotlin闭包

Kotlin作为一门现代语言,集C++与Java设计思想优点之大成,语言简洁、表达力强,易于构建DSL、空安全、与Java、JavaScript的转换、支持Gradle编写等等优点,未来可期。Kotlin由于支持函数式编程、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封装在一个类中。而反汇编结果里也存在一个内部类,这个内部类实现了foo里的calc中的lambda表达式部分,同样可以看到Foo$calc$1构造函数的三个入参,第一个入参是外部类的this指针,第二个变量为外部作用域的局部变量,其被封装在IntRef类型中,因此可以修改其值。简单地说,Kotlin是引用捕获。

通过分析可见,闭包在不同语言的表现不同:
1、C++11 lambda表达式可以指定捕获方式为值捕获或引用捕获。
2、Java的lambda表达式和内部类是值捕获,对外部类变量可以读写,但外部局部变量必须实际是final的。
3、Kotlin的lambda表达式为引用捕获。

WalkeR-ZG

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容