A Comparative Overview of C#中文版(上篇)

类别:.NET开发 点击:0 评论:0 推荐:

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())

{

    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(); //拆箱

拆箱不仅是类型转换的需要,它也是一个无缝处理结构和类之间关系的方式。你要清楚装箱是做了创建包装类的工作,尽管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),感觉似乎模板已浮出地平线,但第一版没有,正因我们上面提到的种种困难。看到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

 

本文地址:http://com.8s8s.com/it/it46196.htm