Hello World
Spiga

匿名类型的硬伤:围绕this的成员捕获策略

2011-06-15 17:27 by 老赵, 7011 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指针存在,一切都是指明的,自然没有任何问题。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

52 条回复

  1. earthengine
    112.213.217.*
    链接

    earthengine 2011-06-15 17:49:55

    JAVA这种特性是来自C++的遗留特性。C++中,内含类如果从其它地方派生,则它也继承了那个地方的符号作用域。

    但是C++没有这个问题,因为它根本不支持捕获this指针。如果需要访问外围类,你必须显式声明一个外围类的变量。语言提供的唯一好处,就是无需声明友元关系,它也可以访问外围类的私有成员。

    看来,JAVA通过允许捕获外围类this来增强C++,却反而造成了其它问题。

  2. 老赵
    admin
    链接

    老赵 2011-06-15 17:53:27

    @earthengine

    你说的C++这个其实是C#里的内部类和Java里的静态内部类。在我看来Java的“内部类”可以算作你这里提到的“增强”,不过我的文章说的倒是“匿名类”。Java这块的确是又复杂又丑陋。泛型呢,则是又复杂,又不好用……

  3. 幸存者
    210.14.75.*
    链接

    幸存者 2011-06-15 17:55:36

    隐式 this 指针是罪魁祸首啊,如果能以某种方式显式的把 this 传给函数就不会出现这个问题了,比如 python。

  4. 老赵
    admin
    链接

    老赵 2011-06-15 18:04:00

    @幸存者

    这哪个语言做不到啊,不就是传个对象嘛……

  5. 幸存者
    210.14.75.*
    链接

    幸存者 2011-06-15 18:25:23

    @老赵

    如果能显示指定this变量(作为参数或者至少改个名称),就不会出现文中的问题了。

  6. 幸存者
    210.14.75.*
    链接

    幸存者 2011-06-15 18:26:06

    C++、Java、C# 都做不到这一点。

  7. horsefaced
    220.250.11.*
    链接

    horsefaced 2011-06-15 18:28:34

    奇怪,近来一直写javascript,结果对上面java的匿名部分居然一点感觉也没有,那结果真是非常的自然啊.不就是这个this不是那个this嘛,后面F#部分看了好久,请问运行结果是不是this.Name为"MyClass"而inner.Name为"MyAbstractClass"?

  8. 老赵
    admin
    链接

    老赵 2011-06-15 18:33:33

    @幸存者

    直接作为参数传进去,有什么做不到的,只是复杂一点而已。

  9. 老赵
    admin
    链接

    老赵 2011-06-15 18:35:04

    @horsefaced

    哪里自然了啊,当然你说这个this的确不是那个this,但JavaScript的this是“不确定”的,而Java的this是确定的。至于F#,太容易理解了嘛,没任何歧义的。

  10. 链接

    幸存者 2011-06-15 18:49:27

    @老赵

    有办法通过参数得到当前实例吗?

    我说的是像这样(伪代码):

    MyAbstractClass someMethod() {
        return new MyAbstractClass() {
            public void print(@self) {
                System.out.println(@self.name);
            }
        };
    }
    

    这样可以默认使用词法作用域的成员,如果需要使用匿名类的成员,就显示地将 "this变量" 作为参数传给方法。

  11. 老赵
    admin
    链接

    老赵 2011-06-15 19:56:15

    @幸存者

    要这样自然是不行的。

  12. 躺着读书
    27.38.153.*
    链接

    躺着读书 2011-06-15 21:54:14

    说实话,一般稍微有点经验的java开发都不会有这个误解的。

    java和c#在细节上是不一样的。比如说一个类声明了some方法,在它的子类也声明了一个some方法(不覆盖)。c#经常搞不清楚调用的是父类还是子类的方法。而java就比较简单,这样的实例方法只能覆盖不能保留2个。

    this肯定是指本类的实例的引用了。感觉用上了父类的实例方法和属性,其实是因为java在构造类型系统的时候从上往下把父类的东西都一路“copy”了下来。所以同名(签名)方法子类必定覆盖父类的。外部类只是一个作用域,和内部类又没有关系。这么想,就容易就想通了。

  13. 老赵
    admin
    链接

    老赵 2011-06-15 23:03:46

    @躺着读书

    要想通自然容易,但是这个思维不自然,和别的语言不一样。而且更无法让人接受的是,就像我文章里说的那样,可能产生问题,连一个warning都没有。更何况在Java 8里的Lambda表达式的this就和匿名类不同(以前的Lambda提案还是相同的),说明词法作用域更为合理,呵呵。

    C#怎么会搞不清楚调用的是父类还是子类。你说的只不过是方法默认virtual与否而已。在C#里,如果子类声明了同名的方法而不是override,那么必须用new,否则就会抛出warning──如果你说warning会被忽略,那么我说你可以选择warning as error。如果你说这个需要额外配置打个钩。那么我就无话可说了……

    事实上我更倾向于C#这种“显式”地override。在Java里,一不小心就默认override了,视Liskov替换原则为无物……

  14. GuangXiN
    119.161.158.*
    链接

    GuangXiN 2011-06-16 11:09:08

    @horsefaced

    回顧了一下自己寫的JS代碼,裡面充斥著各種this和that,頭痛啊

  15. 易水寒
    183.23.244.*
    链接

    易水寒 2011-06-16 11:31:54

    请问老赵, new MyClass().someMethod().print()输出结果为什么不是MyAbstractClass?

  16. 易水寒
    183.23.244.*
    链接

    易水寒 2011-06-16 11:47:38

    Java的匿名类本质上还是普通的类,所以print()的输出并无不正常,老赵不应该将匿名类当成c#的Lambda表达式的等同物

  17. 老赵
    admin
    链接

    老赵 2011-06-16 14:03:17

    @易水寒

    当然跟C#的不同,事实上也跟其他语言不同,我说的是“容易误解”,还有出问题的可能。所以我说这是Java的硬伤,很难设计出美妙的语法。

  18. 易水寒
    183.23.244.*
    链接

    易水寒 2011-06-16 15:11:58

    老赵一直对java有偏见,用c#的眼光看java当然容易误解,关于这个列子,我想任何一个java人都不会误解,更不能用"恶心"这等字眼形容,至于语法美妙不美妙,这是各花各眼的事情,再有c#语法美妙吗?我倒不觉得。

  19. 易水寒
    183.23.244.*
    链接

    易水寒 2011-06-16 15:36:07

    请问老赵,能不能override java的final方法?事实是在java里override final方法是不能编译的,相对于c#的warning,谁更容易出错?

  20. Limin
    202.189.99.*
    链接

    Limin 2011-06-16 19:29:16

    说的很对。要避免你说的这种问题,那么解析getName()和name时候就不能依赖于任何一种顺序。加个关键字closure吧,呵呵。 clusure.name, name, this.name分别指向someMethod.name, MyAbstractClass.name, MyAbstractClass.name

    这样能解决问题么?

  21. mathgl
    203.218.7.*
    链接

    mathgl 2011-06-16 21:34:27

    c#最有用的特征一个是 linq,另外一个是event delegate。后者可以做到比较完整的解耦,而且有助于简化程序。匿名类这类咚咚在c#其实并没有必要存在。

  22. Wuvist
    175.156.200.*
    链接

    Wuvist 2011-06-17 02:11:59

    热衷于动态mixim,动态修改类属性,动态生成类等等等等的py程序员飘过……

    ==============

    语言提供的功能越强大,程序“出错”的可能性也就越大;关键是看程序员本身对于所使用语言了解有多深。

    程序员是否需要更加灵活强大的语言能力?是否需要提高自己水平去掌控这些能力?

    还是需要更加严谨的语言来处处避免自己“犯错”?

    工业开发语言,猜想是希望把语言作为一种工具来限制死程序员,尽可能的避免程序员犯错;最好程序员都是螺丝钉,会转就成了。

    作为程序员,是希望自己成为一个只能转的螺丝钉,还是希望成为会飞的黑客呢?

    =============

    回到文中的“可能会造成的问题”例子,我是直觉是认为这里是程序员A提供的接口变了,那么B编写的基于A接口的代码出现问题,再正常不过;有啥情不能堪的?

    我完全不懂Java,这里请教一下大家,如果程序员A新增的不是protected,而是: private String name = "MyAbstractClass";

    那么程序员B的代码输出是否还会改变?

  23. 老赵
    admin
    链接

    老赵 2011-06-17 10:11:57

    @易水寒: 老赵一直对java有偏见,用c#的眼光看java当然容易误解,关于这个列子,我想任何一个java人都不会误解,更不能用"恶心"这等字眼形容。

    看清楚我的文章,我一直强调的是“跟其他语言不同”,没说C#。如果Java的合理,那么何必Java 8却改变这点,呵呵。你可以觉得某个特性美妙,这是品味,我可以尊重你的品味——所以我后面举例说明这种策略会造成的问题么,这跟品味无关。

  24. 老赵
    admin
    链接

    老赵 2011-06-17 10:15:52

    @易水寒: 请问老赵,能不能override java的final方法?事实是在java里override final方法是不能编译的,相对于c#的warning,谁更容易出错?

    说了你可以warning as error,又被你无视了?

  25. 老赵
    admin
    链接

    老赵 2011-06-17 10:18:12

    @Wuvist: 我是直觉是认为这里是程序员A提供的接口变了,那么B编写的基于A接口的代码出现问题,再正常不过;有啥情不能堪的?

    旧有成员都不变,增加一个新的成员完全不属于破坏性变更。如果增加成员都不允许的话,类库框架还怎么升级?

    语言提供的功能越强大,程序“出错”的可能性也就越大;关键是看程序员本身对于所使用语言了解有多深。程序员是否需要更加灵活强大的语言能力?是否需要提高自己水平去掌控这些能力?还是需要更加严谨的语言来处处避免自己“犯错”?

    功能强大和出错可能性没有必然联系。我也可以说,调查发现不论用什么语言每百行代码的bug数量是差不多的,所以表达能力越差的语言,实现相同功能代码量越多,越容易出错。还有,Java这种不叫做严谨,而是死板。C#也很严谨,Haskell,F#,Scala都很严谨,只不过它们灵活。

  26. LiaWind
    203.208.61.*
    链接

    LiaWind 2011-06-17 11:25:43

    关键问题还是出在”隐含的this指针“上吧,即 name 其实 this.name。 好看难看且不论,只说语义。 F#,javascript都没有隐含this指针,name就是词法作用域上的 name,this.name 才是 this 的成员 name,因此没问题。 而 java 有隐含的this指针,name 既可能是 this.name,也可能是 MyClass.this.name,于是就悲剧了。 我想有一种解决方案就是碰到这种既有 this.name 也有 MyClass.this.name 编译器直接报错,禁止直接使用 name 而显式使用上面两种方式之一。

  27. 易水寒
    119.128.59.*
    链接

    易水寒 2011-06-17 18:46:56

    @老赵:说了你可以warning as error,又被你无视了?

    c#的warning as error就是严谨,java的final method 就不严谨、死板、恶心?这是什么道理?

  28. 老赵
    admin
    链接

    老赵 2011-06-17 18:52:53

    @易水寒

    我说Java的final method不严谨了么?我根本没有评价过virtual by default的好坏,我只是“倾向于”not virtual by default而已。我说Java不严谨了么?我只是说C#,Haskell,F#,Scala也都很严谨。至于死板、恶心的地方,参考这篇文章及之前的文章。

    总之一句话:有没有逻辑?来吵架的不奉陪。

  29. fuliang
    218.249.58.*
    链接

    fuliang 2011-06-18 20:44:25

    我和老赵的观点截然相反。既然匿名内部类是一个类型,它的this当然是表示它本身的了,如果它反而表示外部类的this了,这让这个类情何以堪?我怎么表示这个类的this?难道要MyAbstractClass.this了。。。

    但是这个思维不自然,和别的语言不一样。

    请老赵举一个不是显示传递this/self,更自然的和Java不一样的语言。

    老赵拿Java 8 Lambda表达式的this似乎要佐证匿名内部类的这个设计是不合理不自然的,但是这个证据毫无道理可言。Lambda表达式是一个表达式,不是一个类型,匿名类是一个类型,他们的this设计当然会不一样。

  30. 柏林
    123.138.30.*
    链接

    柏林 2011-06-19 11:28:29

    老赵总是喜欢拿自己认为Java不好,“恶心”的地方说事,如果可以的话,可以分析一些你觉得Java做的不错的地方么。另外, 你这篇文章说的问题和你举的例子都感觉不具有代表性。我觉得一个稍微有一定经验的javaer不会犯这样的错误。当然,咱不是吵架来的。:)

  31. jason
    117.25.191.*
    链接

    jason 2011-06-19 20:10:25

    哈哈,看评论就是一个享受,不过也学到不少东西.

  32. 老赵
    admin
    链接

    老赵 2011-06-20 11:42:41

    @柏林

    用现在的眼光看来,Java已经没啥设计的不错的地方了。我这篇文章说的东西当然可以被“有经验的程序员”避免,有经验的程序员当然可以避免各种错误。

  33. 老赵
    admin
    链接

    老赵 2011-06-20 11:44:51

    @fuliang

    看看我文章怎么说的吧:

    恶心过后,我忽然也意识到有些问题的确也是硬伤,也不能指责Java设计者的“品位”。

    所以Java这方面的确没法设计地不恶心。Lambda表达式构造的就是一个类型,只不过逼不得已弱化了这个类型而已(这样才能稍微不恶心一些)。Java一直说C#的Delegate破坏了对象的纯洁性,现在么……

  34. mathgl
    203.218.7.*
    链接

    mathgl 2011-06-20 20:30:55

    @老赵: 看看我文章怎么说的吧:恶心过后,我忽然也意识到有些问题的确也是硬伤,也不能指责Java设计者的“品位”。所以Java这方面的确没法设计地不恶心。Lambda表达式构造的就是一个类型,只不过逼不得已弱化了这个类型而已(这样才能稍微不恶心一些)。Java一直说C#的Delegate破坏了对象的纯洁性,现在么……

    pure oo 是罪恶的根源呵....

  35. 四有青年
    210.13.101.*
    链接

    四有青年 2011-06-21 17:54:27

    感谢楼主,以前还真没遇到或注意到。可以补充到这篇文章里

    http://www.cnblogs.com/Bruce-Su/archive/2011/06/03/2018610.html

  36. @waynebaby
    112.64.20.*
    链接

    @waynebaby 2011-06-22 03:04:22

    俺们完全不在乎匿名类继承 我们哭着闹着的是要匿名类接口实现 根本不会碰到这里的硬伤

  37. 链接

    Ivony 2011-06-22 11:09:07

    请问老赵,能不能override java的final方法?事实是在java里override final方法是不能编译的,相对于c#的warning,谁更容易出错?

    麻烦先了解下C#,如果不是virtual的方法,直接override也是编译错误。简单的说Java就是强制virtual,自动override。Java显然是更容易出错的,否则也不用弄个劳什子@override出来了。

  38. ouxingzhi
    211.144.121.*
    链接

    ouxingzhi 2011-06-27 09:55:09

    我觉得吧,类在构造时,this自然要指向当前的类,指向外部上层的类那就不正常了。 至少在javascript 中是这样的

  39. Nana's Lich
    202.96.86.*
    链接

    Nana's Lich 2011-06-28 12:09:49

    不喜欢 Java 的很多特征,不过我觉得老赵这次措辞的确很不妥。

  40. 老赵
    admin
    链接

    老赵 2011-06-28 16:06:34

    @Nana's Lich

    这就叫不妥?估计你没看我其他文章,哈哈。

  41. LEAPER
    124.89.10.*
    链接

    LEAPER 2011-07-03 10:30:05

    利令智昏。偏见也令智昏。 :)

  42. kvspas
    221.9.247.*
    链接

    kvspas 2011-07-10 06:10:50

    老赵,把上面那段getname()的例子放到j#中运行,或许结果会很有意思。 另反映个情况,我本想登陆后留言,但是两个登陆链接点击了都没有用,页面没有动静发生。iOS 4.3.3.

  43. 老赵
    admin
    链接

    老赵 2011-07-10 13:05:29

    @kvspas

    J#肯定还是Java啊。

    这个登录是需要open一个小窗口的好像……下次在iPad和Android上试试看。

  44. 黎叔
    113.97.48.*
    链接

    黎叔 2011-07-29 10:02:13

    匿名内部类用起来很爽啊,特别是只在一个地方用的时候。

  45. earthengine
    124.176.224.*
    链接

    earthengine 2012-12-01 22:00:10

    类添加一个公有字段是不是破坏性变更?

    有可能。万一客户原有继承中有一个字段与之同名的话。

    同理,添加一个保护字段也有可能是破坏性变更。

    只有添加一个私有字段才能基本上保证不是。即便如此,如果客户依赖于结构体大小或者反射得到的字段数量的话。。。

    你的例子只是说明了,添加一个保护字段如果刚巧与客户某处作用域内的某个名字同名的话,客户代码会被破坏。

  46. 老赵
    admin
    链接

    老赵 2012-12-01 23:03:07

    @earthengine

    公有字段和保护字段不是破坏性变更,至少在.NET中不是,.NET里没有override关键字那就是new。

    反射等兼容性是不考虑的。

  47. Inshua
    113.205.159.*
    链接

    Inshua 2013-07-03 15:53:29

    委托非常不方便。委托的闭包只能到函数一层。而匿名内部类可以将多个函数作为一个 bundle,回调时可以用 this 互相访问,这个 bunlde 里的函数能共享一组状态。

    对比匿名内部类,委托的抽象级别比较弱。属于匿名内部类的特例。当匿名内部类退化到只有一个函数,且自身无状态时,就成了委托。

    你所说的这个语法偏见,不是什么大问题,只要注意是可以避免的。相比来说,c#的委托问题更严重,由于没有 final,c# 的委托还有回调时内存被释放导致出错的情况。

  48. 老赵
    admin
    链接

    老赵 2013-07-04 15:57:01

    @Inshua

    真是瞎扯啊,你是不是完全不懂C#?

  49. xiuxianren
    27.38.22.*
    链接

    xiuxianren 2013-08-12 19:44:54

    任何匿名特性必然伴随潜在的命名冲突。使用全名可以避免冲突,但是书写不便。为简化书写,我们将作用域视作匿名的命名空间。

    传统的作用域嵌套是完全的,因此简单直白: 作用域A: { int x = 0; // x是作用域A.x 作用域B: { int x =1; // x是作用域B.x,如果不定义这一行,则x是作用域A.x print(x); } } 不会带来任何书写和理解上的混乱,因为作用域1是完全定义在作用域0之中。

    匿名类的作用域继承自外部父作用域,不是完全定义于作用域A中,而是“插入”在作用域A中。 作用域A: { int x = 0; 作用域B extend from 作用域C: { print(x); // x可能是作用域A.x,也可能是作用域C.x。 } // 作用域B实际继承了作用域A以及作用域C。 } 要理解此处的x,光有这部分代码是不够的,还需要拥有作用域C的知识,才可以断定print(x)中的x引用自哪个作用域。父作用域C本身又可能有继承,这使问题更加复杂。这种复杂度的根源在于作用域由单继承变成了多继承,且继承优先级大于嵌套优先级。这种复杂性的后果是损害了软件的模块性:一个模块的规格扩展带来众多模块的关联修改风险,最要命的是这种关联是隐性的。

    通过语法或者编码规范强制在上述场景中使用全名可以避免命名冲突。不过JAVA对于外部作用域的本地变量无法使用全名(其他语言不清楚),因此无法正面解决问题。但通过编码规范(或者设计规约)以其他方式来迂回绕过上述问题是可能的,例如不允许直接访问字段,必须通过方法,字段不能定义成公共等,这样减少绑定x的可能性,x要么是嵌套作用域树中的一个变量,要么是某个类的成员,然后通过类限定符可以明确的绑定x。

    或者通过反转优先级,嵌套优先,也可以消除模块之间的隐性关联。但是这么做会带来以下问题:正常情况下在Java中作用域绑定优先顺序由近及远为块,函数、类,如果在内部类情况下变更为块,函数,嵌套域,类,这损害了Java语言规范的简单性。

    总的来看,不同的方法都有对应的优劣以及相应的实践作为替代解决方案,但嵌套优先带来更自然和精确的理解性,从而有利于软件的模块化,无疑是值得应用的。考虑到各种历史,现在的Java回避了语言规范问题,而以新的语法设施来提供补偿,是需要被理解的。

  50. kkk
    61.144.110.*
    链接

    kkk 2015-06-15 13:37:25

    Class A
    {
    static String name = "A";

    Class B : Base
    {
    public void Print()
    {
    Console.Write(name);
    }
    }
    }

    上述代码是C#中可以出现的,当Base类未来被加了一个name后,是否也存在老赵指出的“可能造成的问题”?其实只要有内部类概念,都会有这种问题存在。

已自动隐藏某些不合适的评论内容(主题无关,争吵谩骂,装疯卖傻等等),如需阅读,请准备好眼药水并点此登陆后查看(如登陆后仍无法浏览请留言告知)。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我