C#与C++、Java之比较概览

Ben Albahari 著  荣耀 译

本文翻译时间较早。欢迎指出任何误失。谢谢。

感谢以下人士的支持和反馈(按字母先后顺序):

Don Box、C.R. Manning、Joe Nalewabau、John Osborn、Thomas Rhode、Daryl Richter。

本文以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语言设计师)和Scott Wiltamuth共同设计的。

目录

  1. C#和Java

  2. 属性

  3. 索引器

  4. 委托

  5. 事件

  6. 枚举

  7. 集合和foreach语句

  8. 结构

  9. 类型一致

  10. 操作符重载

  11. 多态

  12. 接口

  13. 版本处理

  14. 参数修饰符

  15. 特性(attribute)

  16. 选择语句

  17. 预定义类型

  18. 字段修饰符

  19. 跳转语句

  20. 程序集、名字空间和访问级别

  21. 指针运算

  22. 多维数组

  23. 构造器和析构器

  24. 托管运行环境

  25. 互操作性

  26. 结论

1.C#和Java

下面是C#和Java共有的特性列表,目的都是为了改进C++。这些特性虽非本文重点,但了解它们之间的相似之处还是非常重要的。

  • 编译为机器独立、语言独立的代码,运行在托管运行环境中

  • 采用垃圾收集机制,同时摒弃了指针(C#中,指针被限制在标为unsafe的代码内使用)

  • 强有力的反射能力

  • 没有头文件,所有代码都在包或装程序集里,不存在类声明的循环依赖问题

  • 所有的类都派生自object,且必须用new关键字分配于堆上

  • 当进入标以锁定/同步代码时,通过在对象上加锁来支持多线程

  • 接口支持:多继承接口、单继承实现

  • 内部类

  • 类继承时无需指定访问级别 [译注:在C++中,你可以这么做:class cls2: private cls1{};等等]

  • 没有全局函数或常量,一切都必须属于类

  • 数组和字符串都保存长度记数并具边界检查能力

  • 永远使用“.”操作符,不再有“->”、“::”操作符

  • null和boolean/bool是关键字

  • 所有的值在使用前必须被初始化

  • if语句不能使用整数作为判断条件

  • 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#的处理方式有诸多优点。它鼓励程序员按照属性的方式去思考 — 把这个属性标为可读写的和只读的哪个更自然?或者根本不应该为属性?如果你想改变你的属性的名称,你只要检查一处就可以了(我曾看到过中间隔了几百行代码的获取器和设置器里对同一个数据成员/字段的获取器和设置器])。注释也只要一处就可以了,这也避免了彼此同步的问题。IDE是可以帮助做这个事的(事实上,我建议微软开发人员这么做),但应该牢记一个基本编程原理:尽力做好模拟我们问题空间的抽象。而一个支持属性的语言将有助于获得更好的抽象。

作者注:关于属性的此项优点的一个反对意见认为:当采用这种语法时,你搞不清是在操纵一个字段还是属性。然而,在Java(当然也包括C#)中,几乎所有真正复杂一点的类都不会有public字段。字段一般都只具有尽可能小的访问级别(private/protected,或语言所定义的 默认的),并且只通过获取器和设置器方法暴露,这也意味着你可以获得优美的语法。让IDE解析代码也是完全可行的:可以采用不同的颜色高亮显示属性,或提供代码完成信息以表明它是否是一个属性。我们还应该看到,如果一个类设计良好,这个类的用户将只关心该类的接口(或 规范),而不是其内部实现。另外一个可能的争论是属性不够有效率。事实上,好的编译器可以内联仅返回某个字段的获取器,这和直接访问字段一样快。说到底,即使使用字段比获取器/设置器来的有效,使用属性还有如下好处:日后可以改变属性 相关联的字段,而不会影响依赖于该属性的代码。

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为“一切都是一个类先生”好像公平些:) 过去的这几年里,在编程界,“做努力模拟现实的抽象”已经被很多人代之以“现实是面向对象的,所以,我们应该用面向对象的抽象来模拟它”。

Sun和微软关于委托的争论可以在这儿看到:

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而编译器不会发出任何抱怨。坦白地说,在我的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++模板参数可接受任何类型,只要该类型提供了模板 所定义的操作的实现。

C#对该问题提供了一个不同的解决方案。在上一节里,我介绍了C#中的结构,指出基本类型不过是结构的一个别名而已。既然结构拥有所有对象类型拥有的方法, 那么代码就可以这么写:

int i = 5;
System.Console.WriteLine(i.ToString());

假如我们希望像使用对象那样使用结构,C#会为你将该结构装箱为对象,当你再次需要使用结构时,可以通过拆箱来实现:

Stack stack = new Stack ();
stack.Push (i);               // 装箱
int j = (int) stack.Pop();    // 拆箱

拆箱不仅是类型转换的需要,它也是一个无缝地处理结构和类之间关系的方式。你要清楚装箱是做了创建包装类的工作。另外,CLR可以为被装箱的对象提供额外的优化 处理。

译注:可以这么认为,在C#中,对于任何值(结构)类型,都存在如下的包装类:

class T_Box    // 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),我感觉模板似已浮出地平线。不过C#第一版还没有, 原因正在于我们上面提到的种种困难。看到IL规范是如此写法使得IL码可以展现模板(采用一种非破坏的方式以让反射机制可以很好地工作)而字节码则不可以 ,是一件很有趣的事。在此,我还给出一个关于Java社团考虑要加入泛型功能的链接:http://jcp.org/jsr/detail/014.jsp

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

比较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
{
    public virtual void foo () {}
}

class D : B
{
    public override void foo () {}
}

试图覆写一个非虚方法将会导致一个编译错误,除非对该方法加上new关键字,以指明该方法意欲隐藏父类的方法。

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关键字使得阅读源代码时可以清晰地看出哪些方法是覆写的。不过,使用虚方法有利有弊。首先,避免使用虚方法可以略微提高 运行速度,其次,可以清楚地知道哪些方法会被覆写。然而,利也可能是弊。相较而言,Java中默认忽略final修饰符,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修饰符的(这真正表达了程序员的意愿),因此,运行时(runtime)知道让类D的foo方法隐藏类B的foo方法,而不是覆写它。

在此引用C#参考手册中一句有意思的话:“C#处理版本问题,是通过要求开发人员明确自己的意图而实现的”。尽管使用override是一个表达意图的办法,但编译器也能自动生成 — 通过在编译期检查方法是否在执行(而不是声明)一个覆写。这就意味着,你仍然能够拥有象Java一样的语言(Java不用virtual和override关键字),并且仍然能够正确处理版本问题。

参见字段修饰符部分 。

14.参数修饰符

(1)ref参数修饰符

C#(和Java相比)可以让你按引用传递参数。描述这一点的最明显的例子是通用swap方法。不像C++,在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修饰符可以加在方法的最后的参数上,意思是方法将接受任意数量的指定类型的参数。例如:

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#显式传递引用,不管是方法声明时还是调用时。它大大地减少了混乱
[译注:比方说,在C++中,有时你并不知道你是在使用一个对象还是一个对象引用,本节后有示例],并达到了和Java同样的目标,但C#的方式更有表达力。显然这是C#的主旨 — 它不把程序员圈在一个圈里,使他们必须绕一个大弯子才能做成某件事。还记得Java吗?Java指南 对如何解决传递引用的问题建议如下:你应该传递一个具有1个元素的数组以便保存你的值,或者另做一个类以保存这个值。

译注:

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.特性(attribute)

C#和Java的编译代码里都包括类似于字段访问级别的信息。C#扩展了这个能力,对类中的任何元素,比如类、方法、字段甚至是独立参数,你都可以编译自定义的信息,并可以于运行时获取这些信息。这儿有一个非常简单的使用特性的类 的例子:

[AuthorAttribute ("Ben Albahari")]
class A
{
    [Localizable(true)]
    public string Text
    {
        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。private和public和Java中意思一样。在C#中,没有标明访问级别的就是private,而非包(package)范围的。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库[译注:现在这些+多已变成了.NET]。有些库是跨平台的,有些则依赖于Windows,请阅读下一 节关于平台支持的讨论。

26.互操作能力

我认为把互操作分成三个部份论述是比较合适的:语言互操作、平台互操作和标准互操作。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)平台互操作

一般而言,这意味着操作系统互操作,但在过去的几年里,IE自身已经越来越象个平台了。

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能有一个长久而有趣的历史!

27.结论

至此,我希望已为你展示了C#与Java、C++概念上的比较。总的来说,比起Java,我认为C#提供了更好的表达力并且更适于编写性能要求严格的代码,而且它同样具有Java的优雅和简单,这也是它们比C++更具吸引力之处。