Why Java Sucks and C# Rocks(补1):Reddit,兼谈C#属性

原文 - Why Java Sucks and C# Rocks(补1):Reddit,兼谈C#属性

最近博客冷清了不少,主要是事情较多,一是.NET交流会,二是工作,三是几篇暂时无法发在博客上的文章。周末在家,发现邮箱里经常收到SlideShare的邮件,说是我的Why Java Suck and C# Rocks幻灯片在推特上很火热。而今天早上我忽然发现,它被人发到Reddit的编程版块了,讨论地颇为热烈。关于讨论内容,您可以亲自阅读一下。最近的讨论也让我想要补充一些关于C#属性的问题。

关于这个话题,在我不断强调讨论的目的之后,在我的博客上还是有许多朋友没有明白我在说什么,热血上涌当即给予反击,这些从后面的评价中便可以看出。不过在Reddit上,当围观人群看到相同的内容,甚至只有幻灯片,没有更多解释说明,但是大家都能理解我的意思,都能意识到我是希望“用Scala代替Java语言”。而即便是没有看到幻灯片的最后,或是对此有不同意见,也都是在用正常的方式提出质疑。这样的差别让我感到很讽刺,有些话我已经说过很多次了,现在只是引一个例证,也就不再重复了。

在Reddit上的讨论中,有人提出C#里的属性和事件其实都只是方法的语法糖,意义不大。最近在一些人的文章里也出现了类似的说法。我在这个系列的中原本并不打算着重分析C#里的“属性”概念,因为它对于“编程思维”的影响可能并不如其他方面来的明显。不过如果真要深究的话,我还是非常看重这个语言特性的(不过其实属性和事件都是.NET特性)。那么现在我也来简单谈一下“属性”,而“事件”则在后面的文章里讨论吧——那篇文章的草稿已经躺了大半个月了。

属性,在C#里以get/set方法对的形式出现,例如:

C#
public class MyClass
{
private int m_count;
public int Count
{
get { return this.m_count; }
set { this.m_count = value; }
}
}

而在Java中的等价代码则是:

Java
public class MyClass
{
private int m_count;
public int getCount() { return this.m_count; }
public void setCount(int value) { this.m_count = value; }
}

在Java中并非没有“属性”的概念,而是将它直接拆成了getXxx和setXxx两个方法而已,而对于一些工具(如IDE)、框架(如序列化框架)来说,它会忽略方法之前的get/set,直接将后面的字符识别为一种“成员字段”,而在.NET对象中,“属性”天生就是一种表示成员字段的标准方案。

在我看来,从使用上来说,属性比分离的方法要方便一些。例如我们要将MyClass对象的Count属性加1,则可以这么写:

C#
c.Count++;

而在Java里则必须这样:

Java
c.setCount(c.getCount() + 1);

当然您可能会觉得这方面的差距并不大。那么我们换一个场景,现在有一个User数组,我们要根据它的年龄进行排序:

// C#
users.Sort(u => u.Age);
// Java
users.sort(#(User u)(u.getAge()));

这里我们暂且忽略Java中古怪的Lambda表达式写法,只关注“属性”和“get方法”之间的区别。在我看来,C#的代码体现了声明式编程的理念,从语义上讲可以清晰地视为“将u按u.Age字段”排序;与之想法,一旦我们只能利用getAge方法时,便又重新回到了“命令式”思维方式上。为什么这么说?因为调用getAge方法,意味着“获取”这个“Age”字段,它代表的是一个“动作”,表明排序时去“获取”这样一个数值,也就是体现了“怎么做(how to do)”,而不像C#代码中体现的是“做什么(what to do)”。

下面一个例子可能更为典型。例如在使用ORM框架,如(N)Hibernate时,我们时常会在复杂的XML配置中迷失方向。后来有了Fluent NHibernate,让我们可以使用“代码”来进行配置,这样无论是从可读性还是可维护性来说都有了质的飞跃。目前在项目中,由于没有使用关系型数据库,于是我便遵循Fluent Interface的理念,设计了这样的API:

Property(c => c.ArticleID).Identity();
Property(c => c.Content).Name("Html");
Property(c => c.Tags);
Property(c => c.Keywords).ChangeWith(c => c.Content).ChangeWith(c => c.Tags);

以上代码是Article对象的映射规则,四行代码表明:

  • 将ArticleID映射为标识符
  • 将Content映射为存储中的Html字段。
  • 将Tags映射至存储,名称不变。
  • 将Keywords字段映射至存储中,并随Content及Tags字段改变。

我写的映射框架支持传统ORM框架的状态跟踪功能,不过还提供了只读字段的“按需改变”。如Keywords,它是只读属性,框架会读取它的值,并将其放入存储内(以便进行查询),但是我们不需要将其写回对象中去。同时,在Contents和Tags改变的时候,框架也会重新获取Keywords的值,并跟新至存储内。利用C#的表达式树特性,我们可以十分轻松愉快地“声明”出这样的规则。在这个规则中,我们利用“属性”来标明字段之间的关系,这可以说是一种DSL。但如果没有属性,一切都是get方法,那么这段代码又会是什么样子呢?

所以,相对C#来说Java语言中很难写出优雅的声明式代码及Fluent NHibernate这样的框架。原因有多种,如Lambda的古怪语法形式是为一、缺少表达式树功能是为二,而不直接支持“属性”这样的概念也是另一个原因吧。

本文为 赵劼 发表在 个人博客 的系列文章之一。