关于“闭包”

一、闭包的定义。

有很多不同的人都对闭包过进行了定义,这里收集了一些。

是引用了自由变量的函数。这个函数通常被定义在另一个外部函数中,并且引用了外部函数中的变量。 – <>

是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。– <>

是一个匿名的代码块,可以接受参数,并返回一个返回值,也可以引用和使用在它周围的,可见域中定义的变量。– Groovy [‘ɡru:vi]

是一个表达式,它具有自由变量及邦定这些变量的上下文环境。

闭包允许你将一些行为封装,将它像一个对象一样传来递去,而且它依然能够访问到原来第一次声明时的上下文。

是指拥有多个变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

闭包是可以包含自由(未绑定)变量的代码块;这些变量不是在这个代码块或者任何全局上下文中定义的,而是在定义代码块的环境中定义。

在这些定义中都有一些关键字:变量、函数、上下文等,闭包在回调函数、函数式编程、Lambda表达式中有重要的应用,为了更深刻的理解闭包,我们会试图通过JavaScript、C#和JAVA的代码进行举例,不过本次的重点还是通过JAVA如何这内部类来实现闭包,以及闭包的应用。

二、JavaScript中的闭包。

在JavaScript中,闭包是通过函数的嵌套来实现,以下是一个简单的例子:

复制代码

\JSClosure\Closure1.htm

复制代码
这段代码的特点:

1、函数f1()还回了函数f2()

2、函数f2()引用了f1()定义的局变量

正常来讲,我们在外部是不能操作到f1()函数内部所定义的局部变量n,但是通过变通的方法,我们在f1()函数内部定义了一个新的函数f2(),通过f2()输出其外围函数的局部变量n,f2()是f1()的内部函数,对于f2()来说其外围函数所定义的变量、函数等上下文是可以被内部函数所访问到的;最后在f1()函数中再调用f2()以在f1()被调用时触发对f2()的调用,从而把局部变量输出。 我们对照一下闭包的定义:”引用了自由变量的函数”,这里的n就是定义中的自由变量,而函数f2()通过邦定自由变量n从而形式了一个闭包。

二、.NET中的闭包。

在.NET中是通过delegate委托实现闭包的,在C#2.0时代可以通过匿名方法(函数)生成,在C#3.0时代建议使用Lambda生成,但是无论版本怎么变化,其本质还是通过delegate实现,其它形式都是些语法糖。(Lambda表达式实质上还是上生成了匿名函数)。从本质上来讲,最终于生成IL代码后,delegate其实就是一个继承了System.MulticastDelegate 或 System.Delegate的类。

这里是一个匿名方法的例子:

复制代码

\DelegateClosure\Program.cs
public static void TestDelegate(string url)
{
WebRequest request = HttpWebRequest.Create(url);
request.BeginGetResponse(delegate(IAsyncResult ar)
{
using (WebResponse response = request.EndGetResponse(ar))
{
Console.WriteLine(“{0}: {1}”, url, response.ContentLength);
}
}, null);
}
复制代码

这个例子是通过WebRequest获取某个url指定网页的内容大小的示例程序,这里的BeginGetResponse方法需要接收一个委托类型的变量。

delegate void AsyncCallback(IAsyncResult ar);

我们知道,delegate本质上最终于会生成一个类,而在这个委托对象内部分别邦定了request变量和url参数,在这个例子里我们说这个匿名方法以及它所邦定的变量构成了一个闭包。

如果使用Lambda表达式,可以写成这样:

复制代码

\DelegateClosure\Program.cs
public void TestLambda()
{
WebRequest request = HttpWebRequest.Create(url);
request.BeginGetResponse(ar =>
{
using (WebResponse response = request.EndGetResponse(ar))
{
Console.WriteLine(“{0}: {1}”, url, response.ContentLength);
}
}, null);
}
复制代码

可以看到Lambda表达式的代码更为简洁,但本质上它还是生成了匿名函数。

三、JAVA中的闭包。

在JAVA中,闭包是通过“接口+内部类”实现,像C#的delegate一样,JAVA的内部类也可以有匿名内部类。我们现在就来详细认识一下JAVA内部类。

1、内部类。

顾名思义,内部类就是将一个类定义在另一个类的内部。在JAVA中,内部类可以访问到外围类的变量、方法或者其它内部类等所有成员,即使它被定义成private了,但是外部类不能访问内部类中的变量。这样通过内部类就可以提供一种代码隐藏和代码组织的机制,并且这些被组织的代码段还可以自由的访问到包含该内部类的外围上下文环境。

这里提供了一个例子展示这种机制:

复制代码

/JavaClosure/src/innerclass/DemoClass1.java
public class DemoClass1 {
private int length =0;
//private|public
private class InnerClass implements ILog
{
@Override
public void Write(String message) {
//DemoClass1.this.length = message.length();
length = message.length();
System.out.println(“DemoClass1.InnerClass:” + length);
}
}

public ILog logger() {
    return new InnerClass();
}

public static void main(String[] args){
DemoClass1 demoClass1 = new DemoClass1();
demoClass1.logger().Write(“abc”);

    //.new
    DemoClass1 dc1 = new DemoClass1();
    InnerClass ic = dc1.new InnerClass();
    ic.Write(“abcde”);
}

}
复制代码
该例子的主要功能是实现一个写日志的ILog接口,但是该接口的类被定义在DemoClass1这个外围类中了,而且这个InnerClass内部类还可以访问其外围类中的私有变量length。

1.1、.new

从上面的例子可见,InnerClass是定义在DemoClass1内部的一个内部类,而且InnerClass还可以是Private。

如何创建这个InnerClass的实例? 可以通过外围类的实例进行创建,如:

   DemoClass1 dc1 = new DemoClass1();
    InnerClass ic = dc1.new InnerClass();
    ic.Write(“abcde”);

1.2、.this

如何通过this显式引用外围类的变量?通过此格式进行引用:{外围类名}.this.{变量名称}。如:

DemoClass1.this.length = message.length();

2、局部内部类。

局部内部类是指在方法的作用域内定义的的内部类。

复制代码

/JavaClosure/src/innerclass/DemoClass2.java
public class DemoClass2 {
private int length =0;

public ILog logger() {
//在方法体的作用域中定义此局部内部类
class InnerClass implements ILog
{
@Override
public void Write(String message) {
length = message.length();
System.out.println(“DemoClass2.InnerClass:” + length);
}
}
return new InnerClass();
}
}
复制代码
因为InnerClass类是定义在logger()方法体之内,所以InnerClass类在方法的外围是不可见的。

3、匿名内部类。

顾名思义,匿名内部类就是匿名、没有名字的内部类,通过匿名内部类可以更加简洁的创建一个内部类。

复制代码

/JavaClosure/src/innerclass/DemoClass3.java
public class DemoClass3 {
private int length =0;

public ILog logger() {
return new ILog() {
    @Override
    public void Write(String message) {
          length = message.length();
          System.out.println(“DemoClass3.AnonymousClass:” + length);
    }
};
}

}
复制代码
由此可见,要创建一个匿名内部类,可以new关键字来创建。

格式:new 接口名称(){}

格式:new 接口名称(args…){}

4、final关键字。

闭包所绑定的本地变量必须使用final修饰符,以表示为一个恒定不变的数据,创建后不能被更改。

复制代码

/JavaClosure/src/innerclass/DemoClass4.java
public class DemoClass4 {
private int length =0;

public ILog logger(int level) {//final int level
    //final
    final int logLevel = level+1;

    switch(level)
    {
        case 1:
            return new ILog() {
                @Override
                public void Write(String message) {
                    length = message.length();
                    System.out.println(“DemoClass4.AnonymousClass:InfoLog ”

+ length);
System.out.println(logLevel);
}
};
default:
return new ILog() {
@Override
public void Write(String message) {
length = message.length();
System.out.println(“DemoClass4.AnonymousClass:ErrorLog ”
              + length);
System.out.println(logLevel);
}
};

    }
}

public static void main(String[] args){
    DemoClass4 demoClass4 = new DemoClass4();
    demoClass4.logger(1).Write(“abcefghi”);
}

}
复制代码
从例子中可以看到,logger方法接受了一个level参数,以表示要写的日志等级,这个level参数如果直接赋给内部类中使用,会导致编译时错误,提示level参数必须为final,这种机制防止了在闭包共享中变量取值错误的问题。解决方法可以像例子一样在方法体内定义一下新的局部变量,标记为final,然后把参数level赋值给它:

final int logLevel = level ;
或者直接参数中添加一个final修饰符:

public ILog logger(final int level {

5、实例初始化。

匿名类的实例初始化相当于构造器的作用,但不能重载。

复制代码

/JavaClosure/src/innerclass/DemoClass5.java
public ILog logger(final int level) throws Exception {

    return new ILog() {
        {
            //实例初始化,不能重载 
            if(level !=1)
                throw new Exception(“日志等级不正确!”);
        }

        @Override
        public void Write(String message) {
            length = message.length();
            System.out.println(“DemoClass5.AnonymousClass:” + length);
        }
    };
}

复制代码
匿名内部类的实例初始化工作可以通过符号 {…} 来标记,可以在匿名内部类实例化时进行一些初始化的工作,但是因为匿名内部类没有名称,所以不能进行重载,如果必须进行重载,只能定义成命名的内部类。

四、为什么需要闭包。

闭包的价值在于可以作为函数对象或者匿名函数,持有上下文数据,作为第一级对象进行传递和保存。闭包广泛用于回调函数、函数式编程中。

原生java没有提供Lambda表达式,不过可以使用尝试使用Scala的Lambda:

例子1:这个是闭包不?

scala> var add = (x: Int) => x +1
scala> add(10)
例子2:

scala> var more = 1
scala> var addMore = (x: Int) => x + more
scala> addMore(10)
五、闭包的问题。

1、让某些对象的生命周期加长。

让自由变量的生命周期变长,延长至回调函数执行完毕。

2、闭包共享。

inal关键字

复制代码

/JavaClosure/src/innerclass/ShareClosure.java
interface Action
{
void Run();
}

public class ShareClosure {

List<Action> list = new ArrayList<Action>();

public void Input()
{
    for(int i=0;i<10;i++)
    {
        final int copy = i;
        list.add(new Action() {    
            @Override
            public void Run() {
                System.out.println(copy);
            }
        });
    }
}

public void Output()
{
    for(Action a : list){a.Run();}
}

public static void main(String[] args) {
    ShareClosure sc = new ShareClosure();
    sc.Input();
    sc.Output();

}

}
复制代码

这个例子创建一个接口列表List ,先向列表中创建 i 个匿名内部类new Action(),然后通过for遍历读出。

因为 i 变量在各个匿名内部类中使用,这里产生了闭包共享,java编译器会强制要求传入匿名内部类中的变量添加final

关键字,所以这里final int copy = i;需要做一个内存拷贝,否则编译不过。(在c#中没有强制要求会导致列有被遍历时

始终会取 i 最大值,这是因为延迟执行引起的)