在新的平台上编程
----微软 .NET平台系列文章之一
译文/赵湘宁
一年多来,我将注意力一直放在微软的.NET CLR(公共语言运行时:Common Language Runtime)平台。在我看来,今后大多数新的开发都将面向这个平台,因为它使应用程序的开发变得更容易、更简单。同时,我还期望现有的应用开发能迅速移到.NET平台上来。
为了帮助开发人员掌握这个新的平台,本文以及以后的系列文章将专门针对.NET讨论各种编程问题。我将假设你已经熟悉面向对象的编程概念。每一篇文章的内容都聚焦在非选定的特定公共语言运行时编程主题上。所有.NET开发人员必须知道这些主题。
当展示代码例子时,我必须在支持.NET CLR 的多种语言中选择一种。我决定使用C#。它是微软设计的一种新语言。
我的目的是介绍不同的编程主题并就如何实现它们为你提供一些想法。所以,我不想完整的描述每一个主题以及所有与之相关的细微差别。有关主题完整详细的介绍请参考公共语言运行时或者语言文档。
真正的面向对象设计
对于使用Win32 SDK的编程人员来说,对大多数操作系统特性的访问时通过一组从动态链接库输出的独立函数实现的。这些独立的函数从诸如C这样的非面向对象语言中非常容易调用。但对于一个新的开发人员来说,要面对上千个表面上看来毫无关系的独立的函数是相当让人畏惧的。更为困难的是许多函数名是以单词“Get”开始的(如GetCurrentProcess和GetStockObject)。此外,Win32 API已经历数年并且微软添加了新的函数,这些新函数依旧的函数相比。有相似的语义,但提供的特性有些差异。你常常能认出较新的函数,因为它们的名字原来的函数名相似(象CreateWindow/CreateWindowEx,CreateTypeLib/CreateTypeLib2以及我最喜欢的CreatePen/CreatePenIndirect/ExtCreatePen
所有这些问题都使程序员觉得Windows开发很难。随着.NET平台的出现,微软终于为叫苦不迭的开发人员提供了一个完全面向对象的开发平台。平台服务现在被分成为单独的名字空间(如:System.Collections,System.Data,System.IO,SystemSecurity,System.Web等等)并且每一个名字空间包含一组允许访问平台服务的相关类。
因为类方法可以重载,行为差别不大的方法具有相同的名字,并且只有从原型中才能看出差别来。例如,一个类可能提供三个不同版本的CreatePen方法。所有方法都做相同的事情:即创建一支笔。但是,每一个方法都有不同的参数集并且行为不太一样。将来微软还要创建第四个CreatePen方法并且与前面的类方法配合默契。
因为所有的平台服务都通过这种面向对象的方式来实现,所以软件开发者应该对面向对象的编程有所理解。面向对象的方法还带来了其它的一些特点,如使用继承和多态性很容易创建专门版本的基类库类型。我再次强烈建议要熟练掌握这些概念,这对于使用微软的.NET框架很重要。
System.Object
在.NET中,每一个对象都是从System.Object派生而来。也就是说下面的两种类型定义(使用C#)是相同的:
class Jeff {
...
}
和
class Jeff : System.Object {
...
}
因为所有对象都是从System.Object派生出来的,从而可以保证每一个对象具有最小的功能集。表一是System.Object中的公共方法。
公共语言运行时需要所有的对象都要用new操作符创建(调用newobj IL指令)。下列代码示范了如何创建Jeff类型(已在前面声明)的对象实例:
Jeff j = new Jeff("ConstructorParam1");
new操作符根据指定的类型需要从堆中分配字节数来创建对象。它初始化对象的开销成员。每一个对象都会有一些公共语言运行时用来管理对象的附加字节,如对象的许表指针以及对同步快的引用。
调用类的构造函数时,传递的参数在new语句中指定(例子中是串"ConstructorParam1")。注意大多数语言会编译构造函数以便它们调用基类构造函数,但这在公共语言运行时中是不需要的。
在new实现了所有我所提到的操作后,它返回新创建对象的引用。在例子代码中,这个引用被存储在变量j中,它的类型是Jeff。
另外,new操作符没有配对操作(delete)。即没有方法显式地释放或销毁对象。公共语言运行时提供自动地探测的垃圾回收环境,当对象不再被使用或不再被访问时自动地释放和销毁对象,有关这个主题将在下次的讨论中提出。
数据类型的强制转换
在编程过程当中,对象从一个数据类型到另一个数据类型的强制类型转换是十分常见的。在这一部分,我将讨论对象的强制数据类型转换规则。为此,先看下列代码:
System.Object o = new Jeff("ConstructorParam1");
先前的代码编译通过并正确执行是因为有一个隐含的强制类型转换。new操作符返回Jeff的一个引用类型,但o是一个System.Object的引用类型。因为所有的类型(包括Jeff类型)都能被强制转换为System.Object,隐含的强制类型转换是成功的。但是,如果执行下面的代码,就会有编译器错误,因为编译器不提供基类型到派生类型的强制类型转换。
Jeff j = o;
为了能通过编译,必须插入如下的显式强制类型转换:
Jeff j = (Jeff) o;
现在就可以编译通过并成功执行。
再来看另外一个例子:
System.Object o = new System.Object();
Jeff j = (Jeff) o;
第一行创建了一个System.Object类型对象。第二行代码试图将System.Object引用类型转换为Jeff引用类型。两行代码都能编译通过。但是在执行的时候,第二行代码产生一个InvalidCastException异常,如果捕获不到这个异常,将强制应用程序终止。
当第二行代码执行时,公共语言运行时查证o所指的对象就是Jeff类型对象(或任何Jeff派生类型)。如果是,则公共语言运行时允许强制类型转换。否则,如果o所指的对象与Jeff类型无关,或是一个Jeff的基类,则公共语言运行时会预防这种不安全的强制类型转换并产生InvalidCastException异常。
C# 使用as操作符提供另一种方法来实现强制类型转换:
Jeff j = new Jeff(); // 创建一个新的Jeff 对象
System.Object o = j as System.Object; // 强制转换 j 为一个System.Object对象
// 现在o 指Jeff 对象
as操作符试图强制转换一个对象为指定的类型。但与通常的强制转换不一样,如果对象的类型强制转换不成功,结果会是null,as操作符决不会掷出异常。当引用有毛病的强制类型转换发生时,将产生NullReferenceException异常。下列代码示范了这种情况。
System.Object o = new System.Object(); //创建一个新的Object 对象
Jeff j = o as Jeff; //强制转换 o 为一个Jeff对象
// 上面的强制转换失败:不会有异常掷出,而j会被置为null
j.ToString(); // 访问j时产生一个NullReferenceException 异常
除了as操作符以外,C#还提供一个is操作符。它检查是否一个对象实例与给定的类型兼容并判断结果是True或是False。Is操作符不会产生异常。
System.Object o = new System.Object();
System.Bool