Programming for the New Platform
|
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