相同中的不同:Java程序员应该停止低看C#
Java和C#的相同之处比不同处要多得多:两种语言都是写在大括号里的,就像C和C++,类型都是静态、强类型和显式形态,两种语言都是基于类的面向对象语言,两者用的都是运行时编译的思路,并且很好的使用了垃圾处理。
所以这篇文章里,我要重点谈谈它们的相同点,以及C#的巧妙之处。
统一类型系统(Unified type system)
在Java中,原始数据类型(byte、int、bool、float、char等)和其他的类不同,它们并不算是面向对象,也不和引用类型共享相同的祖先类,但它们是有自己的包装类的,用来代表自己并且用来插入到对象结构中(例如int使用Integer类),这样做可以提高性能。
在另一边,C#的统一类型系统却都是从一个公用的根类型System.Object类中衍生而来的,即使是原始数据类型。所有的数据都要用到对象方法(ToString、Equal、GetHashCode等),所以你会碰上像3.ToString()这样的表达式,这种把方法混合到后缀,就带来了dsl风格的语句:
TimeSpan workingTime = 7.Hours() + 30.Minutes();
这么做的美妙之处在于当开发者把数据类型当做值来使用时,它们能够和Java的原始类型一样高效,只有在想要把它们当做对象使用时,系统才需要使用boxing/unboxing来分配堆内存。
显式虚方法(Explicit virtual method)
在Java中,默认所有的方法都是虚方法(虽然这些方法可以使用final封装起来而不允许覆盖),而C#则不同,如果想在C#中写一个虚方法,必须先要用virtual关键字显式声明一下。
有几种原因决定了这样的选择,首先是性能上的考虑:虚方法都有一个悬在头上的性能问题,因为它们不是正常的内联,需要通过vtable来进行调用,这种做法并不直接(Sun的JVM可以内联上最经常调用的虚方法)。第二个也是更重要的原因就是版本问题:C#的设计思路是向后兼容,因此不同版本类库中的基类和衍生类是可以进化发展和保持兼容的。例如,C#能够完全支持基类中新加入的成员和衍生类中的成员同名,而不会导致无法预料的错误。最后一点是可读性:开发者的编程意图能够非常明显的读出来。在Java中,如果开发者不写出Override annotation的话,你不会知道他到底是不是想要重写这个方法。
class Meme { public virtual void Spread() {} } class ThreeHundred : Meme { public override void Spread() { Console.Write("This is sparta!"); } } class Dbz: Meme { // Not a method override public void Spread() { Console.Write("It's over nine thousaaannnd!"); } }
真正的泛型(True Generic)
关于泛型,Java和C#显示出语法上的相似性,但真正深入理解之后你会发现这两种语言在泛型处理上的差别很大。
Java的泛型是在编译器中处理的,运行时并不关心泛型类型。Java在编译中使用叫做类型擦除转换的泛型类与方法:所有的泛型类型都被它们的原始版本替换,并且会在客户代码中插入cast和类型检查,生成的字节代码中并不包含任何泛型类型或参数的引用。Java的泛型是让你在语法编写上尝到甜头,但不会让你的应用执行起来更有效。
而C#的泛型并不全是语言上的功能,它是放置在CLR(Common Language Runtime, 相当于JVM)中的。在编译时需要进行泛型类型检查验证,但指定类型会推迟到类装载时生成。代码调用时的泛型是完全编译的,而且可以假设泛型在类型上是安全的,这被称为泛型的具体化。和Java不同,C#不需要插入cast或者类型检查。泛型类型和方法可以通过引用(class、delegate、interface等)和值类型(primitive type、struct、enum等)来创建。
C#中泛型的使用能够带来效率的提高(不需要cast和值类型的boxing/unboxing),还能够提高深层次的安全验证和反映能力。
public void AwesomeGenericMethod(T t) where T : new() { T newInstance = new T (); // Causes a type creation error in Java T[] array = new T [0]; // Causes generic array creation error in Java T defaultValue = default(T); Type type = typeof(T); List list = new List (); } // Generic with same name but a different number of generic type public void AwesomeGenericMethod(T t, U u) where T : new() { }
Oracle的Java平台总架构师Mark Reinhold在Devoxx 2011大会上曾经探讨过给Java添加泛型的具体化问题,但这项功能还没有规划进Java的下一个主要版本中。
告别被检查异常(checked exception)
Java和C#的异常检查工作差不多一样,二者唯一的主要区别是:Java中包含了checked exception这样的异常。在Java里你可以在方法声明中抛出ExceptionType,这样做可以强迫任何调用方法的函数来处理异常。这个想法在纸面上说说很好,但实际使用中却很烦人,而且带来了新问题。
版本问题:在新版本的方法声明中加入一个checked exception会破坏客户代码,就像给一个接口添加方法一样。比如在版本1中你创建了一个foo方法,声明抛出异常A和B,在版本2中你添加了一些新功能,抛出异常D,这就是一个破坏性变化,因为现有的调用程序不能处理这个异常。
扩展性问题:在大规模的应用项目中,相互依赖的工作是非常多的,因此抛出的异常会多的难以统计,开发者经常会绕开掉这个功能,通过抛出泛型异常或者使用空的catch块。
checked exception背后的思路是了不起的,但是尤其在大项目中,它有点太强迫性了。这就是C#为什么不使用checked exception的原因,其他主流语言也一样:留给开发者自己处理。
访问器和修改器
Java的访问器和修改器(getAddress、setAddress、isValid等)使用命名惯例。而在C#中,访问器和修改器是内置的,自身带有属性,不需要再写getter和setter,所有的工作看上去都是直来直去,即使内部并不是这样的机制(许多其他语言也是这样)。
class Meme { // A private backing field is created by the compiler public string CatchPhrase { get; set;} public string URL { get; set;} } Meme meme = new Meme(); meme.CatchPhrase = "Rick roll'd"; meme.URL = "http://www.youtube.com/watch?v=EK2tWVj6lXw"; // Equivalent in Java class Meme { private String catchPhrase; private String url; public String getCatchPhrase() { return catchPhrase; } public void setCatchPhrase(String catchPhrase) { this.catchPhrase = catchPhrase; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } }
当你声明一个属性为自动执行时,编译器会创建一个私有的匿名域,只有这个属性的get和set访问器可以读取。这带来了兼容性,即使是在类的内部这个域也总是通过访问器使用,这看上去干净简练。
C#中有一类访问器是Java中没有的:索引器(indexer),它就像带有参数的get和set。C#中的collection类比如Dictionary(和Java Map相类似)使用indexer。
var keywordsMapping = new Dictionarystring, string>(); keywordsMapping["super"] = "base"; keywordsMapping["boolean"] = "bool"; Console.Write("Java => C# : {0} => {1}", "super", keywordsMapping["super"]);
你可能会说,没问题吧,这不就是写了一个初始化函数吗?
因为经常要创建对象,然后初始化,这些可以用构造器来完成,要不然在创建对象之后你就要调用不同的set方法。
而对象的索引器可以在创建对象时就把值分配给对象的各种可以访问的域或属性,这样就不需要调用构造器了。
Meme leeroy = new Meme { CatchPhrase = "Leeroy Jenkins", URL = "http://www.youtube.com/watch?v=LkCNJRfSZBU" };
在collection类中也可以使用。
Listint> digits = new Listint> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; Dictionarystring, string> keywordsMapping = new Dictionarystring, string>() { {"super", "base"}, {"boolean", "bool"}, {"import", "using"} };
逐字字符串(Verbatim string)
从字符串中把字符分解出来是非常痛苦的工作,尤其是混合着不同含义的正则表达式。C#的逐字字符串允许反斜杠、制表符、引号和换行符作为字符串的一部分,不再需要转义字符。
string pattern = @"d{3}-d{3}-d{4}"; string input = @"Multiline string 325-532-4521"; Regex.IsMatch(input, pattern, RegexOptions.Multiline);
总结
通过本文我想说C#不仅和Java很相像,而且它能够让开发者的生活变得更轻松,能够实在的减轻他们的负担(其他语言也一样),即使这是一只山寨猫,那么它做的也是相当不错。
实际上Java开发者们也做出了相似的回应,有些运行在JVM上的脚本语言例如Groovy就提供了这里说到的大多数功能,但Java本身还略显顽固,没有做出改变。