闭包 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 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 {
    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

你可能感兴趣的:(闭包 C++、Java、Kotlin)