总体了解C#( 1 C #和Java)
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。
译者:荣耀
1. 下面是C#和Java共有的特性列表,目的都是为了改进C++。这些特性虽非本文重点,但了解它们之间的相似之处还是很重要的。
编译为机器独立、语言独立的代码,运行在受控执行环境里;
采用垃圾收集机制,同时摒弃了指针(C#中,指针被限制在标为unsafe的代码内使用);
强有力的反射能力;
没有头文件,所有的代码都在包或组合体里,不存在类声明的循环依赖问题;
所有的类都派生自object,且必须用new关键字分配在堆上;
【译注:Java中为Object;C#中为object,相当于.NET的System.Object】
当进入标为锁定/同步代码时,通过在对象上加锁来支持多线程;
【译注:例如Java中可对方法施以synchronized关键字,在C#中可使用Monitor类、Mutex类、lock语句等等】
接口支持—多继承接口,单继承实现;
内部类;
类继承时无需指定访问级别;
【译注:在C++中,你可以这么做:class cls2: private cls1{};等等】
没有全局函数或常量,一切都必须属于类;
数组和字符串都保存长度记数并具边界检查能力;
永远使用“.”操作符,不再有“->”、“::”操作符;
null和boolean/bool是关键字;
【译注:Java中为boolean、C#中为bool,相当于System.Boolean】
所有的值在使用前必须被初始化;
if语句不能使用整型数为判别条件;
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中则可以用接口处理。它通过提供类型安全和支持多方法改进了函数指针方式;它通过可以进行方法调用而不需要内部类适配器或额外的代码去处理多方法调用问题而改进了接口方式。委托最重要用途是事件处理,下一节将通过一个例子加以介绍。
〖译注: 委托又分单播委托和多播委托,多播委托的返回值只能是void声明委托时,CLR其实会为它创建一个类,所以,我们可以理解成声明一个委托的模板类。并且,它可以将函数名将参数传递〗
5. 事件
C#提供了对事件的直接支持。尽管事件处理一直是编程的基本部分,但令人惊讶的是,大多数语言在正式化这个概念上所做的努力都微乎其微。如果看看现今主流框架是如何处理事件的,我们可以举出如下例子:Delphi的函数指针(称为闭包)和Java的内部类适配器,当然还有Windows API消息系统。C#使用delegate和event关键字提供了一个清爽的事件处理方案。我认为描述这个机制的最好的办法是举个例子来说明声明、触发和处理事件的过程:
// 委托声明定义了可被调用的方法签名【译注:这里的签名可以理解为“原型”】
Public delegate void ScoreChangeEventHandler (int newScore, ref bool cancel);
// 产生事件的类
public class Game
{
file://注意使用关键字
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!");
}
}
}
file://测试类
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. 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())
{
Object o = collection.get();
collection.next()
//...
2. for (int i = 0; i < array.length; i++)
//...
在 C#中:
1. foreach (object o in collection)
//...
2. foreach (int i in array)
//...
C#的for循环将工作于集合对象上(数组实现一个集合)。集合对象有一个GetEnumerator()方法,该方法返回一个Enumerator对象。Enumerator对象有一个MoveNext()方法和一个Current属性。
8. 结构
把C#的结构视为使语言的类型系统更为优雅而不仅是一种“如果你需要的话可以利用之写出真正有效率的代码”的概念更好些。
在C++中,结构和类(对象)都可分配在栈或堆上。在C#中,结构永远创建在栈上,类(对象)则永远创建在堆上。使用结构实际上可以生成更有效率的代码:
public struct Vector
{
public float direction;
public int magnitude;
}
Vector[] vectors = new Vector [1000];
这将把1000个Vector分配在一块空间上,这比我们把Vector声明为类并使用for循环去实例化1000个独立的Vector来得有效率得多。
【译注:因怀疑原文有误,此处故意漏译一句,但不应影响你对这节内容的理解】:
int[] ints = new ints[1000];//【译注:此处代码有误,应为int[] ints = new int[1000];】
C#完全允许你扩展内建在语言中的基本类型集。实际上,C#所有的基本类型都以结构方式实现的。int型只不过是System.Int32结构的别名,long型不过是System.Int64结构的别名等等。这些基本类型当然可被编译器特别处理,但是语言本身并无区别【译注:意思是语言自身对处理所有类型提供了一致的方法】。在下一节中,我们可看到C#是如何做到这一点的。
9. 类型一致
大多数语言都有基本类型(int、long等等)。高级类型最终是由基本类型构成的。能以同样的方式处理基本类型和高级类型通常来说是有用处的。例如,如果集合可以象包容sting那样包容int是有用的。为此,Smalltalk通过牺牲些许效率象处理string或Form一样来处理int和long。Java试图避免这个效率损失,它象C和C++那样处理基本类型,但又为每一个基本类型提供了相应的包装类—int包装为Integer,double包装为Double。C++模板参数可接受任何类型,只要该类型提供了模板定义的操作的实现。
【译注:在Java中,你可以这么写:
int i = 1;
double d = 1.1;
Integer iObj = new Integer(1);
Double dObj = new Double(1.1);
以下写法是错误的:
int I = new int(1);
Integer iObj = 1;
】
C#对该问题提供了一个不同的解决方案。在上一节里,我介绍了C#中的结构,指出基本类型不过是结构的一个别名而已。既然结构拥有所有对象类型拥有的方法,那代码就可以这么写:
int i = 5;
System.Console.WriteLine (i.ToString());
如果我们想象使用一个对象那样使用一个结构,C#将为你装箱该结构为对象,当你再次需要使用结构时,可以通过拆箱实现:
Stack stack = new Stack ();
stack.Push (i); // 装箱
int j = (int) stack.Pop(); file://拆箱
拆箱不仅是类型转换的需要,它也是一个无缝处理结构和类之间关系的方式。你要清楚装箱是做了创建包装类的工作,尽管CLR可以为被装箱的对象提供附加的优化。
【译注:可以这么认为,在C#中,对于任何值(结构)类型,都存在如下的包装类:
class T_Box file://T代表任何值类型
{
T Value;
T_Box(T t){Value = t;}
}
当装箱时,比如:
int n = 1;
object box = n;
概念上相当于:
int n = 1;
object box = new int_Box(i);
当拆箱时,比如:
object box = 1;
int n = (int)box;
概念上相当于:
object box = new int_Box(1);
int n = ((int_Box)box).Value; 】
【作者注:C#的设计者在设计过程中应该考虑过模板。我怀疑未采用模板有两个原因:第一个是混乱,模板可能很难和面向对象的特性融合在一起,它为程序员的带来了太多的(混乱)设计可能性,而且它很难和反射一起工作;第二点是,如果.NET库(例如集合类)没有使用模板的话,模板将不会太有用。不过,果真.NET类使用了它们,那将有20多种使用.NET类的语言不得不也要能和模板一起工作,这在技术上是非常难以实现的。
注意到模板(泛型)已经被Java社团考虑纳入Java语言规范之中是一件有意思的事。或许每个公司都会各唱各的调—Sun说“.NET患了最小公分母综合症”,而微软则说“Java不支持多语言”。
( 8 月 10 日 致歉)看了一个对Anders Hejlsberg的专访后(http://windows.oreilly.com/news/hejlsberg_0800.html),感觉似乎模板已浮出地平线,但第一版没有,正因我们上面提到的种种困难。看到IL规范是如此写法使得IL码可以展现模板(用一个非破坏的方式以让反射可以很好的工作)而字节码则不可以是一件很有趣的事。在此,我还给出了一个关于Java社团考虑要加入泛型的链接:http://jcp.org/jsr/detail/014.jsp 】
【译注:此处是上文提到的对Anders Hejlsberg采访的中文版链接:http://www.csdn.net/develop/article/11/11580.shtm。另外,如欲了解更多关于泛型编程知识,请参见此处链接:http://www.csdn.net/develop/article/11/11440.shtm】
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 file://【译注:应为class B】
{
public virtual void foo () {}
}
Class D : B file://【译注:应为class D : B】
{
public override void foo () {}
}
试图重载一个非虚的方法将会导致一个编译时错误,除非对该方法加上“new”关键字,以指明该方法意欲隐藏父类的方法。
Class N : D file://【译注:应为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
file://只看调用处,是不知道你使用的引用还是对象的,但运行结果迥异!
return 0;
}
】
15. 特性
C#和Java的编译代码里都包括类似于字段访问级别的信息。C#扩展了这个能力,对类中的任何元素,比如类、方法、字段甚至是独立参数,你都可以编译自定义的信息,并可以于运行时获取这些信息。这儿有一个非常简单的使用特性的类的例子:
[AuthorAttribute ("Ben Albahari")]
class A
{
[Localizable(true)]
public String Text file://【译注:应为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++这种模型获得了巨大的成功(你所使用的大多数软件可能都是这样编译的),但也有其局限性:
程序无丰富的接口以和其它程序进行交互(微软的COM就是为了克服这个限制而创建的)
程序不能以跨平台的形式分发
不能把程序限制执行在一个安全操作的沙箱里
为了解决这些问题,Java采用了Smalltalk采用过的方式,即编译成字节码,运行在虚拟机里。在被编译前,字节码维持程序的基本结构。这就使得Java程序和其它程序进行各种交互成为可能。字节码也是机器中立的,这也意味着同样的class文件可以运行于不同的平台。最后,Java语言没有显式的内存操作(通过指针)的事实使得它很适合于编写“沙箱程序”。
最初的虚拟机利用解释器来把字节码指令流转换为机器码。但是这个过程慢得可怕以致于对于那些关注性能的程序员来说,从来都没有吸引力。如今,绝大多数JVM都利用JIT编译器,基本编译成机器码—在进入类框架的范围之前和方法体执行之前。在它运行前,还有可能将Java程序转换为汇编语言,可以避免启动时间和即时编译的内存负担。和编译Visual C++程序相比,这个过程并不需要移去程序对运行时的依赖。Java运行时(这个术语隐藏在术语Java虚拟机下之下)将处理程序执行的很多至关重要的方面,比如垃圾收集和安全管理。运行时也被认为是受控执行环境。
尽管术语有点含糊不清,尽管从不用解释器,但.NET基本模型也是使用如上所述方式。.NET的重要的改进将来自于IL自身的设计的改进。Java可以匹敌的唯一方式是修改字节码规范以达到严格的兼容。我不想讨论这些改进的细节,这应该留给那些极个别的既了解字节码也了解IL码的开发人员去讨论。99%的象我这样的开发人员不打算去研究IL代码规范,这儿列出了一些意欲改进字节码的IL设计决策:
提供更好的类型中立(有助于实现模板);
提供更好的语言中立;
执行前永远都编译成汇编语言,从不解释;
能够向类、方法等加入附加的声明性信息。参见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++更具吸引力之处。
—全文完—