A Comparative Overview of C#中文版
作者:Ben Albahari
公司:Genamics
日期:2000年7月31日初版,2000年8月10日修订。
感谢以下人士支持和反馈(按字母先后顺序):Don Box、 C.R. Manning、 Joe Nalewabau、 John Osborn、 Thomas Rhode & Daryl Richter。
译者:荣耀
【译序:C#入门经典!希望文中针对新手的译注不会影响阅读的流畅性。译文中所有程序调试环境均为Microsoft Visual Studio.NET 7.0 Beta2和 Microsoft .NET Framework SDK Beta2。代码就是文章,请仔细阅读代码J】
10.操作符重载
利用操作符重载机制,程序员可以创建让人感觉自然的好似简单类型(如int、long等等)的类。C#实现了一个C++操作符重载的限制版,它可以使诸如这样的精辟的例子—复数类操作符重载表现良好。
在C#中,操作符==是对象类的非虚的(操作符不可以为虚的)方法,它是按引用比较的。当你构建一个类时,你可以定义你自己的==操作符。如果你在集合中使用你的类,你应该实现IComparable接口。这个接口有一个叫CompareTo(object)方法,如果“this”大于、小于或等于这个object,它应该相应返回正数、负数或0。如果你希望用户能够用优雅的语法使用你的类,你可以选择定义<、<=、>=、>方法。数值类型(int、long等等)实现了IComparable接口。
下面是一个如何处理等于和比较操作的简单例子:
public class Score : IComparable
{
int value;
public Score (int score)
{
value = score;
}
public static bool operator == (Score x, Score y)
{
return x.value == y.value;
}
public static bool operator != (Score x, Score y)
{
return x.value != y.value;
}
public int CompareTo (object o)
{
return value - ((Score)o).value;
}
}
Score a = new Score (5);
Score b = new Score (5);
Object c = a;
Object d = b;
按引用比较a和b:
System.Console.WriteLine ((object)a == (object)b; // 结果为false
【译注:上句代码应该为:System.Console.WriteLine ((object)a == (object)b); // 结果为false】
比较a和b的值:
System.Console.WriteLine (a == b); // 结果为true
按引用比较c和d:
System.Console.WriteLine (c == d); // 结果为false
比较c和d的值:
System.Console.WriteLine (((IComparable)c).CompareTo (d) == 0); // 结果为true
你还可以向Score类添加<、<=、>=、>操作符。C#在编译期保证逻辑上要成对出现的操作符(!=和==、>和<、>=和<=)必须一起被定义。
11.多态
面向对象的语言使用虚方法表达多态。这就意味着派生类可以有和父类具有同样签名的方法,并且父类可以调用派生类的方法【译注:此处应该是对象(或对象引用、指向对象的指针)】。在Java中,缺省情况下方法就是虚的。在C#中,必须使用virtual关键字才能使方法被父类调用。
在C#中,还需要override关键字以指明一个方法将重载(或实现一个抽象方法)其父类的方法。
Class B //【译注:应为class B】
{
public virtual void foo () {}
}
Class D : B //【译注:应为class D : B】
{
public override void foo () {}
}
试图重载一个非虚的方法将会导致一个编译时错误,除非对该方法加上“new”关键字,以指明该方法意欲隐藏父类的方法。
Class N : D //【译注:应为class N : D】
{
public new void foo () {}
}
N n = new N ();
n.foo(); // 调用N的foo
((D)n).foo(); // 调用D的foo
((B)n).foo(); // 调用D的foo
和C++、Java相比,C#的override关键字使得阅读源代码时可以清晰地看出哪些方法是重载的。不过,使用虚方法有利有弊。第一个有利点是:避免使用虚方法轻微的提高了执行速度。第二点是可以清楚地知道哪些方法会被重载。【译注:从“不过”至此,这几句话显然不合逻辑,但原文就是如此:“However, requiring the use of the virtual method has its pros and cons. The first pro is that is the slightly increased execution speed from avoiding virtual methods. The second pro is to make clear what methods are intended to be overridden.”。我认为,若将被我标为斜体的method改为keyword的话,逻辑上会顺畅些。这样,第一句话就可认为是和Java比,因其方法缺省是虚的,第二句话主要是和C++比】。然而,利也可能是弊。和Java中缺省忽略final修饰符【译注:在Java中可利用final关键字,对方法上锁,相当于C#/C++中没有用virtual关键字修饰方法/成员函数的情况】以及C++中缺省忽略virtual修饰符相比,Java中缺省选项【译注:即虚的】使得你程序略微损失一些效率,而在C++中,它可能妨碍了扩展性,虽然这对基类的实现者来说,是不可预料的。
12.接口
C#中的接口和Java中的接口差不多,但是有更大的弹性。类可以随意地显式实现某个接口:
public interface ITeller
{
void Next ();
}
public interface IIterator
{
void Next ();
}
public class Clark : ITeller, IIterator
{
void ITeller.Next () {}
void IIterator.Next () {}
}
这给实现接口的类带来了两个好处。其一,一个类可以实现若干接口而不必担心命名冲突问题。其二,如果某方法对一般用户来说没有用的话,类能够隐藏该方法。显式实现的方法的调用,需把类【译注:应该是对象】造型转换为接口:
Clark clark = new Clark();
((ITeller)clark).Next();
13.版本处理
解决版本问题已成为.NET框架一个主要考虑。这些考虑的大多数都体现于组合体中。在C#中,可在同一个进程里运行同一个组合体的不同版本的能力是令人印象深刻的。
当代码的新版本(尤其是.NET库)被创建时,C#可以防止软件失败。C#语言参考里详细地描述了该问题。我用一个例子简明扼要地讲解如下:
在Java中,假定我们部署一个称为D的类,它是从一个通过VM发布的叫B的类派生下来的。类D有一个叫foo的方法,而它在B发布时,B还没有这个方法。后来,对类B做了个升级,现在B包括了一个叫foo的方法,新的VM现在安装在使用类D的机器上了。现在,使用D的软件可能会发生故障了,因为类B的新实现可能会导致一个对D的虚函数调用,这就执行了一个类B始料未及的动作。【译注:因Java中方法缺省是虚的】在C#中,类D的foo方法应该声明为不用override修饰符的(这个真正表达了程序员的意愿),因此,运行时知道让类D的foo方法隐藏类B的foo方法,而不是重载它。
引用C#参考手册的一句有意思的话“C#处理版本问题是通过需要开发人员明确他们的意图来实现的”。尽管使用override是一个表达意图的办法,但编译器也能自动生成—通过在编译时检查方法是否在执行(而不是声明)一个重载。这就意味着,你仍然能够拥有象Java一样的语言(Java不用virtual和override关键字),并且仍然能够正确处理版本问题。
参见字段修饰符部分。
14.参数修饰符
(1)ref参数修饰符
C#(和Java相比)可以让你按引用传递参数。描述这一点的最明显的例子是通用交换方法。不象C++,不但是声明时,调用时也要加上ref指示符:【译注:不要误会这句话,C++中当然是没有ref关键字】
public class Test
{
public static void Main ()
{
int a = 1;
int b = 2;
swap (ref a, ref b);
}
public static void swap (ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
}
(2)out参数修饰符
out关键字是对ref参数修饰符的自然补充。Ref修饰符要求参数在传入方法之前必须被赋值。而out修饰符则明确当方法返回时需显式给参数赋值,。
(3)params参数修饰符
params修饰符可被加在方法的最后的参数上,方法将接受任意数量的指定类型的参数【译注:在一个方法声明中,只允许一个params性质的参数】。例如:
public class Test
{
public static void Main ()
{
Console.WriteLine (add (1, 2, 3, 4).ToString());
}
public static int add (params int[] array)
{
int sum = 0;
foreach (int i in array)
sum += i;
return sum;
}
}
【作者注:学习Java时一个非常令人诧异的事是发现Java不能按引用传递参数,尽管不久以后,你很少会再想要这个功能,并且写代码时也不需要它了。当我第一次阅读C#规范的时候,我常想,“他们干吗把加上这个功能,没有它我也能写代码”。经过反省以后,我意识到这其实并不是说明某些功能是否有用的问题,更多是说明了没有它你就另需别的条件才能实现的问题。
当考虑到C++是怎么做的时候,Java是干了件好事,它简化了参数如何传递的问题。在C++中,方法【译注:C++中没有方法一说,应该称为“函数”或“成员函数”】的参数和方法调用通过传值、引用、指针【译注:例如int、int*、int&】,使得代码变得不必要的复杂。C#显式传递引用,不管是方法声明时还是调用时。它大大地减少了混乱【译注:这句话应该这么理解:由于C++的语法问题,有时你并不知道你是在使用一个对象还是一个对象引用,本节后有示例】,并达到了和Java同样的目标,但是C#的方式更有表达力。显然这是C#的主旨—它不把程序员圈在一个圈里,使他们必须绕一个大弯子才能做成某件事。还记得Java吗?Java指南里,建议如何解决传引用的问题,你应该传递一个1个元素的数组去保存你的值,或另做一个类以保存这个值。
【译注:
#include "stdafx.h"
class ParentCls
{
public:
virtual void f(){printf("ParentCls\t");}
};
class ChildCls : public ParentCls
{
public:
virtual void f(){printf("ChildCls\t");}
};
void Test1(ParentCls pc) {pc.f();}
void Test2(ParentCls& pc) {pc.f();}
int main(int argc, char* argv[])
{
ChildCls cc;
Test1(cc);//输出ParentCls
Test2(cc);//输出ChildCls
//只看调用处,是不知道你使用的引用还是对象的,但运行结果迥异!
return 0;
}
】
15.特性
C#和Java的编译代码里都包括类似于字段访问级别的信息。C#扩展了这个能力,对类中的任何元素,比如类、方法、字段甚至是独立参数,你都可以编译自定义的信息,并可以于运行时获取这些信息。这儿有一个非常简单的使用特性的类的例子:
[AuthorAttribute ("Ben Albahari")]
class A
{
[Localizable(true)]
public String Text //【译注:应为public string Text或public System.String Text,如果前面没有using System的话】
{
get {return text;}
//...
}
}
Java使用一对/** */和@标签注释以包含类和方法的附加信息,但这些信息(除了@deprecated【译注:Java1.1版本及以后】)并未build到字节码中。C#使用预定义的特性Obsolete特性,编译器可以警告你,排除废代码(就象@deprecated),并用Conditional特性使得可以条件编译。微软新的XML库使用特性来表达字段如何序列化到XML中,这就意味着你可以很容易地把一个类序列化到XML中,并可以再次重建它。另外一个对特性的恰当的应用是创建真正有威力的类浏览工具。C#语言规范详尽第解释了怎样创建和使用特性。
16.switch语句
C#中的switch语句可以使用整型、字符、枚举或(不象C++或Java)字符串。在Java和C++中,如果你在任何一个case语句里忽略了一个break语句,你就有其它case语句被执行的危险。我想不通为什么这个很少需要的并容易出错的行为在Java和C++中都成了缺省行为,我也很高兴地看到C#不会是这个样子。
【译注: 因为C#不支持从一个case标签贯穿到另一个case标签。如果需要的话,可以使用goto case或goto default实现】
17.预定义类型
C#基本类型基本上和Java的差不多,除了前者还加入了无符号的类型。C#中有sbyte、byte、short、ushort、int、uint、long、ulong、char、float和double。唯一令人感到惊奇的地方是这儿有一个16个字节【译注:原文误写为12个字节】的浮点型数值类型decimal,它可以充分利用最新的处理器。
【译注:补充一下,尽管decimal占用128位,但它的取值范围比float(32位)、Double(64位)远远小得多,但它的精度比后二者的要高得多,可以满足精度要求极高的财务计算等】
18.字段修饰符
C#中字段修饰符基本上Java相同。为了表示不可被修改的字段,C#使用const和readonly修饰符。const字段修饰符就象Java的final字段修饰符,该字段的实际值被编译成IL代码的一部分。只读字段在运行时计算值。对标准C#库来说,这就可以在不会破坏你的已经部署的代码的前提下升级。
19.跳转语句
这儿没有更多的令人惊讶的地方,可能除了臭名卓著的goto语句。然而,这和我们记得的带来麻烦的20年前的basic的goto语句大不相同。一个goto语句必须指向一个标签【译注:goto语句必须必须在该标签的作用域内,或者换句话说,只允许使用goto语句将控制权传递出一个嵌套的作用域,而不能将控制权传递进一个嵌套域】或是switch语句里的一个选择支【译注:即所谓的goto case语句】。指向标签的用法和continue差不多。Java里的标签,自由度大一些【译注:Java中的break和continue语句后可跟标签】。C#中,goto语句可以指向其作用域的任意一个地方,这个作用域是指同一个方法或finally程序块【译注:如果goto语句出现在finally语句块内,则goto语句的目的地也必须在同一个finally语句块内】。C#中的continue语句和Java中的基本等价,但C#中不可以指向一个标签。
【译注:Java把goto作为保留字,但并未实现它】
20.组合体、名字空间和访问级别
在C#中,你可以把你源代码中的组件(类、结构、委托、枚举等)组织到文件、名字空间和组合体中。
名字空间不过是长类名的语法上的甜言蜜语而已。例如,用不着这么写Genamics.WinForms.Grid,你可以如此声明类Grid并将其包裹起来:
namespace Genamics.WinForms
{
public class Grid
{
//....
}
}
对于使用Grid的类,你可以用using关键字导入【译注:即using Genamics.WinForms】,而不必用其完整类名Genamics.WinForms.Grid。
组合体是从项目文件编译出来的exe或dll。.NET运行时使用可配置的特性和版本法则,把它们创建到组合体,这大大简化了部署—不需要写注册表,只要把组合体拷到相关目录中去即可。组合体还可以形成一个类型边界,从而解决类名冲突问题。同一组合体的多个版本可以共存于同一进程。每一个文件都可以包含多个类、多个名字空间。一个名字空间可以横跨若干个组合体。如此以来,系统将可获得更大的自由度。
C#中有五种访问级别:private、internal、protected、internal protected和public【译注:internal protected当然也可以是protected internal,此外再无其它组合】。private和public和Java中意思一样。C#中,没有标明访问级别的就是private,而不是包范围的。internal访问被局限在组合体中而不是名字空间(这和Java更相似)中。Internal protected等价于Java的protected。protected等价于Java的private protected,而它已被Java废弃。
21.指针运算
在C#中,指针运算可以被使用在被标为unsafe修饰符的方法里。当指针指向一个可被垃圾收集的对象的时候,编译器强迫使用fixed关键字去固定对象。这是因为垃圾收集器是靠移动对象来回收内存的。但是如果当你使用原始指针时,它所指的对象被移动了,那你的指针将指向垃圾。我认为这儿用unsafe这个关键字是个好的选择—它不鼓励开发人员使用指针除非他们真的想这么做。
22.多维数组
C#可以创建交错数组【译注:交错数组是元素为数组的数组。交错数组元素的维度和大小可以不同】和多维数组。交错数组和Java的数组非常类似。多维数组使得可以更有效、更准确地表达特定问题。以下是这种数组的一个例子:
int [,,] array = new int [3, 4, 5]; // 创建一个数组
int [1,1,1] = 5;//【译注:此行代码有误:应为array[1,1,1] = 5;】
使用交错数组:
int [][][] array = new int [3][4][5]; // 【译注:此行代码有误,应为:int [][][] array = new int[3][][];】
int [1][1][1] = 5; 【译注:此行代码有误:应为array[1][1][1] = 5;】【译注:小心使用交错数组】
若和结构联合使用,C#提供的高效率使得数组成为图形和数学领域的一个好的选择。
23.构造器和析构器
你可以指定可选的构造器参数:
class Test
{
public Test () : this (0, null) {}
public Test (int x, object o) {}
}
你也可以指定静态构造器:
class Test
{
static int[] ascendingArray = new int [100];
static Test ()
{
for (int i = 0; i < ascendingArray.Length; i++)
ascendingArray [i] = i;
}
}
析构器的命名采用C++的命名约定,使用~符号。析构器只能应用于引用类型,值类型不可以,并且不可被重载。析构器不可被显式调用,这是因为对象的生命期被垃圾收集器所管制。在对象所占用的内存被回收前,对象继承层次里的每一个析构器都会被调用。
尽管和C++的命名相似,C#中的析构器更象Java中的finalize方法。这是因为它们都是被垃圾收集器调用而不是显式地被程序员调用。而且,就象Java的finalize,它们不能保证在各种情况下都肯定被调用(这常常使第一次发现这一点的每一个人都感到震惊)。如果你已习惯于采用确定性的析构编程模式(你知道什么时候对象的析构器被调用),当你转移到Java或C#时,你必须适应这个不同的编程模型。微软推荐的和实现的、贯穿于整个.NET框架的是dipose模式。你要为那些需要管理的外部资源(如图形句柄或数据库连接)的类定义一个dispose()方法。对于分布式编程,.NET框架提供一个约定的基本模型,以改进DCOM的引用计数问题。
24. 受控执行环境
对[C#/IL码/CLR]和[Java/字节码/JVM]进行比较是不可避免的也是正当的。我想,最好的办法是首先搞清楚为什么会创造出这些技术来。
用C和C++写程序,一般是把源代码编译成汇编语言代码,它只能运行在特定的处理器和特定的操作系统上。编译器需要知道目标处理器,因为不同的处理器指令集不同。编译器也要知道目标操作系统,因为不同的操作系统对诸如如何执行工作以及怎样实现象内存分配这些基本的C/C++的概念不同。C/C++这种模型获得了巨大的成功(你所使用的大多数软件可能都是这样编译的),但也有其局限性:
l 程序无丰富的接口以和其它程序进行交互(微软的COM就是为了克服这个限制而创建的)
l 程序不能以跨平台的形式分发
l 不能把程序限制执行在一个安全操作的沙箱里
为了解决这些问题,Java采用了Smalltalk采用过的方式,即编译成字节码,运行在虚拟机里。在被编译前,字节码维持程序的基本结构。这就使得Java程序和其它程序进行各种交互成为可能。字节码也是机器中立的,这也意味着同样的class文件可以运行于不同的平台。最后,Java语言没有显式的内存操作(通过指针)的事实使得它很适合于编写“沙箱程序”。
最初的虚拟机利用解释器来把字节码指令流转换为机器码。但是这个过程慢得可怕以致于对于那些关注性能的程序员来说,从来都没有吸引力。如今,绝大多数JVM都利用JIT编译器,基本编译成机器码—在进入类框架的范围之前和方法体执行之前。在它运行前,还有可能将Java程序转换为汇编语言,可以避免启动时间和即时编译的内存负担。和编译Visual C++程序相比,这个过程并不需要移去程序对运行时的依赖。Java运行时(这个术语隐藏在术语Java虚拟机下之下)将处理程序执行的很多至关重要的方面,比如垃圾收集和安全管理。运行时也被认为是受控执行环境。
尽管术语有点含糊不清,尽管从不用解释器,但.NET基本模型也是使用如上所述方式。.NET的重要的改进将来自于IL自身的设计的改进。Java可以匹敌的唯一方式是修改字节码规范以达到严格的兼容。我不想讨论这些改进的细节,这应该留给那些极个别的既了解字节码也了解IL码的开发人员去讨论。99%的象我这样的开发人员不打算去研究IL代码规范,这儿列出了一些意欲改进字节码的IL设计决策:
l 提供更好的类型中立(有助于实现模板);
l 提供更好的语言中立;
l 执行前永远都编译成汇编语言,从不解释;
l 能够向类、方法等加入附加的声明性信息。参见15.特性;
目前,CLR还提供多操作系统支持,而且在其它领域还提供了对JVM的更好的互用性的支持。参见26.互用性。
25.库
语言如果没有库那它是没什么用的。C#以没有核心库著称,但它利用了.NET框架的库(它们中的一些就是用C#创建的)。本文着重于讲述C#语言的特别之处,而不是.NET的,那应该另文说明。简单地说,.NET库包括丰富的线程、集合、XML、ADO+、ASP+、GDI+以及WinForm库【译注:现在这些+们多已变成了.NETJ】。有些库是跨平台的,有些则是依赖于Windows的,请阅读下一段关于平台支持的讨论。
26.互用性
我认为把互用性分成三个部份论述是比较合适的:de,,并且对那些追求语言互用性、平台互用性和标准互用性。Java长于平台互用性,C#长于语言互用性。而在标准互用性方面,二者都各有长短。
(1) 语言互用性
和其它语言集成的能力只存在集成度和难易程度的区别。JVM和CLR都允许你用多种语言写代码,只要它们编译成字节码或IL码即可。然而,.NET平台做了大量的工作—不仅仅是能够把其它语言写的代码编译成IL码,它还使得多种语言可以自由共享和扩展彼此的库。例如,Eiffel或Visual Basic程序员可以导入C#类,重载其虚方法;C#对象也可以使用Visual Basic方法(多态)。如果你怀疑的话,VB.NET已经被大幅升级,它已具有现代面向对象特性(付出了和VB6兼容性的损失)。
为.NET写的语言一般插入Visual Studio.NET环境中,如果需要的话,可以使用同样的RAD框架。这就克服了使用其它语言是“二等公民”的印象。
C#提供了P/Invoke【译注:Platform Invocation Service,平台调用服务】,这比Java的JNI和C代码交互起来要简单得多(不需要dll)。这个特性很象J/direct,后者是微软Visual J++的一个特性。
(2) 平台互用性
一般而言,这意味着操作系统互用性。但是在过去的几年里,internet浏览器自身已经越来越象个平台了。
C#代码运行在一个受控执行环境里。这是使C#能够运行在不同操作系统上的技术重要的一步。然而,一些.NET库是基于Windows的,特别是WinForms库,它依赖于多如牛毛的Windows API。有个从Windows API移植到Unix系统项目,但目前还没有启动,而且微软也没有明确的暗示要这么做。
然而,微软并没有忽视平台互用性。.NET库提供了编写HTML/DHTML解决方案的扩展能力。对于可以用HTML/DHTML来实现的客户端来说,C#/.NET是个不错的选择。对于跨平台的需要更为复杂的客户界面的项目,Java是个好的选择。Kylix—Delphi的一个版本,允许同样的代码既可以在Windows上也可以在Linux上编译,或许将来也会成为跨平台解决方案的一个好的选择。
(3) 标准互用性
几乎所有标准,例如数据库系统、图形库、internet协议和对象通讯标准如COM和CORBA,C#都可以访问。由于微软在制订这些大多数标准上拥有权利或发挥了很大的作用,他们对这些标准的支持就处于一个很有利的位置。他们当然会因为商业上的动机(我没有说他们是否公正)而提供较少的标准支持—对于和他们竞争的东西—比如CORBA(COM的竞争对手)和OpenGL(DirectX的竞争对手)。类似地,Sun的商业动机(再一次,我没有说他们是否公正)意味着Java不会尽其所能地支持微软的标准。
由于C#对象被实现为.NET对象,因此它自动暴露为COM对象。C#因此就既可以暴露COM对象也可以使用COM对象。这样,就可以集成COM代码和C#项目。.NET是一个有能力最终替代COM的框架—但是,已经有那么多已部署的COM组件,我相信,不等.NET取代掉COM,它已经被下一波技术所取代了。但无论如何,希望.NET能有一个长久而有趣的历史!J
27.结论
到此为止,我希望已给了你一个C#与Java、C++在概念上的比较。总的来说,比起Java,我相信C#提供了更好的表达力并且更适合编写对性能有严格要求的代码,它也同样具有Java的优雅和简单,这也是它们都比C++更具吸引力之处。
—全文完—