匿名类型的硬伤:围绕this的成员捕获策略
2011-06-15 17:27 by 老赵, 8165 visits时不时听到一些C#程序员说,希望在C#里出现像Java匿名类一样的特性。以前我也觉得Java里的匿名类是个不错的特性,C#应该吸取进来。不过前段时间我仔细地理解了Java语言规范中关于内部类、匿名类的部分之后,一下子就被恶心到了。恶心过后,我忽然也意识到有些问题的确也是硬伤,也不能指责Java设计者的“品位”。例如,现在我想要谈的关于匿名类中this使用的问题——如果C#没法漂亮地实现这个特性,我宁愿它继续保持现状。
Java匿名类中的this
Java的匿名类特性,在于可以在项目里“内联”地实现一个类型,它可以继承一个现有的具体或抽象类,或是实现接口,并提供完整的成员实现。例如,这里有个抽象类,定义了一个抽象方法:
// Java abstract class MyAbstractClass { String getName() { return "MyAbstractClass"; } abstract void print(); }
然后我们在另一个地方使用一个匿名类,继承这个类:
// Java class MyClass { String getName() { return "MyClass"; } MyAbstractClass someMethod() { return new MyAbstractClass() { public void print() { System.out.println(getName()); } }; } }
好,现在提一个问题,运行下面这行代码会打印出什么结果?
// Java new MyClass().someMethod().print();
输出结果是MyAbstractClass而不是MyClass。换句话说,匿名类型中调用的getName方法是定义在MyAbstractClass里的,而不是定义在词法作用域(Lexical Scope)里的getName方法。根据Java规范,匿名类中的this(包括上面代码中“隐式”的this)表示类型本身对象,而与上下文无关。如果要访问词法作用域里的getName方法(即MyClass的方法),则反而必须显式指定MyClass类,例如:
// Java class MyClass { String getName() { return "MyClass"; } MyAbstractClass someMethod() { return new MyAbstractClass() { public void print() { System.out.println(MyClass.this.getName()); } }; } }
可能会造成的问题
在我看来,Java的这个设计决策很不好,十分容易让人误解代码的意图,但我相信肯定也有人会认为这只是个“品位”区别而已,没有高低。那么现在我们撇开“品位”不谈,谈点这个决策可能会造成的问题吧。例如,程序员A写了一个抽象类:
// Java abstract class MyAbstractClass { abstract void print(); }
程序员B在另一个类的方法中编写了MyAbstractClass的匿名子类:
// Java MyAbstractClass someMethod() { final String name = "MyClass"; return new MyAbstractClass() { public void print() { System.out.println(name); } }; }
很显然,print方法会打印出name变量的值MyClass。相安无事多日,忽然某一天,程序员A需要为MyAbstractClass添加一些新功能,新增了一个受保护的name字段:
// Java abstract class MyAbstractClass { abstract void print(); // new field protected String name = "MyAbstractClass"; }
于是第二天程序员B惊奇地发现,自己明明没有动过任何一行代码,MyAbstractClass忽然就无法正常工作了。这真让人情何以堪。
Java 8的Lambda表达式
事实上,关于这种“内联”定义函数的写法,我能想到的语言都是采取“词法作用域”,因此我想Java这方面的“特立独行”的确容易让人误会。当然客观来说,Java设计成这样也是无奈之举,因为它过于强调“类型”,匿名类还是一个类,既然是个类便会有自己的成员,既然有成员就应该让内联的函数有办法调用这些成员。与之相对,虽然C#中也可以定义内联的函数,却完全不会有Java的困扰,因为C#中内联的只是“函数”而不是完整的“类型”。
说到,底还是多亏.NET中提供了“委托”这种纯粹的,可以让“函数”独立存在的概念。当时在C# 1.0刚出现时,Sun官方还发布文章,认为“委托”破坏了面向对象的纯粹性,“内部类”完全可以作为委托来使用。现在看来,这中观点无疑是一个笑话。追求纯粹的面向对象与盲目套用设计模式类似,都是舍本逐末的做法,我们追求的是“良好的设计”,“面向对象”只是手段而不是目标。如今C#已经发布近十年了,Java社区也在努力向Java7、Java 8里引入部分C#的特性,例如Lambda表达式。
但是,由于Java中没有“委托”,即便是Lambda表达式依旧无法提供单独函数,还是必须附带一个完整的类型。因此this问题依旧存在,这依然是个硬伤。例如我以前的文章里也提到过Java 7里的SAM类型和Lambda表达式上下文成员的捕获策略。从当时的资料来看,Lambda表达式的策略与匿名类相同,依旧以“匿名类”的成员优先,换句话说Lambda表达式只是匿名类的简单写法而已。不过现在这方面有了些许变化,例如这份幻灯片第18页里提到:Lambda表达式是一个拥有词法作用域的匿名方法(A lambda expression is a lexically scoped anonymous method),它的上下文成员捕获与Java的内部类、匿名类有明显不同。
当然,如果使用匿名类的语法定义一个SAM类型,this相关的策略还是要与以前保持不变。Java和C#这类工业化语言的一个包袱,便是要保证兼容性——包括类库等其他方面。所以我还是一直认为,像Python,Ruby这般“洒脱”的技术平台及社区,的确很难进入企业开发市场。
硬伤
this问题可以说是Java匿名类特性的硬伤。C#如果想要引入这个匿名特性,似乎也完全无法躲开这一点。我并不希望C#引入一个“丑陋”的语言特性,幸好也没有任何迹象表明C#有这方面的打算。有趣的是,F#提供了类似Java匿名类的特性,但完全没有这个问题。为什么呢?一看代码便知:
// Java [<AbstractClass>] type MyAbstractClass() = member this.Name = "MyAbstractClass" abstract member Print: unit -> unit type MyClass() = let name = "Local" member this.Name = "MyClass" member this.MyMethod () = { new MyAbstractClass() with override inner.Print () = printfn "%s" this.Name printfn "%s" inner.Name printfn "%s" name }
在F#中,定义一个类型的成员时,需要指定“该方法中表示自身对象的标识符”,我们可以将标识符取名为this,也可以取名为inner或是任意值。再加上F#中没有“隐式”的this指针存在,一切都是指明的,自然没有任何问题。
JAVA这种特性是来自C++的遗留特性。C++中,内含类如果从其它地方派生,则它也继承了那个地方的符号作用域。
但是C++没有这个问题,因为它根本不支持捕获this指针。如果需要访问外围类,你必须显式声明一个外围类的变量。语言提供的唯一好处,就是无需声明友元关系,它也可以访问外围类的私有成员。
看来,JAVA通过允许捕获外围类this来增强C++,却反而造成了其它问题。