** 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 】
本文将以 C#提供的新的编程方式以及它是如何改进两个近邻—Java和C++为中心。C#在很多方面和Java用了类似的方式改进C++。因此,我不打算重复诸如单根对象层次的优点之类的东西。正文将以C#和Java的相似之处概述开始,然后着重探究C#的新特性。
** 背景 **
2000年6月,微软同时宣布了.NET平台和一个名为C#的新的编程语言。C#是一个很好地融合了简单、表达力、性能的强类型的面向对象的语言。.NET平台以公共语言运行时(类似于Java虚拟机)和一个可被多种语言(它们可以通过编译成中间语言从而可以协同工作)共用的库为中心。C#和.NET有那么一点共生关系—C#的一些特性和.NET协作得很好,反之亦然(尽管.NET的目标是和多种语言很好地协作)。本文主要关注于C#,但视需要偶尔也会提及.NET。C#的设计借鉴了多种语言,但最主要的还是Java和C++。它是由Anders Hejlsberg(大名鼎鼎的Delphi【译注:说成Object Pascal更合适些】语言设计师)和Scott Wiltamuth共同设计的。
** 目录 **
1. C#和Java
2. 属性
3. 索引器
4. 委托
5. 事件
6. 枚举
7. 集合和 foreach语句
8. 结构
9. 类型一致
10. 操作符重载
11. 多态
12. 接口
13. 版本处理
14. 参数修饰符
15. 特性【译注:即 attribute,我在《C#首席设计师Anders Hejlsberg专访》译文中(参见CSDN的 http://www.csdn.net/develop/article/11/11580.shtm )曾说过,到目前为止,该词译法仍较混乱,甚至和property不分,都被译为“属性”(Visual Studio.NET 7.0 Beta 2 的联机文档就是如此)。但本文中,仍将其译为“特性”,以示区分】
16. 选择语句
17. 预定义类型
18. 字段修饰符
19. 跳转语句
20. 组合体、名字空间和访问级别【译注: Assembly一词译法比较混乱,有的译为“配件”,有的译为“组件”,有的译为“组合体”,而Visual Studio.NET 7.0 Beta2联机文档上则译为“程序集”,从技术上讲,这个译法说的倒很事实,但总感觉和这个词的外观远了点,在译法尚未统一之前,本文暂译为“组合体”】
21. 指针运算
22. 多维数组【译注:这一节里还谈到了交错数组】
23. 构造器和析构器
24. 受控执行环境
25. 库
26. 互用性
27. 结论
** 1.C#和Java **
下面是 C#和Java共有的特性列表,目的都是为了改进C++。这些特性虽非本文重点,但了解它们之间的相似之处还是很重要的。
l 编译为机器独立、语言独立的代码,运行在受控执行环境里;
l 采用垃圾收集机制,同时摒弃了指针( C#中,指针被限制在标为unsafe的代码内使用);
l 强有力的反射能力;
l 没有头文件,所有的代码都在包或组合体里,不存在类声明的循环依赖问题;
l 所有的类都派生自 object,且必须用new关键字分配在堆上;【译注:Java中为Object;C#中为object,相当于.NET的System.Object】
l 当进入标为锁定 /同步代码时,通过在对象上加锁来支持多线程;【译注:例如Java中可对方法施以synchronized关键字,在C#中可使用Monitor类、Mutex类、lock语句等等】
l 接口支持—多继承接口,单继承实现;
l 内部类;
l 类继承时无需指定访问级别;【译注:在 C++中,你可以这么做:class cls2: private cls1{};等等】
l 没有全局函数或常量,一切都必须属于类;
l 数组和字符串都保存长度记数并具边界检查能力;
l 永远使用“ .”操作符,不再有“->”、“::”操作符;
l null和boolean/bool是关键字;【译注:Java中为boolean、C#中为bool,相当于 System.Boolean 】
l 所有的值在使用前必须被初始化;
l if语句不能使用整型数为判别条件;
l try语句块后可以跟finally从句。【译注:标准C++不可以,但Visual C++对SEH做了扩展,可以用__try和__finally】
** 2.属性 **
对于Delphi和Visual Basic的用户来说,属性是个熟悉的概念。使用属性的目的是将获取器/设置器[译注:原文为getter/setter]的概念正式化,这是一个被广泛使用的模式,尤其是在RAD(快速应用开发)工具里。
以下是你可能在Java或C++里写的典型代码:
foo.setSize (getSize () + 1);
label.getFont().setBold (true);
同样代码在C#里可能会变成:
foo.size++;
label.font.bold = true;
C#代码对于使用foo和label的用户来说更直观、更可读。在实现属性方面,差不多同样简单:
Java/C++:
public int getSize()
{
return size;
}
public void setSize (int value)
{
size = value;
}
C#:
public int Size
{
get {return size;}
set {size = value;}
}
特别是对于可读写的属性, C#提供了一个处理此概念的更清爽的方式。在C#中,get和set方法是内在的,而在Java和C++里则需人为维护。C#的处理方式有诸多优点。它鼓励程序员按照属性的方式去思考—把这个属性标为可读写的和只读的哪个更自然?或者根本不应该为属性?如果你想改变你的属性的名称,你只要检查一处就可以了(我曾看到过中间隔了几百行代码的获取器和设置器【译注:此处是指C++(Java)里对同一个数据成员/字段(一般来说是)的获取器和设置器】)。注释也只要一处就可以了,这也避免了彼此同步的问题。IDE【译注:集成开发环境】是可以帮助做这个事的(事实上,我建议他们这么做【译注:此处的“他们”应该是指微软有关人员】),但应该牢记编程上的一个基本原理—尽力做好模拟我们问题空间的抽象。一个支持属性的语言将有助于获得更好的抽象。
【作者注:关于属性的这个优点的一个反对意见认为:当采用这种语法时,你搞不清是在操纵一个字段还是属性。然而,在 Java(当然也包括C#)中,几乎所有真正复杂一点的类都不会有public的字段。字段一般都只具有尽可能小的访问级别(private/protected,或语言所定义的缺省的),并且只通过获取器和设置器方法暴露,这也意味着你可以获得优美的语法。让IDE解析代码也是完全可行的,可用不同的颜色高亮显示属性,或提供代码完成信息以表明它是否是一个属性。我们还应该看到,如果一个类设计良好,这个类的用户将只关心该类的接口(或规范)【译注:此处是指该类向其客户公开(不单单是public,对其派生类来说,也可能是protected)的方法、属性(C++/Java无显式属性概念)等,这里的客户包括其派生类等等】,而不是其内部实现。另外一个可能的争论是属性不够有效率。事实上,好的编译器可以内联仅返回某个字段的获取器,这和直接访问字段一样快。说到底,即使使用字段要比获取器/设置器来的有效,使用属性还有如下好处—日后可以改变属性的字段【译注:是指可以改变获取器/设置器的实现代码部分,比如改变获取器/设置器里所操作的字段,也可以在获取器/设置器里做一些校验或修饰工作等】,而不会影响依赖于该属性的代码】
** 3.索引器 **
C#通过提供索引器,可以象处理数组一样处理对象。特别是属性,每一个元素都以一个get或set方法暴露。
public class Skyscraper
{
Story[] stories;
public Story this [int index]
{
get
{
return stories [index];
}
set
{
if (value != null)
{
stories [index] = value;
}
}
}
//...
}
Skyscraper empireState = new Skyscraper (/.../);
empireState [102] = new Story ("The Top One", /.../);
【译注:索引器最大的好处是使代码看上去更自然,更符合实际的思考模式】
** 4.委托 **
委托可以被认为是类型安全的、面向对象的函数指针,它可以拥有多个方法。委托处理的问题在C++中可以用函数指针处理,而在Java中则可以用接口处理。它通过提供类型安全和支持多方法改进了函数指针方式;它通过可以进行方法调用而不需要内部类适配器或额外的代码去处理多方法调用问题而改进了接口方式。委托最重要用途是事件处理,下一节将通过一个例子加以介绍。
** 5.事件 **
C#提供了对事件的直接支持。尽管事件处理一直是编程的基本部分,但令人惊讶的是,大多数语言在正式化这个概念上所做的努力都微乎其微。如果看看现今主流框架是如何处理事件的,我们可以举出如下例子:Delphi的函数指针(称为闭包)和Java的内部类适配器,当然还有Windows API消息系统。C#使用delegate和event关键字提供了一个清爽的事件处理方案。我认为描述这个机制的最好的办法是举个例子来说明声明、触发和处理事件的过程:
// 委托声明定义了可被调用的方法签名【译注:这里的签名可以理解为“原型”】
public delegate void ScoreChangeEventHandler (int newScore, ref bool cancel);
// 产生事件的类
public class Game
{
//注意使用关键字
public event ScoreChangeEventHandler ScoreChange;
int score;
// 属性Score
public int Score
{
get
{
return score;
}
set
{
if (score != value)
{
bool cancel = false;
ScoreChange (value, ref cancel);
if (! cancel)
score = value;
}
}
}
}
// 处理事件的类
public class Referee
{
public Referee (Game game)
{
// 监视game中的score的分数改变
game.ScoreChange += new ScoreChangeEventHandler (game_ScoreChange);
}
// 注意这个方法签名和ScoreChangeEventHandler的方法签名要匹配
private void game_ScoreChange (int newScore, ref bool cancel)
{
if (newScore < 100)
System.Console.WriteLine ("Good Score");
else
{
cancel = true;
System.Console.WriteLine ("No Score can be that high!");
}
}
}
//测试类
public class GameTest
{
public static void Main ()
{
Game game = new Game ();
Referee referee = new Referee (game);
game.Score = 70;//【译注:输出 Good Score】
game.Score = 110;// 【译注:输出 No Score can be that high!】
}
}
在 GameTest里,我们分别创建了一个game和一个监视game的referee,然后,然后我们改变game的Score去看看referee对此有何反应。在这个系统里,game没有referee的任何知识,任何类都可以监听并对game的score变化产生反应。关键字event隐藏了除了+=和-=之外的所有委托方法。这两个操作符允许你添加(或移去)处理该事件的多个事件处理器。
【译注:我们以下例说明后面这句话的意思:
public class Game
{
public event ScoreChangeEventHandler ScoreChange;
protected void OnScoreChange()
{
if (ScoreChange != null) ScoreChange(30, ref true);//在类内,可以这么使用
}
,但在这个类外, ScoreChange就只能出现在运算符+=和-=的左边】
你可能首先会在图形用户界面框架里遇到这个系统。game好比是用户界面的某个控件,它根据用户输入触发事件,而referee则类似于一个窗体,它负责处理该事件。
【作者注:委托第一次被微软Visual J++引入也是Anders Hejlsberg设计的,同时它也是造成Sun和微软在技术和法律方面争端的起因之一。James Gosling,Java的设计者,对Anders Hejlsberg曾有过一个故作谦虚听起来也颇为幽默的评论,说他因为和Delphi藕断丝连的感情应该叫他“方法指针先生”。在研究Sun对委托的争执后,我觉得称呼Gosling为“一切都是一个类先生”好像公平些 J 过去的这几年里,在编程界,“做努力模拟现实的抽象”已经被很多人代之以“现实是面向对象的,所以,我们应该用面向对象的抽象来模拟它”。
Sun和微软关于委托的争论可以在这儿看到:
http://www.Javasoft.com/docs/white/delegates.html http://msdn.microsoft.com/visualj/technical/articles/delegates/truth.asp 】
** 6.枚举 **
枚举使你能够指定一组对象,例如:
声明:
public enum Direction {North, East, West, South};
使用:
Direction wall = Direction.North;
这真是个优雅的概念,这也是 C#为什么会决定保留它们的原因,但是,为什么Java却选择了抛弃?在Java中,你不得不这么做:
声明:
public class Direction
{
public final static int NORTH = 1;
public final static int EAST = 2;
public final static int WEST = 3;
public final static int SOUTH = 4;
}
使用:
int wall = Direction.NORTH;
看起来好像 Java版的更富有表达力,但事实并非如此。它不是类型安全的,你可能一不小心会把任何int型的值赋给wall而编译器不会发出任何抱怨【译注:你显然不可以这么写:Direction wall = Direction.NORTH;】。坦白地说,在我的Java编程经历里,我从未因为该处非类型安全而花费太多的时间写一些额外的东西来捕捉错误。但是,能拥有枚举是一件快事。C#带给你的一个惊喜是—当你调试程序时,如果你在使用枚举变量的地方设置断点,调试器将自动译解direction并给你一个可读的信息,而不是一个你自己不得不译解的数值:
声明:
public enum Direction {North=1, East=2, West=4, South=8};
使用:
Direction direction = Direction.North | Direction.West;
if ((direction & Direction.North) != 0)
//....
如果你在 if语句上设置断点,你将得到一个你可读的direction而不是数值5。
【译注:这个例子改一下,会更有助于理解:
声明:
public enum Direction {North=1, East=2, West=4, South=8, Middle = 5/注意此处代码/};
使用:
Direction direction = Direction.North | Direction.West;
if ((direction & Direction.North) != 0)
//....
如果你在 if语句上设置断点,你将得到一个可读性好的direction(即Middle)而不是数值5】
【作者注:枚举被 Java抛弃的原因极有可能是因为它可以用类代替。正如我上面提到的,单单用类我们不能够象用别的概念一样更好地表达某个特性。Java的“如果它可以用类处理,那就不引入一个新的结构”的哲学的优点何在?看起来最大的优点是简单—较短的学习曲线,并且无需程序员去考虑做同一件事的多种方式。实际上,Java语言在很多方面都以简化为目标来改进C++,比如不用指针,不用头文件,以及单根对象层次等。所有这些简化的共性是它们实际上使得编程—唔—简单了,可是,没有我们刚才提到的枚举、属性和事件等等,反而使你的代码更加复杂了】
** 7.集合和foreach语句 **
C#提供一个for循环的捷径,而且它还促进了集合类更为一致:
在 Java或C++中:
1. while (! collection.isEmpty())
<SPAN lang=EN-US style="FONT-SIZE: 9pt; FONT-FAMILY: