本文以及以后的系列文章将专门针对NET讨论各种编程问题我将假设你已经熟悉面向对象的编程概念每一篇文章的内容都聚焦在非选定的特定公共语言运行时编程主题上所有NET开发人员必须知道这些主题
当展示代码例子时我必须在支持NET CLR 的多种语言中选择一种我决定使用C#它是微软设计的一种新语言
真正的面向对象设计
对于使用Win SDK的编程人员来说对大多数操作系统特性的访问时通过一组从动态链接库输出的独立函数实现的这些独立的函数从诸如C这样的非面向对象语言中非常容易调用但对于一个新的开发人员来说要面对上千个表面上看来毫无关系的独立的函数是相当让人畏惧的更为困难的是许多函数名是以单词Get开始的(如GetCurrentProcess和GetStockObject)此外Win API已经历数年并且微软添加了新的函数这些新函数依旧的函数相比有相似的语义但提供的特性有些差异你常常能认出较新的函数因为它们的名字原来的函数名相似(象CreateWindow/CreateWindowExCreateTypeLib/CreateTypeLib以及我最喜欢的CreatePen/CreatePenIndirect/ExtCreatePen
所有这些问题都使程序员觉得Windows开发很难随着NET平台的出现温柔终于为叫苦不迭的开发人员提供了一个完全面向对象的开发平台平台服务现在被分成为单独的名字空间(如SystemCollectionsSystemDataSystemIOSystemSecuritySystemWeb等等)并且每一个名字空间包含一组允许访问平台服务的相关类
因为类方法可以重载行为差别不大的方法具有相同的名字并且只有从原型中才能看出差别来例如一个类可能提供三个不同版本的CreatePen方法所有方法都做相同的事情即创建一支笔但是每一个方法都有不同的参数集并且行为不太一样将来微软还要创建第四个CreatePen方法并且与前面的类方法配合默契
因为所有的平台服务都通过这种面向对象的方式来实现所以软件开发者应该对面向对象的编程有所理解面向对象的方法还带来了其它的一些特点如使用继承和多态性很容易创建专门版本的基类库类型我再次强烈建议要熟练掌握这些概念这对于使用微软的NET框架很重要
SystemObject在NET中每一个对象都是从SystemObject派生而来也就是说下面的两种类型定义(使用C#)是相同的
class Jeff {
}
和
class Jeff : SystemObject {
}
因为所有对象都是从SystemObject派生出来的从而可以保证每一个对象具有最小的功能集表一是SystemObject中的公共方法
公共语言运行时需要所有的对象都要用new操作符创建(调用newobj IL指令)下列代码示范了如何创建Jeff类型(已在前面声明)的对象实例
Jeff j = new Jeff(ConstructorParam)
new操作符根据指定的类型需要从堆中分配字节数来创建对象它初始化对象的开销成员每一个对象都会有一些公共语言运行时用来管理对象的附加字节如对象的许表指针以及对同步快的引用
调用类的构造函数时传递的参数在new语句中指定(例子中是串ConstructorParam)注意大多数语言会编译构造函数以便它们调用基类构造函数但这在公共语言运行时中是不需要的
在new实现了所有我所提到的操作后它返回新创建对象的引用在例子代码中这个引用被存储在变量j中它的类型是Jeff
另外new操作符没有配对操作(delete)即没有方法显式地释放或销毁对象公共语言运行时提供自动地探测的垃圾回收环境当对象不再被使用或不再被访问时自动地释放和销毁对象有关这个主题将在下次的讨论中提出
数据类型的强制转换
在编程过程当中对象从一个数据类型到另一个数据类型的强制类型转换是十分常见的在这一部分我将讨论对象的强制数据类型转换规则为此先看下列代码
SystemObject o = new Jeff(ConstructorParam)
先前的代码编译通过并正确执行是因为有一个隐含的强制类型转换new操作符返回Jeff的一个引用类型但o是一个SystemObject的引用类型因为所有的类型(包括Jeff类型)都能被强制转换为SystemObject隐含的强制类型转换是成功的但是如果执行下面的代码就会有编译器错误因为编译器不提供基类型到派生类型的强制类型转换
Jeff j = o
为了能通过编译必须插入如下的显式强制类型转换
Jeff j = (Jeff) o
现在就可以编译通过并成功执行
再来看另外一个例子
SystemObject o = new SystemObject()
Jeff j = (Jeff) o
第一行创建了一个SystemObject类型对象第二行代码试图将SystemObject引用类型转换为Jeff引用类型两行代码都能编译通过但是在执行的时候第二行代码产生一个InvalidCastException异常如果捕获不到这个异常将强制应用程序终止
当第二行代码执行时公共语言运行时查证o所指的对象就是Jeff类型对象(或任何Jeff派生类型)如果是则公共语言运行时允许强制类型转换否则如果o所指的对象与Jeff类型无关或是一个Jeff的基类则公共语言运行时会预防这种不安全的强制类型转换并产生InvalidCastException异常
C# 使用as操作符提供另一种方法来实现强制类型转换
Jeff j = new Jeff(); // 创建一个新的Jeff 对象
SystemObject o = j as SystemObject; // 强制转换 j 为一个SystemObject对象
// 现在o 指Jeff 对象
as操作符试图强制转换一个对象为指定的类型但与通常的强制转换不一样如果对象的类型强制转换不成功结果会是nullas操作符决不会掷出异常当引用有毛病的强制类型转换发生时将产生NullReferenceException异常下列代码示范了这种情况
SystemObject o = new SystemObject(); //创建一个新的Object 对象
Jeff j = o as Jeff; //强制转换 o 为一个Jeff对象
// 上面的强制转换失败不会有异常掷出而j会被置为null
jToString(); // 访问j时产生一个NullReferenceException 异常
除了as操作符以外C#还提供一个is操作符它检查是否一个对象实例与给定的类型兼容并判断结果是True或是FalseIs操作符不会产生异常
SystemObject o = new SystemObject();
SystemBoolean b = (o is SystemObject); // b 是 True
SystemBoolean b = (o is Jeff); // b 是 False
注意如果对象引用是nullis操作符总是返回False因为得不到对象来检查其类型
为了肯定你理解了刚才所说的内容假设下列两各类定义存在
class B {
int x;
}
class D : B {
int x;
}
现在参见图二看看哪一行代码通过编译并执行成功(ES)哪一行代码导致编译器错误(CE)哪一行代码导致公共语言运行时错误(RE)
集合与名字空间
类型集可以被分组成集合(一个或多个文件集)并且被展开在一个集合中可以只存在单独的名字空间对应用程序开发人员来说名字空间就像有关联的类型的逻辑分组例如基本类库集合包含许多名字空间System名字空间包括Object基类型ByteIntExceptionMath和Delegate之类的核心低级类型而SystemCollections名字空间包括的类型如AarryListBitAarryQueue和Stack
对于编译器来说名字空间只不过是名字较长的的类型名以及其唯一性是用句点分隔某些符号名来保证的对于编译器而言System名字空间中的Object类型只不过是用一个叫做SystemObject的类型来表示同样SystemCollections名字空间中的Queue类型简单地用标示符SystemCollectionsQueue来表示
运行时引擎不知道关于名字空间的任何信息当你访问一个类型时公共语言运行时只需要知道完整的类型名字以及哪一个集合包含这个类型的定义以便公共语言运行时能正确加载集合从而找到要访问的类型并处理之
编程人员通常都想用最简练的方法来表达算法但用完全限定名引用每一个类类型的话极其麻烦因此许多编程语言提供一条语句来指示编译器添加各种前缀到类型名直到实现一个匹配当用C#编程时我经常在源代码的最前面是用下面的语句
using System
当我在代码中引用一个类型时编译器需要保证这个类型被定义过并且我的代码要以正确的方式访问这个类型如果编译器不能找到指定的类型它试图将System添加到类型名并检查产生的类型名字是否与现存的类型名匹配前面的代码行允许我在代码中使用Object并且编译器将自动将名字展开为SystemObject我肯定你能轻松想象这样省去了多少键盘输入
当进行类型定义的检查时编译器必须知道哪一个集合包含了这个类型以便这个集合的信息和类型信息能被送到结果文件中为了获得集合信息你必须将定义了任何引用类型的集合传给编译器
正如你所设想的一样这种设计存在一些潜在的问题为了编程方便你应该避免创建名字沖突的类型但是在某些情况中它完全不可能NET鼓励组件重用你的应用程序可以利用Microsoft所创建的组件同时你也可以用Richter创建的另一个组件这些公司的组件可能都提供了一个叫做FooBar的类型Microsoft的FooBar所做的事情与Richter的FooBar所做的事情完全不同在这种情况下你无法控制类类型的命名为了引用Microsoft的FooBar你使用MicrosoftFooBar为了引用Richter的FooBar你使用RichterFooBar
在下列的代码中对FooBar的引用是不明确的编译器报告一个错误也就罢了但是实际上C#编译器挑选FooBar类型的一种可能的情况直到运行时你才能发现问题
using Microsoft;
using Richter;
class MyApp {
method void Hi() {
FooBar f = new FooBar(); // Ambiguous compiler picks
}
}
为了排除这种不明确的引用你必须显式地告诉编译器你想创建哪一个FooBar
using Microsoft;
using Richter;
class MyApp {
method void Hi() {
RichterFooBar f = new RichterFooBar(); // 明确引用
}
}
另一种语句形式是允许你为单独类型创建别名如果你只使用名字空间中的几种类型并且不想用所有的名字空间类型污染整个名字空间的话这种方法很方便下列代码示范了另一种解决类型不明确问题的方法
// 定义RichterFooBar 为RichterFooBar的别名
using RichterFooBar = RichterFooBar;
class MyApp {
method void Hi() {
RichterFooBar f = new RichterFooBar(); // 不会出错
}
}
这种方法对于消除类型歧义有用但不尽人意的地方仍然存在假设澳大利亚的飞镖(Boomerang)公司(简称ABC)和阿拉斯加的船舶(Boat)公司(也简称ABC)两家公司各自创建了一个类型可能两家公司都创建了叫做ABC的名字空间在名字空间中包含一个叫做BuyProduct的类型任何试图开发需购买飞镖和船舶应用程序的人将会陷入麻烦除非编程语言提供编程方法来区分两家公司的集合而不仅仅是两家公司的名字空间
不幸的是C#语言只支持名字空间并不提供任何方式来详细说明集合但实际上碰到这个问题的时候并不多属于罕见问题如果是设计希望第三方使用的组件类型推荐在一个名字空间中定义类型以便编译器轻松排除类型问题事实上应该使用公司全名(不是只取首字母)作为最高级名字空间名来降低沖突的可能性你能看到Microsoft使用Microsoft作为名字空间
在代码中写一个名字空间声明来创建名字空间是一件很简单的事情就像下面这样
namespace CompanyName { // CompanyName
class A { // CompanyNameA
class B { } // CompanyNameAB
}
namespace X { // CompanyNameX
class C { } // CompanyNameXC
}
}
注意名字空间是隐含的公共类型(public)不能通过任何访问修饰符改变这一点但可以在内部的名字空间中定义类型(不能在集合外面使用)或者在公共的名字空间中定义类型(能被任何集合访问)名字空间只表示逻辑上的限制策略可访问性和包装是通过将名字空间放入一个集合来完成的
下一次的讨论中我将阐述所有NET编程人员必须掌握的简单数据类型引用类型和数值类型对于每一个NET程序透彻理解数值类型是非常重要的