一些关于闭包的废话

继函数形参传值传指针、GC机制的Blog后,貌似这又是说烂的问题?没办法,最近在回顾各种知识,发现有联系的就拿出来写一写,省得日后发霉。如果你现在对闭包还不是很了解,可以阅读一下学习Javascript闭包(Closure)深入理解JavaScript闭包(1)理解 JavaScript 闭包三篇文章。难度依次上升。

JavaScript的闭包

闭包的概念已经比科学发展观都深入人心了,简单说它就是“具有独立作用域并且可以引用外层作用域的对象”。例如C#中的匿名函数和Lambda表达式就是基于的闭包概念,其他的高级语言,类似Java、JavaScript和Python等也都有闭包的应用。当然也不要忘了JavaScript的内存泄露主因之一也是闭包,所以先看JS好了。

还是先从代码入手,下面两段代码是从阮一峰老师的那篇闭包讲解中摘取的最后习题,那么现在提问,两段代码分别alert什么内容?

var bar = "Global";
  var object1 = {
    bar : "local",
    foo : function(){
      return function(){
        return this.bar;
      };
    }
  };
  alert(object1.foo()());

//我是分隔线

  var object2 = {
    bar : "local",
    foo : function(){
          var that = this;
      return function(){
        return that.bar;
      };
    }
  };
  alert(object2.foo()());

答案分别是”Global”和”Local”。alert中的参数是一个调用链,直接用第一次返回的对象(函数对象)执行,所以会有()()这种奇怪的语法。对于两次的调用,我们可以拆解为:

var calleeA1 = object1.foo();
var calleeA2 = calleeA1();
//object2类似,不写了。

既然.运算符后面的内容相同,那么必然是.之前的this指代不同。根据JavaScript中this关键字的特性可知,它表示调用者,也就是方法运行时候的上下文。对于object1而言,第一次.foo()之后,函数foo()所在的上下文和this指代就是window了,那么返回的bar就是全局的;反之object2中that = this则是将object2的this交给function使用。所以返回的是object.bar。简而言之就是闭包所在的区域中值并不是一次绑定,而是随着执行变化的,所以上文中this的指代会有所不同。

这个例子可以用更有趣的方式改编[4]:

function object1(){
    this.foo = function(a){
        alert(a);
        return this.foo;
    };
};
new object1().foo(1)(2)(3)(4);

//我是分隔线

function object2(){
    var that = this;
    this.foo = function(a){
        alert(a);
        return that.foo;
    };
};
new object2().foo(1)(2)(3)(4);

虽然形式不同,但原因是一样的:如果没有动态绑定this,那么它就是window的指代。所以在()()后window.foo = undefine,再()就是失败了。

闭包陷阱

仍然是先看代码,运行一下两段代码,看看区别:

arr = new Array();
for ( var i = 0; i < 10; ++i ) {
    arr.push( (function() { return i; })() );
}
alert( arr[0] + "," + arr[9] );

//我是分隔线

arr2 = new Array();
for ( var i = 0; i < 10; ++i ) {
    arr2.push( function() { return i; } );
}
alert( arr2[0]() + "," + arr2[9]() );

分别输出了

0,9
10,10

闭包造成的晚绑定(lazy binding)让局部作用域获取到的变量是运行时才确定的,所以把function对象加入Array之后,运行时拿到了一直保存着的局部变量i(别忘了闭包产生了变量引用,i不会被析构掉),这个i对于任何arr2中的对象都是等价的。

Java和.Net的闭包

目前Android平台的Java上(或者是所有版本的Java上?),让人很恼火的就是写下面代码的时候:

for ( int i = 0; i < length; i++ ) {
    Button button = new Button( getContext() );
    setButtonListener( button, i );
    button.setOnClickListener( new OnClickListener() {
        @Override
        public void onClick( View v ) {
            setFocused( i );
        }
    } );
}

就会被提示说“i不是final的 blah blah blah”,但很明显在一个循环中我们不可能将自增量设定为final,所以就不得不写成:

for ( int i = 0; i < length; i++ ) {
            Button button = new Button( getContext() );
            setButtonListener( button, i );
        }

//绑定函数
    private void setButtonListener( Button aButton, final int aIndex ) {
        aButton.setOnClickListener( new OnClickListener() {
            @Override
            public void onClick( View v ) {
                setFocused( aIndex );
            }
        } );
    }

也就是说,Java支持匿名类,但如果使用的话,它的参数,包括使用的局部变量,都必须是final的。那么为什么如此设计[2]?在[2]中提到:

The methods in an anonymous class don't really have access to local variables and method parameters.
Rather, when an object of the anonymous class is instantiated,
copies of the final local variables and method parameters referred to by the object's methods are stored as instance variables in the object.
The methods in the object of the anonymous class really access those hidden instance variables.
匿名类中的方法并不能访问局部变量和方法参数。在匿名类的对象实例化时,
final修饰的局部变量和方法参数的副本会被对象方法引用,并且作为实例变量存储在对象中。
匿名类对象的方法实际上访问的是这些隐藏的实例变量。

验证这一点可以查看编译后的*$1.class文件,其中匿名类对象使用的各种变量、参数都变成了private final的,在编译期就决定了取值。但这只是另一种表现方式,至于真正的原因恐怕只有Java语言的设计者和开发者才知道了,简单考虑一下不外乎:

  • 维护对象的可用性、一致性(final),避免出现意外问题
  • 就是不支持闭包,各位感觉如何?

没错,Java 在JDK 7之前实际上不支持闭包特性。Java.net之前做过一个调查[3],在开发者感兴趣但Java不支持的特性中,闭包的票数遥遥领先:

  • Closures 47% (856 votes)

另外一个询问是否希望在JDK 7中见到闭包的调查中,支持的也占到了四成。另外比较有趣的是“不知道什么是闭包”的票数比“不希望支持闭包”的还多。我们虽然可以用某种方式在Java中进行闭包的模拟,但说白了还是必须进行静态绑定的匿名函数,就像上面那段代码中演示的一样。其实对于Java中闭包特性是否应该支持的争论由来已久,比如[3]这么个平和的问题竟然引发了论战,另外还有Java 理论与实践: 闭包之争这样历史悠久的帖子(4年前)从各种专业角度讨论闭包的优劣。希望增加闭包特性的人奇怪这个特性强大好用居家旅行必备,这个问题简直不值得讨论;而反对增加的人主要是觉得Lambda表达式会给“纯洁的”Java增加许多函数式编程特性,语法变得奇形怪状。Sun易手之后,去年Oracle终于扭扭捏捏地在Java 7里面支持了“简单闭包”,也就是用“#”声明的Lambda表达式:

int i = #()(3).();
//也可以写成:
#int() foo = #()(3);
int i = foo.();

不少Java开发者看到后都这样:( ´゚д゚)(゚д゚` )。先不说那个莫名其妙的#,调用方法时候的“.”也显得多余。

对比一下C#中的Lambda表达式:

Func<int> foo = () => 3;
int i = foo();
//也可以写成
delegate int bar();
bar b = () => 3;
int i = b();

C#中的Func类实际上是一种特殊的delegate,这样无论是匿名方法、delegate、匿名函数、Lambda表达式还是event,都用一根线串起来了。从各种意义上C#都是由Java而生、借鉴了很多优点的语言,但这次Java的“简单闭包”特性更像是匆匆上马,未经过任何雕琢和详细论证。其被批丑陋不仅仅是#这个奇怪的语法,更在于不完整的特性支持。于是婊Java时候就又多了一道利器,连博客园大牛老赵(Jeffrey Zhao)在系列文章Why Java Sucks and C# Rock也提到了相关内容。

老实说我不太喜欢过多的匿名对象出现在代码中,虽然.net和VS强大到连这样的代码运行、调试都没问题,但我实在担心有后来人看完代码后我自己的人身安全。

for ( int i = 0; i < _imgs.Length; i++ ) {
    ImageItem item = _imgs[i];
    item.Image = ImageBank.Instance.Get( item.ImageUrl, ( source ) => {
        Deployment.Current.Dispatcher.BeginInvoke( () => {
            item.Image = source;
        } );
    } );
}

延伸阅读

刚好看到Jeffrey Zhao时隔一年又开始轮Java了:挖坟鞭尸:当年Sun公司的白皮书《About Microsoft “Delegates”》。评论比文章有趣,而且信息量大多了。

一片翻译文,从代码层次对比两种语言的闭包机制:C#和Java的闭包-Jon谈《The Beauty of Closures》

参考资料

  1. 闭包及其在Java中的模拟实现, liuxuan
  2. The Essence of OOP using Java, Anonymous Classes
  3. Which of these excluded-from-Java-7 features were you most interested in?
  4. 超级BT的JS写法。
  5. Java 7的第一类函数:学习闭包的使用

Comments are closed.