IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

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

老赵点滴 2011-08-17 13:53:11 累计浏览 2,919 次
本机暂存

    时不时听到一些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
[]
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指针存在,一切都是指明的,自然没有任何问题。

同分类推荐文章

  1. 科技爱好者周刊(第 401 期):如何赚到10亿美元 (2026-06-26 08:05:38)
  2. 如何做决策 - 从 Go 的一个 issue 说起 (2026-06-26 08:00:00)
  3. Seven Player:Windows上播放115网盘视频的增强工具 (2026-06-09 00:06:47)

查看更多 开发者 文章 →

建议继续学习

  1. SmartSprites - 命令行形式的CSS Sprites生成器 (累计阅读 123,895)
  2. Java开发岗位面试题归类汇总 (累计阅读 22,157)
  3. android 开发入门 (累计阅读 19,527)
  4. 我的PHP,Python和Ruby之路 (累计阅读 13,147)
  5. HashMap解决hash冲突的方法 (累计阅读 12,654)
  6. 一个大二学生有关如何成为一名软件工程师的疑问及答复 (累计阅读 9,181)
  7. Java程序员应该知道的10个eclipse调试技巧 (累计阅读 8,012)
  8. 如何让员工忠于公司? (累计阅读 7,940)
  9. Java技术路线 (累计阅读 7,726)
  10. 聊聊ThoughtWorks面试 (累计阅读 7,614)