一些匿名语言元素
本文将涉及Java语言的匿名类、C#语言的匿名委托和Javascript的匿名函数。由于具体的名称不同,这里统称它们为“匿名语言元素”
1 Java的匿名类
匿名类是Java语言中很重要的特性,从很早开始就得到Java语言的支持了。例如:
publicinterface MyInterface
...
{
void doSomething();
}
final
String msg
=
"
Hello world!
"
;
MyInterface myInter
=
new
MyInterface()
...
{
publicvoid doSomething() ...{
System.out.println(msg);
}
}
;
myInter.doSomething();
变量myInter的类型是匿名的,只是在代码中即时的由接口MyInterface实现而来。如果通过如下语句打印其类型名称,得到的只是一个“类名$1”为样的形式:
System.out.println(myInter.getClass().getName());
注意代码中msg的声明中关键字final是必须的,因为语法规则中要求匿名类中使用的非参数变量必须是final的。在之后的C#代码中,我们可以看到去掉了这种限制会带来什么后果。
Java语言的匿名类特性在事件回调时得到充分应用,例如为界面上一个按钮编写按钮被触发时的响应函数时,只需要添加一个由相应的Listener接口实现来的匿名类,并实现里面的成员函数即可,这样用起来还是相当方便的。
2 C#的匿名委托
C# 1.x语法规则中委托的使用是比较复杂的,必须先声明一个委托类型,再声明一些与这个委托类型兼容的函数,使用之前先把需要的函数通过“+=”操作符或其他一些成员函数附加到一个委托上,然后即可将委托变量当成一个函数来使用。而事实上我们经常会用到一些比较短小的函数作为委托内容,这时再把这些内容都声明为单独的函数进而再附加到委托上,语法上看上去很笨重。C# 2.0提供了匿名委托特性,可以在为委托变量附加函数时即时的声明函数体,而无需编写单独的函数,例如:
对照前面Java的匿名类,不难发现C#的匿名委托中使用的非参数变量msg在外面并没有被声明为const,这就使得在匿名委托函数中可以修改外面变量的值,事实上也确实如此。例如:
3 C#匿名委托与垃圾收集
看到这你也许会问,那么这个匿名委托是在什么时候真正被实现的呢,如果在其内部所使用的变量被垃圾收及器回收了会怎么样?这里我们可以做如下的实验。道德声明这样一个类,它包含了一个“析构函数”(这还是我第一次在C#里使用这种形式的析构函数,纯粹只是为了做实验而用的):
class
C
...
{
public void DoSomething()
...{
Console.WriteLine("Hello world");
}
~C()
...{
Console.WriteLine("finalize");
}
}
然后写如下一段构造匿名委托的函数代码:
MyDelegate mydele
=
GetDele();
//
GC.Collect();
mydele();
注意到这段代码里有两种被注释了,下面就是针对这两处注释进行的实验。首先运行此段代码,得到的结果没有什么异常:
In delegate
finalize
我们使用了这个委托,于是打出了“In delegate”这句话。程序结果时会收集还没收集的变量,于是析构函数里的话也打印出来了。下面去掉“
GC.Collect();”的注释,再次运行,结果变成了:
finalize
In delegate
由于我们手工的调用了垃圾收集器,强制进行了一次垃圾收集,所以类C的实例被收集了。注意这里把构造匿名委托的代码写成一个单独的函数是必要的,否则会因为C的变量与调用GC的代码在同一个函数中,它仍然没有被收集。最后,把“
c.DoSomething();”的注释也去掉,得到的结果为:
In delegate
Hello world
finalize
注意,由于我们在匿名委托中使用了变量c,因此虽然在这之后它超出了作用域,而且我们也调用了垃圾收集器,但它仍然没有被收集,直到程序结束时它才真正的被收集。在这之后我又进行了第四个实验,把使用匿名委托的三句话放在一个单独的函数里:
UseDele();
GC.Collect();
Console.WriteLine(
"
in Main
"
);
那么输出也就变成了:
In delegate
Hello world
finalize
in Main
可见,.Net环境只有当确定一个变量确实不可能再被使用了时,才会去收集它。匿名委托所使用的一个变量虽然超出了作用域,但由于此委托变量本身还有效,所以就还不能收集它。只有这个委托本身也可以收集了,其所使用到的变量才有可能被收集到。这样的机制保证了匿名委托可以正确的工作。
4 Javascript的匿名函数
(本节Javascript代码在IE和Firefox中都通过测试,且结果是相同的)
Javascript也具有一部分“面向对象”的特性,只是一部分。在Javascript中函数可以看作是一个类,可以像声明类实例那样声明一个函数的实例,也可以通过一些手段在其函数上实现继承、多态等特性,这不是本文的重点,可以参看一些相关的文章。在Javascript中声明函数时,既可以使用传统的命名函数,也可以使用匿名的函数,例如:
var
msg
=
"
hello world
"
;
var
myfunc
=
function
()
...
{
document.write(msg);
}
;
myfunc();
这样就将一个匿名函数赋值给了变量myfunc,之后也可以像普通函数那样去使用它。与Java、C#类似的,匿名函数中也可以使用外面的非参数变量,并且,所使用的变量也像C#那样没有final、const或类似的要求。但是,Javascript不同于C#、Java的是,对外面变量的使用有着与另类的表现。考察下面的C#代码和Javascript代码:
C#:
var
myfunc
=
null
;
for
(
var
i
=
0
; i
<
2
; i
++
)
...
{
var msg = "hello world" + i;
if(i == 0)
myfunc = function()
...{
document.write(msg);
};
}
myfunc();
初看,这两段代码功能是相同的,都是先声明一个变量,然后做一个只进行两次的循环,在循环第一次时为所声明的变量赋值一个匿名委托或匿名函数,里面都使用到了循环里的局部变量msg,其值是与循环次数有关的,在循环第二次时除声明一个新的msg变量外什么也不做。最后调用一下刚才赋值的匿名委托或函数。
运行这两段代码,结果却大不相同。C#给出的结果为:
Hello world!0
而Javascript的结果为:
hello world1
另外,类似的程序也可以写出Java版本,其结果与C#的相同。显然,在Javascript中虽然第二次循环时用var声明了一个新的msg变量,但它实际上与前一次循环时是同一份的。实验表明,这种情况下Javascript对变量的使用是“基于名称”的,即只要是在同一个函数中,不论是使用循环还是使用顺序结果,只要前一个同名变量超出了作用域,后一个变量就会使用与前一份相同的内存。这在平常来说没什么问题,毕竟前一份变量已经超出作用域,已经不能再被访问到了。但是在使用匿名函数时就暴露出了其缺陷。
还可以在进行下面这个实验,以证明Javascript对同一作用域下变量的使用是基于名称的:
function
GetFunction()
...
{
return function()
...{
document.write(msg);
}
}
var
myfunc
=
GetFunction();
var
msg
=
"
Hello world
"
;
myfunc();
这段代码也可以正确的输出“Hello world”这个结果。虽然在声明匿名函数时变量msg根本还没被声明。但由于匿名函数所在的作用域是声明msg的作用域的子域,所以在这里msg也算是可见的。如果改为如下形式就不行了:
function GetFunction()
...
{
return function()
...{
document.write(msg);
}
}
function Func2()
...
{
var myfunc = GetFunction();
var msg = "Hello world1";
myfunc();
}
Func2();
这里msg和匿名函数分属两个不同的作用域,这时对匿名函数来说msg是不可见的,因此其结果不正确,也就不难理解了。
Javascript这种基于名称的变量使用给我们直观上带来的感觉就是其匿名函数的实现类似于宏代换,直接把函数体替换到调用函数的位置上.但事实上又不是这么简单,例如,把上面循环的代码稍加修改,"重构"一下,变成如下形式:
var
myfunc
=
null
;
function
SetFunc(msg, i)
...
{
if(i == 0)
myfunc = function()
...{
document.write(msg);
};
}
for
(
var
i
=
0
; i
<
2
; i
++
)
...
{
var msg = "hello world" + i;
SetFunc(msg, i);
}
myfunc();
这段代码功能看似与之前的循环相同,但其实不然,运行一下,就会发现其输出结果由Hello world1变成了Hello world0, 说明把声明匿名函数的部分放在单独的函数里以后,由于其作用域的关系,使得它们所用的变量已经是不同的了,之后对同名变量的声明不会影响到之前的匿名函数的行为.