Programming for the New Platform

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

Programming for the New Platform
基于新平台的程序设计


Jeffrey Richter
Translator: Bin Yang (AREC PINK TEAM)


在过去的这些年里,我一直关注微软.NET通用语言运行(Common Language Runtime)平台。我认为,大多数新的开发将集中到这个平台上来,因为它使得程序开发更容易、更便捷。我同样希望目前的应用程序开发尽快转移到这个平台上来。
为了帮助开发人员了解这种新的平台,下面的章节介绍了在.NET上的各种不同的编程方式.我假定你们已经非常熟悉面向对象的程序设计概念。每个章节都将重点介绍针对通用语言运行的一般性的程序设计主题。所有的.NET开发人员必须了解这些主题。
为了表示一些代码的例子,我必须选择一种支持.NET通用语言运行的程序语言。本来可选择的最通用的语言应该是中间语言(Intermediate Language)汇编。然而,IL汇编的使用并不普遍,因此我决定使用C#。C#是微软设计的用于Managed Code开发的一种新的语言。在这里,虽然文中的代码样例是用C#来写的,注释也是针对C#的编码方式,但是我们讨论的概念是针对通用语言运行的,因此这些概念可应用于任何.NET平台上的语言。
我会介绍各种各样的程序设计主题,并且告诉你们它们是如何实现的。至于详细描述每个主题和围绕该主题所有的细节并不是我的目的。如果需要了解任何程序设计主题的详尽内容,请参考通用语言运行和语言文档。现在,我们开始具体介绍.

真正的面向对象的设计
对于使用Win32(r) SDK的程序员来说,访问操作系统的大部分特性是通过DLL导出的独立的函数来实现的。如果用非面向对象的语言,比如C,去调用这些独立的函数将是非常容易的,但是让没有经验的开发人员去面对成千上万,表面上看起来没有多少联系的函数,会使他们非常困惑。而使问题变得更困难的是这样一个事实:很多函数以“Get”打头(例如,GetCurrentProcess 和GetStockObject).另外这些年来WIN32API有了新的发展,微软已经增加了新的函数,它们与以前的函数相比,语义相似,但又有一些细微的不同特性。通常你可以识别这些新的函数,因为它们的名字与早期的函数名非常相似(象CreateWindow/CreateWindowEx, CreateTypeLib/CreateTypeLib2, 以及我个人非常喜爱的函数: CreatePen /CreatePenIndirect/ExtCreatePen)。
所有这些问题给程序开发人员一个印象,那就是在.Net平台上WINDOWS(r)的开发是比较困难的。微软听取了开发人员的要求, 建立了一个完全面向对象的平台。现在可以将平台服务划分为独立的命名空间(namespaces)。(例如System.Collections, System.Data, System.IO, System.Security, System.Web等等 ),每个命名空间包含一系列相关的允许访问平台服务的类。
既然类的方法可以重用(overloaded),那些在功能上差不多的方法可以使用相同的名字,它们通过各自不同的原型来区分。例如一个类可以提供3种不同版本的 CreatePen 方法。所有的方法都实现一样的功能:创造一个画笔对象。但是,每种方法使用的参数不一样,在功能上有细小的区别。将来如果微软需要创造出第四种CreatePen方法,这种新的方法将如同一流的居民一样无缝的融入类中。
. 既然所有的平台服务都通过这样的面向对象的范例来提供,软件开发人员应该理解面向对象的程序设计。这种面向对象的范例也可以应用到其他的功能上。例如,使用继承和多态,创造一种特殊版本的基类库类型,将变得非常容易。我再次强烈建议你们对于这些概念熟悉起来,因为它们对于微软.NET框架下的工作非常重要。

System.Object
在.NET中,所有的对象都是从System.Object继承的。这意味着下面两种类型的定义是一致的:(用C#表示)

class Jeff { 

???
}
and
class Jeff : System.Object {

???

}


既然所有对象类型都继承于System.Object ,所以每种类型的每个对象可以确保具备一定的最小限度的功能。类 System.Object 的可用的公共方法在图1中列出。

Figure 1 System.Object Public Methods 

Method

Description

Equals

This virtual method returns true if two objects have the same value. Almost every type should override this method. For more information about this, see the section "Casting Between Data Types."

GetHashCode

This virtual method returns a hash code for this object's value. A type should override this method if its objects are to be used as a key in a hashtable. The method should provide a good distribution for its objects.

GetType

This nonvirtual method returns a Type-derived object that identifies the type of this object. The returned Type object can be used with the Reflection classes to obtain metadata information about the type.

ToString

By default, this virtual method returns the full name of the type. However, it is common to override this method so that it returns a String object containing a string representation of the object's state.




通用语言运行要求所有的对象实例使用New运算符来创建(它会调用newobj IL 指令)。下面的代码显示如何创建一个Jeff类型的实例(该类型在前面定义过)

Jeff j = new Jeff("ConstructorParam1");

New运算符从系统管理的堆(managed heap)上给指定的类型分配需要的字节, 从而创建对象。它初始化对象的主要成员。每个对象还有一些额外的字节空间,它是通用语言运行(CLR)用来对对象进行管理的。例如对象的虚表指针(virtual table pointer)和同步块的引用。
在调用New的具体过程中,类的构造函数被调用, 指定的参数被传递给类的构造函数,(在前面的代码中所传递的参数是"ConstructorParam1")。注意,大多数语言要编译构造函数,这样它们就可以调用基类的构造函数。但是,在通用语言运行中不需要这么做。
在New操作符完成了所有前面我提到过的操作之后,它返回一个对新创建对象的引用。在上面的代码样例中,这个引用保存在变量j中,j的类型是Jeff。
顺便提一下,New操作符没有反操作符,也就是说,没有一种方法来显式地释放或破坏这个创建的对象。通用语言运行提供一个资源回收(garbage-collected)环境,监测那些再也不被使用或访问的对象,并且自动将这些对象释放。这一方面的内容,我会在将要发行的MSDN(r) Magazine中介绍。

数据类型的转换
在编程时,将一个对象从一种数据类型转换到另一种数据类型是非常普遍的操作。在这一节,我将介绍一些关于对象的数据类型转换的规则。 现在我们从下面一行代码开始:

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的异常(exception),如果这个异常没有被处理,程序将中止退出。
当第二行代码执行时, 通用语言运行会判断o引用的对象类型是否是一个Jeff类型 (或是任何Jeff类型的导出类型) 的对象, 如果是的话, 通用语言运行允许类型转换; 然而, 如果o引用的对象类型与Jeff毫无关系,或者是对Jeff基类的引用,通用语言运行将阻止这种不安全的转换,并产生InvalidCastException异常错误。
C#提供了另外一种方式-as操作符-来实现数据类型的转换:

Jeff j = new Jeff(); // 创建一个新的Jeff对象
System.Object o = j as System.Object; // 将j转换为Object类型
// o现在引用的是对象Jeff


as操作符将一个对象转换成指定的类型。但是,不同于正常的数据类型转换,as操作符将不会产生异常(exception)错误。取而代之的是,如果数据类型不能成功地转换, 结果将是NULL。当使用错误的类型转换结果时,会产生NullReferenceException异常错误. 下面的代码证明了这个概念。

System.Object o = new System.Object(); //创建一个新的Object对象
Jeff j = o as Jeff; //将o转换成类型Jeff的对象
// 转换失败: 没有出现异常错误,但是j设为NULL .

j.ToString(); // 对j的访问会产生一个NullReferenceException的异常错误

除了as操作符,C#还提供了is操作符。 is操作符检查对象实例是否和给定的数据类型一致,它的结果是False或True。 is操作符不会产生异常错误。

System.Object o = new System.Object();
System.Boolean b1 = (o is System.Object); // b1的值是 True
System.Boolean b2 = (o is Jeff); // b2的值是False

注意,如果引用的对象是空的,is操作符总是返回False,因为没有对象可以用来检查它的类型。
为了确信你已经理解了这些内容,假定下面两个类的定义存在。

class B {
int x;
}

class D : B {
int x;
}

现在,检查图2,看看哪些代码行可以成功编译并执行(ES),哪些会引起编译错误(CE),哪些会引起通用语言运行错误(RE)。
Figure 2 Data Type Casting Results 

Statement

ES

CE

RE

System.Object o1 = new System.Object();

Y

N

N

System.Object o2 = new B();

Y

N

N

System.Object o3 = new D();

Y

N

N

System.Object o4 = o3;

Y

N

N

B b1 = new B();

Y

N

N

B b2 = new D();

Y

N

N

D d1 = new D();

Y

N

N

B b3 = new System.Object();

N

Y

N

D d3 = new System.Object();

N

Y

N

B b3 = d1;

Y

N

N

D d2 = b2;

N

Y

N

D d4 = (D) d1;

Y

N

N

D d5 = (D) b2;

Y

N

N

D d6 = (D) b1;

N

N

Y

B b4 = (B) o1;

N

N

Y

B b5 = (D) b2;

Y

N

N




集合和命名空间(assemblies and namespaces)


一系列类型可以组成一个assembly(一组文件)并进行发布。在一个集合(assembly)里,存在着不同的命名空间. 对开发者来说,命名空间看起来就象是相关类型的逻辑组合。例如,基类库集合包括很多命名空间。System命名空间包括一些核心的底层类型,如Object, Byte,Int32,Exception,Math和 Delegate等等.而System.Collection命名空间包含这些类型:ArrayList, BitArray, Queue 和Stack。
对于编译器来说,命名空间仅仅是一种确保类型名称独一无二的简单方式,它通过由点分隔的标识符号实现, 这也使类型名称更长。 对编译器来说,在System命名空间中的类型Object实际上是一个System.Object类型。相似的,在System.Collection命名空间中的类型Queue实际上是一个System.Collections.Queue类型。
运行引擎对于命名空间一无所知。当你访问一个类型,通用语言运行只需要了解该类型的全名以及哪个集合包含了这个类型的定义,这样,通用语言运行就可以装入正确的集合,找到该类型,并进行操作。
程序员们通常想要最准确的方式表示他们的算法;使用类型的全名来引用每个类是非常不方便的。由于这个原因,很多程序开发语言提供了一种语句,告诉编译器在类型名前加上各种各样的前缀,直到匹配完成。 当用C#编码时,我通常把下面这一行放在我的源代码模块的顶端:

using System;

当我在我的代码中引用一个类型时,编译器需要确信该类型定义过,而且我的代码是以正确的方式访问该类型。如果编译器不能根据指定的名字找到类型,它就在类型名字前加上"System",再检查产生的名字是否同现存的类型相匹配。前面的代码行允许我们在代码中使用"Object",编译器将自动将名字扩展为System.Object。
在检查类型定义时,编译器需要知道哪个集合包含这个类型,这样集合信息和类型信息就可以发布到结果文件中。要获取集合信息,你一定要将定义了引用类型的集合传递给编译器。
你也许会想到,这种方案会有一些潜在的问题。为了程序开发的便利,你应该避免创建具有冲突名字的类型。但是,在某些情况下,这几乎是不可能的。.NET鼓励组件的重用。你的程序可能同时使用了微软创建的组件和我的公司创建的其它一些组件。这些公司的组件可能都提供了一种称为FooBar的类型---------微软的FooBar实现一种功能,而Richer的FooBar实现的功能完全不一样。在这种情况下,你没有类型的命名控制权。要引用微软的FooBar,你要使用Microsoft.FooBar。如果你要引用我们的FooBar,你应该使用Richter.FooBar。
在下面的代码中,对于FooBar的引用是不确定的。如果编译器在这儿报错,那还不错,但是事实上C#编译器是选取一种可能的FooBar类型,你只有在运行时才会发现问题:

using Microsoft;
using Richter;

class MyApp {
method void Hi() {
FooBar f = new FooBar(); //不确定,编译器选取一种可能的FooBar类型
}
}
为了避免这种不确定的引用,你一定要明确告诉编译器你要创建哪个类型的FooBar。

using Microsoft;
using Richter;

class MyApp {
method void Hi() {
Richter.FooBar f = new Richter.FooBar(); // 很清楚
}
}
using语句还有一种方式允许你给一个类型创建一个别名。如果你要使用一个命名空间中的一些类型,而又不愿意影响该命名空间的所有类型,那么这个方法是很便利的。下面的代码展示了如何使用这种方式来解决类型不确定的问题。

// 将 RichterFooBar 定义为 Richter.FooBar 的一个别名
using RichterFooBar = Richter.FooBar;

class MyApp {
method void Hi() {
RichterFooBar f = new RichterFooBar(); // 现在没有错误了
}
}
这些避免类型不确定的方法是很有用的,但是还是有一些情况它们无法解决。想象一下这样一种情况,澳大利亚回飞器械公司(ABC)和阿拉斯加船舶公司(ABC)都创建了一种称为BuyProduct的类型,它们在各自的集合中发布这个类型。很有可能这两个公司都创建了包含类型BuyProduct的命名空间ABC。对于任何试图开发一套用于购买回飞器械和船的软件的开发人员来说,如果他们的程序语言中不能给出一种编程方法来区分这两种集合(而不仅仅是区分命名空间),就会带来很多麻烦。
不幸的是,C#的using语句仅仅支持命名空间,而不提供任何方式区分集合。然而,在真实世界里,这种问题并不是经常出现的,因此,它还不算是个严重的问题。如果你想设计让第三方也能使用的组件类型,你应该把这些类型定义在一个命名空间里,这样编译器能够很容易地区分他们。事实上,你应该使用公司全名(不是缩写)作为最上层的命名空间的名字,减少名字冲突发生的可能性。你可以看到微软使用“Microsoft“的命名空间。
下面的代码演示如何创建一个命名空间:

namespace CompanyName { // CompanyName
class A { // CompanyName.A
class B { ... } // CompanyName.A.B
}

namespace X { // CompanyName.X
class C { ... } // CompanyName.X.C
}
}

注意,命名空间隐含定为公开的,你不能包含任何存取修饰符去改变这一属性。然而,你可以在一个命名空间中定义内部类型(不能字集合外使用)或公开类型(可以被任何集合访问)。命名空间仅仅表示一种逻辑上的限制, 访问和包装是通过将命名空间放到集合中实现的。
在我的下一章中,我将向大家介绍所有.NET程序员必须了解的各种不同的类型:原子类型,引用类型和值类型。对于每个.NET程序员来说, 深刻地理解值类型是非常重要的。

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