C#锐利体验

文章来源:MSDN http://www.microsoft.com/china/msdn/library/langtool/vcsharp/cornyfield.mspx?mfr=true

发布日期: 2/4/2002 | 更新日期: 6/22/2004

南京邮电学院 李建忠([email protected]

C# 语言是一门简单,现代,优雅,面向对象,类型安全,平台独立的一门新型组件编程语言。其语法风格源自C/C++家族,融合了Visual Basic的高效和C/C++强大,是微软为奠定其下一互联网霸主地位而打造的Microsoft.Net平台的主流语言。其一经推出便以其强大的操作能 力,优雅的语法风格,创新的语言特性,第一等的面向组件编程的支持而深受世界各地程序员的好评和喜爱。“它就是我多年来梦寐以求的计算机语言!”--很多 资深程序员拿到C#都是这样的惊讶。从C#语言的名字(C Sharp)我们也可见微软用其打造其下一代互联网络深度服务的勃勃雄心。C#语言目前已由微软提交欧洲计算机制造商协会ECMA,经过标准化后的C#将 可由任何厂商在任何平台上实现其开发工具及其支持软件,这为C#的发展提供了强大的驱动力,我们也可从这里看到微软前所未有的眼光和智慧。

组 件编程已经成为当今世界软件业面向下一代程序开发的一致选择,是90年代面向对象编程的深度发展。C#生逢其时,占尽天时地利,“第一等的面向组件编程的 支持”也决不是简单说说那么轻松。实际上,组件特性已经深深植入C#语言的各个层面,是为C#锐利(Sharp)之处。在下面的文章中笔者将从C#语言的 各个层面来展现C#语言中无处不见的组件特性,深度阐述C#面向组件编程。整个专题共分为十讲:“第一讲 ‘Hello,World!’程序”,“第二讲 C#语言基础介绍”,“第三讲 Microsoft.NET平台基础构造”,“第四讲 类与对象”,“第五讲 构造器与析构器”,“第六讲 方法”,“第七讲 域与属性”,“第八讲 索引器与操作符重载”,“第九讲 数组与字符串”,“第十讲 特征与映射”,“第十一讲 COM互操作 非托管编程与异常处理”,“第十二讲 用C#编织未来--C#编程模型概述”。

*
本页内容
第一讲 “Hello,World!”程序 第一讲 “Hello,World!”程序
第二讲 C#语言基础介绍 第二讲 C#语言基础介绍
第三讲 Microsoft.NET平台基础构造 第三讲 Microsoft.NET平台基础构造
第四讲 类与对象 第四讲 类与对象
第五讲 构造器与析构器 第五讲 构造器与析构器
第六讲 方法 第六讲 方法
第七讲 域与属性 第七讲 域与属性
第八讲 索引器与操作符重载 第八讲 索引器与操作符重载

第一讲 “Hello,World!”程序

“Hello World!”程序是程序员一直以来的一个浪漫约定,也是一个伟大的梦想--总有一天,出自人类之手的计算机会面对这个美丽的世界说一声“Hello World!”。它是学习一门新语言的一个很好的起点,我们就从这里开始,看下面例子:

//HelloWorld.cs by Cornfield,2001
//csc HelloWorld.cs
using System;
class HelloWorld
{
public static void Main()
{
Console.WriteLine("Hello World !");
}
}

我们可以打开Windows自带的简易的"记事本"程序来编写这段代码--笔者推荐刚开始采用这个极其简单却能把程序代码暴露的相当清 晰的编辑工具。我们将它的文件名保存为HelloWorld.cs,其中".cs"是C#源代码文件的扩展名。然后在配置好C#编译器的命令行环境里键入 "csc HelloWorld.cs"编译文件。可以看到编译输出文件HelloWorld.exe。我们键入HelloWorld执行这个文件可得到下面的输 出:

Hello World !

下面我们来仔细分析上面的代码和整个程序的编译输出及执行过程。先看文件开始的两行代码,这是C#语言的单行注释语句。和C++语言类似,C#支持两种注释方法:以"//"开始的单行注释和以"/*","*/"配对使用的多行注释。注释之间不能嵌套。

再 来看下面的"using System;"语句,这是C#语言的using命名空间指示符,这里的"System"是Microsoft.NET系统提供的类库。C#语言没有自己 的语言类库,它直接获取Microsoft.NET系统类库。Microsoft.NET类库为我们的编程提供了非常强大的通用功能。该语句使得我们可以 用简短的别名"Console"来代替类型"System.Console"。当然using指示符并不是必须的,我们可以用类型的全局名字来获取类型。 实际上,using语句采用与否根本不会对C#编译输出的程序有任何影响,它仅仅是简化了较长的命名空间的类型引用方式。

接着我们声明 并实现了一个含有静态Main()函数的HelloWorld类。C#所有的声明和实现都要放在同一个文件里,不像C++那样可以将两者分离。Main ()函数在C#里非常特殊,它是编译器规定的所有可执行程序的入口点。由于其特殊性,对Main()函数我们有以下几条准则:

1.

Main()函数必须封装在类或结构里来提供可执行程序的入口点。C#采用了完全的面向对象的编程方式,C#中不可以有像C++那样的全局函数。

2.

Main()函数必须为静态函数(static)。这允许C#不必创建实例对象即可运行程序。

3.

Main()函数保护级别没有特殊要求, public,protected,private等都可,但一般我们都指定其为public。

4.

Main()函数名的第一个字母要大写,否则将不具有入口点的语义。C#是大小写敏感的语言。

5.

Main ()函数的参数只有两种参数形式:无参数和string 数组表示的命令行参数,即static void Main()或static void Main(string[]args) ,后者接受命令行参数。一个C#程序中只能有一个Main()函数入口点。其他形式的参数不具有入口点语义,C#不推荐通过其他参数形式重载Main() 函数,这会引起编译警告。

6.

Main()函数返回值只能为void(无类型)或int(整数类型)。其他形式的返回值不具有入口点语义。

我 们再来看"HelloWorld.cs"程序中Main()函数的内部实现。前面提过,Console是在命名空间System下的一个类,它表示我们通 常打交道的控制台。而我们这里是调用其静态方法WriteLine()。如同C++一样,静态方法允许我们直接作用于类而非实例对象。WriteLine ()函数接受字符串类型的参数"Hello World !",并把它送入控制台显示。如前所述,C#没有自己的语言类库,它直接获取Microsoft.NET系统类库。我们这里正是通过获取 Microsoft.NET系统类库中的System.Console.WriteLine()来完成我们想要的控制台输出操作。这样我们便完成了 "Hello World!"程序。

但事情远没那么简单!在我们编译输出执行程序的同时,Microsoft.NET底层的诸多机制却 在暗地里涌动,要想体验C#的锐利,我们没有理由忽视其背靠的Microsoft.NET平台。实际上如果没有Microsoft.NET平台,我们很难 再说C#有何锐利之处。我们先来看我们对"HelloWorld.cs"文件用csc.exe命令编译后发生了什么。是的,我们得到了 HelloWorld.exe文件。但那仅仅是事情的表象,实际上那个HelloWorld.exe根本不是一个可执行文件!那它是什么?又为什么能够执 行?

好的,下面正是回答这些问题的地方。首先,编译输出的HelloWorld.exe是一个由中间语言(IL),元数据 (Metadata)和一个额外的被编译器添加的目标平台的标准可执行文件头(比如Win32平台就是加了一个标准Win32可执行文件头)组成的PE (portable executable,可移植执行体)文件,而不是传统的二进制可执行文件--虽然他们有着相同的扩展名。中间语言是一组独立于CPU的指令集,它可以被 即时编译器Jitter翻译成目标平台的本地代码。中间语言代码使得所有Microsoft.NET平台的高级语言C#,VB.NET,VC.NET等得 以平台独立,以及语言之间实现互操作。元数据是一个内嵌于PE文件的表的集合。元数据描述了代码中的数据类型等一些通用语言运行时(Common Language Runtime)需要在代码执行时知道的信息。元数据使得.NET应用程序代码具备自描述特性,提供了类型安全保障,这在以前需要额外的类型库或接口定义 语言(Interface Definition Language,简称IDL)。

这样的解释可能还是有点让人困惑,那么我们来实际 的解剖一下这个PE文件。我们采用的工具是.NET SDK Beta2自带的ildasm.exe,它可以帮助我们提取PE文件中的有关数据。我们键入命令"ildasm /output:HelloWorld.il HelloWorld.exe",一般可以得到两个输出文件:helloworld.il和helloworld.res。其中后者是提取的资源文件,我 们暂且不管,我们来看helloworld.il文件。我们用"记事本"程序打开可以看到元数据和中间语言(IL)代码,由于篇幅关系,我们只将其中的中 间语言代码提取出来列于下面,有关元数据的表项我们暂且不谈:

class private auto ansi beforefieldinit HelloWorld
extends [mscorlib]System.Object
{
.method public hidebysig static void Main() cil managed
{
.entrypoint
// Code size 11 (0xb)
.maxstack 8
IL_0000: ldstr "Hello World !"
IL_0005: call void [mscorlib]System.Console::WriteLine(string)
IL_000a: ret
} // end of method HelloWorld::Main
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method HelloWorld::.ctor
} // end of class HelloWorld

我们粗略的感受是它很类似于早先的汇编语言,但它具有了对象定义和操作的功能。我们可以看到它定义并实现了一个继承自 System.Object 的HelloWorld类及两个函数:Main()和.ctor()。其中.ctor()是HelloWorld类的构造函数,可在 "HelloWorld.cs"源代码中我们并没有定义构造函数呀--是的,我们没有定义构造函数,但C#的编译器为我们添加了它。你还可以看到C#编译 器也强制HelloWorld类继承System.Object类,虽然这个我们也没有指定。关于这些高级话题我们将在以后的讲座中予以剖析。

那么PE文件是怎么执行的呢?下面是一个典型的C#/.NET应用程序的执行过程:

1.

用户执行编译器输出的应用程序(PE文件),操作系统载入PE文件,以及其他的DLL(.NET动态连接库)。

2.

操作系统装载器根据前面PE文件中的可执行文件头跳转到程序的入口点。显然,操作系统并不能执行中间语言,该入口点也被设计为跳转到mscoree.dll(.NET平台的核心支持DLL)的_ CorExeMain()函数入口。

3.

CorExeMain()函数开始执行PE文件中的中间语言代码。这里的执行的意思是通用语言运行时按照调用的对象方法为单位,用即时编译器将中间语言编译成本地机二进制代码,执行并根据需要存于机器缓存。

4.

程序的执行过程中,垃圾收集器负责内存的分配,释放等管理功能。

5.

程序执行完毕,操作系统卸载应用程序。

清 楚的知晓编译输出的PE文件的执行过程是深度掌握C#语言编程的关键,这种过程的本身就诠释着C#语言的高级内核机制以及其背后 Microsoft.NET平台种种诡秘的性质。一个"Hello World !"程序的概括力已经足够,在我们对C#语言有了一个很好的起点之后,下面的专题会和大家一起领略C#基础语言,窥探Microsoft.NET平台构 造,步步体验C#锐利编程的极乐世界,Let's go!

第二讲 C#语言基础介绍

在体验C#的锐利之前,关乎语言基本知识的掌握是必不可少的一环。由于C#基本语言很多源自C/C++,在这里对那些和C/C++类似的地方仅作简单介绍,我们将体验专注于那些区别于传统C/C++的关键的语言基础知识。

数据类型

C# 语言的数据类型主要分为两类:值类型和引用类型。另外一种数据类型"指针"是为unsafe上下文编程专门设定的,其中unsafe上下文指对代码进行 unsafe标示以满足利用指针对内存直接进行操作要求的C#非托管代码,这些代码将失去Microsoft.NET平台的垃圾收集等CLR性质,我们放 在"COM互操作 非托管编程与异常处理"专题里阐述。值类型的变量本身包含他们的数据,而引用类型的变量包含的是指向包含数据的内存块的引用或者叫句柄。从下面这幅图中可 以清晰地看出两者的差别:

引用类型带来的可能的问题便是当多个变量引用同样的内存块时,对任何一个引用变量的修改都会导致该对象的值的改变。null值表示引用类型没有对任何实际地址进行引用。

值 类型可分为结构类型和枚举类型。结构类型包括简单类型和用户自定义结构类型。枚举类型和用户自定义结构类型我们将在"第九讲 结构,枚举,数组与字符串"专题里详细阐述。简单类型又可分为布尔类型和数值类型。C#语言中布尔类型严格与数值类型区分,只有true和false两种 取值,不存在像C/C++里那样和其他类型之间的转换。数值类型包括整值,浮点和decimal三种类型。整值类型有sbyte,byte,short, ushort,int,uint,long,ulong,char共九种。除了char类型外,其他8种两两一组分别为有符号和无符号两种。浮点值有 float和double两种。decimal主要用于金融,货币等对精度要求比较高的计算环境。下表是对这些简单类型的一个详细的描述:

简单类型 描 述 示 例

sbyte

8-bit 有符号整数

sbyte val = 12;

short

16-bit 有符号整数

short val = 12;

int

32-bit有符号整数

int val = 12;

long

64-bit有符号整数

long val1 = 12; long val2 = 34L;

byte

8-bit无符号整数

byte val1 = 12; byte val2 = 34U;

ushort

16-bit 无符号整数

ushort val1 = 12; ushort val2 = 34U;

uint

32-bit 无符号整数

uint val1 = 12; uint val2 = 34U;

ulong

64-bit 无符号整数

ulong val1 = 12; ulong val2 = 34U; ulong val3 = 56L; ulong val4 = 78UL;

float

32-bit单精度浮点数

float val = 1.23F;

double

64-bit双精度浮点数

double val1 = 1.23; double val2 = 4.56D;

bool

布尔类型

bool val1 = true; bool val2 = false;

char

字符类型 ,Unicode 编码

char val = 'h';

decimal

28个有效数字的128-bit十进制类型

decimal val = 1.23M;

引 用类型共分四种类型:类,接口,数组,委派。类除了我们可以定义自己的类型外,又包括两个比较特殊的类型object和string。object是C# 中所有类型(包括所有的值类型和引用类型)的继承的根类。string类型是一个密封类型(不能被继承),其实例表示Unicode字符串,它和数组类型 我们将放在"第九讲 结构,枚举,数组与字符串"中详述。接口类型定义一个方法的合同,我们将在"第七讲 接口 继承与多态"中讲述。委派类型是一个指向静态或实例方法的签名,类似于C/C++中的函数指针,将在"第八讲 委派与事件"中讲述。实际上我们将从后面的专题中看到这些类型都是类的某种形式的包装。

每种数据类型都有对应的缺省值。数值类型的缺省 值为0或0.0,其中char的缺省为'\x0000'。布尔类型的缺省值为false。枚举类型的缺省值为0。结构类型的缺省值是将它所有的值类型的域 设置为对应值类型的缺省值,将其引用类型的域设置为null。所有引用类型的缺省值为null。

不同类型的数据之间可以转换,C#的类 型转换有隐含转换,明晰转换,标准转换,自定义转换共四种方式。隐含转换与明晰转换和C++里一样,数据从"小类型"到"大类型"的转换时为隐含转换,从 "大类型"到"小类型"的转换为明晰转换,明晰转换需要如"(Type)data"一般的括号转换操作符。标准转换和自定义转换是针对系统内建转换和用户 定义的转换而言的,两者都是对类或结构这样的自定义类型而言的。

变量与常量

变量表示存储位置,变量必须有确定的数据类型。C#的类型安全的含义之一就是确保变量的存储位置容纳着合适的类型。可以将C#中的变量分为静态变量,实例变量,传值参数,引用参数,输出参数,数组参数和本地变量共七种。本地变量则是在方法体内的临时变量。

静 态变量和实例变量主要是针对类或结构内的数据成员(又叫域)而言的。静态变量在它寄存的类或结构类型被装载后得到存储空间,如果没有对它进行初始化赋值, 静态变量的初始值将是它的类型所持有的缺省值。实例变量在它的类实例被创建后获得存储空间,如果没有经过初始化赋值,它的初始值与静态变量的定义相同。两 者更详细的说明我们放在"第六讲 域 方法 属性与索引器"专题里。

传值参数,引用参数,输出参数,数组参数主要针对方法的参数类型而 言的。简单的讲传值参数是对变量的值的一种传递,方法内对变量的改变在方法体外不起作用。对于传值参数本身是引用型的变量稍有不同,方法内对该引用(句 柄)变量指向的数据成员即实际内存块的改变将在方法体外仍然保留改变,但对于引用(句柄)本身的改变不起作用。引用参数是对变量的句柄的一种传递,方法内 对该变量的任何改变都将在方法体外保留。输出参数是C#专门为有多个返回值的方法而量身定做的,它类似于引用变量,但可以在进入方法体之前不进行初始化, 而其他的参数在进入方法体内C#都要求明确的初始化。数组参数是为传递大量的数组元素而专门设计的,它从本质上讲是一种引用型变量的传值参数。它们更详细 的阐述我们也放在"第六讲 域 方法 属性与索引器"专题里。

本地变量严格的讲是在C#的块语句,for语句,switch语句,using语句内声明的变量,它的生命周期严格地被限制在这些语句块内部。

常量在编译时便确定它的值,在整个程序中也不许修改。常量声明的同时必须赋值。由于它的编译时确定值的特性,引用类型可能的值只能为string和null(除string外,引用类型的构建器必须在运行时才能确定引用类型的值)。

操作符与表达式

C# 保留了C++所有的操作符,其中指针操作符(*和->)与引用操作符(&)需要有unsafe的上下文。C#摈弃了范围辨析操作符 (::),一律改为单点操作符(.)。我们不再阐述那些保留的C++的操作符,这里主要介绍C#引入的具有特殊意义的几个操作符:as,is,new, typeof,sizeof,stackalloc。

as操作符用于执行兼容类型之间的转换,当转换失败时,as 操作符结果为null。is 操作符用于检查对象的运行时类型是否与给定类型兼容,当表达式非null且可以转化为指定类型时,is操作符结果为true,否则为false。as和 is操作符是基于同样的类型鉴别和转换而设计的,两者有相似的应用场合。实际上expression as type相当于expression is type ? (type)expression : (type)null。

作为操作符的new用于在堆上创建对象和调用构造函数, 值得注意的是值类型对象(例如结构)是在堆栈上创建的,而引用类型对象(例如类)是在堆上创建的。new也用于修饰符,用于隐藏基类成员的继承成员。为隐 藏继承的成员,使用相同名称在派生类中声明该成员并用 new 修饰符修改它。typeof 运算符用于获得某一类型的 System.Type 对象,我们将在"第十讲 特征与映射"里结合Microsoft.NET的类型系统对它作详细的阐述。sizeof 运算符用于获得值类型(不适用于引用类型)的大小(以字节为单位)。stackalloc用于在堆栈上分配内存块, 仅在局部变量的初始值设定项中有效,类似于C/C++语言的_alloca。sizeof和statckalloc都由于涉及内存的直接操作而需要 unsafe上下文。

C#里的某些操作符可以像C++里那样被重载。操作符重载使得自定义类型(类或结构)可以用简单的操作符来方便的表达某些常用的操作。

为完成一个计算结果的一系列操作符和操作数的组合称为表达式。和C++一样,C#的表达式可以分为赋值表达式和布尔表达式两种,C#没有引入新的表达式形式,我们对此不再赘述。

命名空间与语句

C# 采用命名空间(namespace)来组织程序。命名空间可以嵌套。using指示符可以用来简化命名空间类型的引用。using指示符有两种用法。 "using System;"语句可以使我们用简短的类型名"Console"来代替类型"System.Console"。"using Output = System.Console;"语句可以使我们用别名"Output"来代替类型"System.Console"。命名空间的引入大大简化了C#程序 的组织方式。

C#语句可以分为标号语句,声明语句,块语句,空语句,表达式语句,选择语句,反复语句,跳转语句,try语句,checked/unchecked语句,lock语句,using语句。

标 号语句主要为goto跳转设计,C#不允许跨方法的跳转,但允许小规模的方法内的跳转。声明语句可以同时进行初始化赋值,对象的实例化声明需要new关键 字。块语句采用"{"和"}"定义语句块,主要是界定局部变量的作用范围。空语句在C#中用分号";"表示,没有执行语义。表达式语句通过表达式构成语 句。

选择语句有if语句和switch语句两种,与C++别无二致。反复语句除了while,do,for三种循环结构外引入了foreach语句用于遍历集合中所有的元素,但这需要特定的接口支持,我们在后面的章节里对之作详细阐述。

跳转语句有break,continue,goto,return,throw五种语句,前四种与C++里的语义相同,throw语句与后面的try语句我们将在"第十一讲 COM互操作 非托管编程与异常处理"阐述。

checked/unchecked语句主要用于数值运算中溢出检查的上下文。lock语句主要用于线程信号量的锁控制。using语句主要用于片断资源管理。这些我们在后续章节里都会有具体的涉及。

第三讲 Microsoft.NET平台基础构造

抛 开Microsoft.NET平台去谈C#是没有意义的,C#之“Sharp”也正在其后端强大的平台。仅仅拘泥于语法层面是体验不了C#的锐利之处的, C#程序很多诡秘之处必须依靠Microsoft.NET平台才能深度的掌握和运用。简单的讲,Microsoft.NET平台是一个建立在开放互联网络 协议和标准之上,采用新的工具和服务来满足人们的计算和通信需求的革命性的新型XML Web智能计算服务平台。它允许应用程序在因特网上方便快捷地互相通信,而不必关心使用何种操作系统和编程语言。

从技术层面具体来说, Microsoft.NET平台主要包括两个内核,即通用语言运行时(Common Language Runtime,简称CLR)和Microsoft.NET框架类库,它们为Microsoft.NET平台的实现提供了底层技术支持。通用语言运行时是 建立在操作系统最底层的服务,为Microsoft.NET平台的执行引擎。Microsoft.NET框架包括一套可被用于任何编程语言的类库,其目的 是使得程序员更容易地建立基于网络的应用和服务。在此之上是许多应用程序模板,这些模板为开发网络应用和服务提供高级的组件和服务。 Microsoft.NET平台之浩瀚绝非这里的几千字能够廓清,我们下面将着重体验那些对我们用C#开发应用程序至关重要的平台基础构造。

通用语言运行时(CLR)

通 用语言运行时是整个Microsoft.NET框架赖以建构的基础,它为Microsoft.NET应用程序提供了一个托管的代码执行环境。它实际上是驻 留在内存里的一段代理代码,负责应用程序在整个执行期间的代码管理工作,比较典型的有:内存管理,线程管理,安全管理,远程管理,即时编译,代码强制安全 类型检查等。这些都可称得上Microsoft.NET框架的生命线。

实际上我们可以看出来,CLR代理了一部分传统操作系统的管理功 能。在CLR下的代码称之为托管代码,否则称为非托管代码。我们也可将CLR看作一个技术规范,无论程序使用什么语言编写,只要能编译成微软中间语言 (MSIL),就可以在它的支持下运行,这使得应用程序得以独立于语言。目前支持CLR的编程语言多达二三十种。微软中间语言是我们在 Microsoft.NET平台下编译器输出的PE文件的语言。它是Microsoft.NET平台最完整的语言集,非常类似于PC机上的汇编语言。即时 编译器在运行时将中间语言编译成本地二进制代码。它为Microsoft.NET平台提供了多语言的底层技术支持。另外根据需要, Microsoft.NET即时编译器提供了特殊情况下的经济型即时编译和安装时编译技术。

CLR的设计目的便是直接在应用程序运行环 境中为基于组件的编程提供第一等的支持。正如在Windows中添加了对窗口、控件、图形和菜单的直接支持,为基于消息的编程添加了底层结构,为支持设备 无关性添加了抽象内容一样,CLR直接支持组件(包括属性和事件)、对象、继承性、多态性和接口。对属性和事件的直接支持使得基于组件的编程变得更简单, 而不需要特殊的接口和适配设计模式。在组件运行时,CLR负责管理内存分配、启动和中止线程和进程、强化安全系数,同时还调整任何该组件涉及到的其他组件 的附属配置。序列化支持允许以多种格式操作存储在磁盘上的组件,包括基于业界标准XML的SOAP。CLR提供了处理错误条件的有力、协调的方式。每个模 块都具有内置的完整的元数据,这意味着诸如动态创建和方法调用之类的功能更容易,也更安全。映射甚至允许我们灵活地创建和执行代码。我们可以控制应用程序 使用的组件的版本,这使应用程序更加可靠。组件代码是与处理器无关的和易于验证的中间语言 ( IL),而不是某一种特定的机器语言,这意味着组件不但可以在多种计算机上运行,而且可以确保组件不会覆盖它们不使用的内存,也不会潜在地导致系统崩溃。 CLR根据托管组件的来源(例如来自因特网,企业局域网,本地机)等因素对他们判定以适当的信任度,这样CLR会根据他们的信任度来限定他们执行如读取文 件,修改注册表等某些敏感操作的权限。借助通用类型系统(Common Type System,简称CTS)对代码类型进行严格的安全检查避免了不同组件之间可能存在的类型不匹配的问题。CLR下的编程全部是围绕组件进行的。

值得指出的是CLR通常寄宿在其他高性能的服务器应用程序中,比如:因特网信息服务器(IIS),Microsoft SQL Server。这使得我们可以充分利用通用语言运行时诸多的安全,高效的优点来部署自己的商业逻辑。

内存管理

CLR 对程序员影响最大的就是它的内存管理功能,以至于我们很有必要单独把它列出来阐述。它为应用程序提供了高性能的垃圾收集环境。垃圾收集器自动追踪应用程序 操作的对象,程序员再也用不着和复杂的内存管理打交道。这在某些喜欢张口闭口底层编程的所谓的高手来说,自动内存管理从来都是他们嘲笑的对象。的确,为通 用软件环境设计的自动化内存管理器永远都抵不上自己为特定程序量身订制的手工制作。但现代软件业早已不再是几百行代码的作坊作业,动辄成千上万行的代码, 大量的商业逻辑凸现的已不再是算法的灵巧,而是可管理性,可维护性的工程代码。.NET/C#不是为那样的作坊高手准备的,C语言才是他们的尤物。在 Microsoft.NET托管环境下,CLR负责处理对象的内存布局,管理对象的引用,释放系统不再使用的内存(自动垃圾收集)。这从根本上解决了长期 以来困扰软件的内存泄漏和无效内存引用问题,大大减轻了程序员的开发负担,提高了程序的健壮性。实际上我们在托管环境下根本找不到关于内存操作或释放的语 言指令。值得指出的是Microsoft.NET应用程序可以使用托管数据,也可以使用非托管数据,但CLR并不能判断托管数据与非托管数据。

垃 圾收集器负责管理.NET应用程序内存的分配和释放。当用new操作符创建新的对象时,垃圾收集器在托管堆(Managed Heap)中为对象分配内存资源。只要托管堆内的内存空间可用,垃圾收集器就为每一个新创建的对象分配内存。当应用程序不再持有某个对象的引用,垃圾收集 器将会探测到并释放该对象。值得注意的是垃圾收集器并不是在对象引用无效时就立即开始释放工作,而是根据一定算法来决定什么时候进行收集和对什么对象进行 收集。任何一个机器的内存资源总是有限的,当托管堆内的内存空间不够用时,垃圾收集器启动收集线程来释放系统内存。垃圾收集器根据对象的存活时间,对象历 经的收集次数等来决定对哪些对象的内存进行释放。宏观的看,我们并不知道垃圾收集的确切行为,但Microsoft.NET类库为我们提供了控制垃圾收集 行为的部分功能,在某些特殊情况下,我们有必要进行一些受限的操作。

垃圾收集器并不意味着程序员从此可以一劳永逸,如果正在操作一个包装了如文件,网络连接,Windows句柄,位图等底层操作系统资源的对象,我们还是需要明确地释放这些非托管资源的。这在“第五讲 构造器与析构器”里有详细的阐述。

Microsoft.NET框架类库

Microsoft.NET 框架类库是一组广泛的,面向对象的可重用类的集合,为应用程序提供各种高级的组件和服务。它将程序员从繁重的编程细节中解放出来专注于程序的商业逻辑,为 应用程序提供各种开发支持--不管是传统的命令行程序还是Windows图形界面程序,拟或是面向下一代因特网分布式计算平台的ASP.NET或XML Web服务。下面是对这些组件和服务的一个概括。

系统框架服务

服务框架包括一套开发人员希望在标准语言库中存在的基类库,例如:集合、输入/输出,字符串及数据类。另外,基类库提供访问操作系统服务如图画、网络、线程、全球化和加密的类。服务框架也包括数据访问类库,及开发工具,如调试和剖析服务,能够使用的类。

ADO.NET组件

ADO.NET为基于网络的可扩展的应用程序和服务提供数据访问服务。ADO.NET不仅支持传统的基于连接指针风格的数据访问,同时也为更适合于把数据返回到客户端应用程序的无连接的数据模板提供高性能的访问支持。

XML数据组件

所有的数据都可被看作XML,开发人员可以通过XML为任何数据使用转换,传输和确认服务。系统框架对XML数据提供第一等的操作支持。系统也支持ADO.NET数据与XML数据之间的通用转换。

Windows表单组件

Windows表单组件为开发人员提供了强大的Windows应用程序模型和丰富的Windows用户接口,包括传统的ActiveX控件和Windows XP的新界面,如透明的、分层的、浮动窗口。对设计时的强大支持也是Windows表单组件令人兴奋的地方。

ASP.NET应用服务

ASP.NET的核心是高性能的用于处理基于低级结构的HTTP请求的运行语言。编译运行方式大大提高了它的性能。ASP.NET使用基于构件的Microsoft .NET框架配制模板,因此它获得了如XCOPY配制、构件并行配制、基于XML配制等优点。它支持应用程序的实时更新,提供高速缓冲服务改善性能。

ASP.NET Web表单

ASP.NET Web表单把基于VB的表单的高生产性的优点带到了网络应用程序的开发中来。ASP.NET Web表单支持传统的将HTML内容与角本代码混合的ASP语法,但是它提出了一种将应用程序代码和用户接口内容分离的更加结构化的方法。ASP.NET提供了一套映射传统的HTML用户接口部件(包括列表框,文本框和按钮)的ASP.NET Web表单控件和一套更加复杂强大的网络应用控件(如日历和广告转板)。

XML Web服务

ASP.NET应用服务体系架构为用ASP.NET建立XML Web服务提供了一个高级的可编程模板。虽然建立XML Web服务并不限定使用特定的服务平台,但是它提供许多的优点将简化开发过程。使用这个编程模型,开发人员甚至不需要理解HTTP、SOAP或其它任何网络服务规范。 ASP.NET XML Web服务为在Internet上绑定应用程序提供了一个利用现存体系架构和应用程序的简单的、灵活的、基于产业标准的模型。

第四讲 类与对象

组件编程不是对传统面向对象的抛弃,相反组件编程正是面向对象编程的深化和发展。类作为面向对象的灵魂在C#语言里有着相当广泛深入的应用,很多非常“Sharp”的组件特性甚至都是直接由类包装而成。对类的深度掌握自然是我们“Sharp XP”重要的一环。

C# 的类是一种对包括数据成员,函数成员和嵌套类型进行封装的数据结构。其中数据成员可以是常量,域。函数成员可以是方法,属性,索引器,事件,操作符,实例 构建器,静态构建器,析构器。我们将在“第五讲 构造器与析构器”和“第六讲 域 方法 属性与索引器”对这些成员及其特性作详细的剖析。除了某些导入的外部方法,类及其成员在C#中的声明和实现通常要放在一起。

C#用多种修饰符来表达类的不同性质。根据其保护级C#的类有五种不同的限制修饰符:

1.

public可以被任意存取;

2.

protected只可以被本类和其继承子类存取;

3.

internal只可以被本组合体(Assembly)内所有的类存取,组合体是C#语言中类被组合后的逻辑单位和物理单位,其编译后的文件扩展名往往是“.DLL”或“.EXE”。

4.

protected internal唯一的一种组合限制修饰符,它只可以被本组合体内所有的类和这些类的继承子类所存取。

5.

private只可以被本类所存取。

如果不是嵌套的类,命名空间或编译单元内的类只有public和internal两种修饰。

new修饰符只能用于嵌套的类,表示对继承父类同名类型的隐藏。

abstract用来修饰抽象类,表示该类只能作为父类被用于继承,而不能进行对象实例化。抽象类可以包含抽象的成员,但这并非必须。abstract不能和new同时用。下面是抽象类用法的伪码:

abstract class A
{
public abstract void F();
}
abstract class B: A
{
public void G() {}
}
class C: B
{
public override void F()
{
//方法F的实现
}
}

抽象类A内含一个抽象方法F(),它不能被实例化。类B继承自类A,其内包含了一个实例方法G(),但并没有实现抽象方法F(),所以仍然必须声明为抽象类。类C继承自类B,实现类抽象方法F(),于是可以进行对象实例化。

sealed用来修饰类为密封类,阻止该类被继承。同时对一个类作abstract和sealed的修饰是没有意义的,也是被禁止的。

对象与this关键字

类 与对象的区分对我们把握OO编程至关重要。我们说类是对其成员的一种封装,但类的封装设计仅仅是我们编程的第一步,对类进行对象实例化,并在其数据成员上 实施操作才是我们完成现实任务的根本。实例化对象采用MyClass myObject=new MyClass()语法,这里的new语义将调用相应的构建器。C#所有的对象都将创建在托管堆上。实例化后的类型我们称之为对象,其核心特征便是拥有了 一份自己特有的数据成员拷贝。这些为特有的对象所持有的数据成员我们称之为实例成员。相反那些不为特有的对象所持有的数据成员我们称之为静态成员,在类中 用static修饰符声明。仅对静态数据成员实施操作的称为静态函数成员。C#中静态数据成员和函数成员只能通过类名引用获取,看下面的代码:

using System;
class A
{
public int count;
public void F()
{
Console.WriteLine(this.count);
}
public static string name;
public static void G()
{
Console.WriteLine(name);
}
}
class Test
{
public static void Main()
{
A a1=new A();
A a2=new A();
a1.F();
a1.count=1;
a2.F();
a2.count=2;
A.name="CCW";
A.G();
}
}

我们声明了两个A对象a1,a2。对于实例成员count和F(),我们只能通过a1,a2引用。对于静态成员name和G()我们只能通过类型A来引用,而不可以这样a1.name,或a1.G()。

在 上面的程序中,我们看到在实例方法F()中我们才用this来引用变量count。这里的this是什么意思呢?this 关键字引用当前对象实例的成员。在实例方法体内我们也可以省略this,直接引用count,实际上两者的语义相同。理所当然的,静态成员函数没有 this 指针。this 关键字一般用于从构造函数、实例方法和实例访问器中访问成员。

在构造函数中this用于限定被相同的名称隐藏的成员,例如:

class Employee
{
public Employee(string name, string alias)
{
this.name = name;
this.alias = alias;
}
}

将对象作为参数传递到其他方法时也要用this表达,例如:

CalcTax(this);

声明索引器时this更是不可或缺,例如:

public int this [int param]
{
get
{
return array[param];
}
set
{
array[param] = value;
}
}

System.Object类

C#中所有的类都直接或间接继承自System.Object类,这使得C#中的类得 以单根继承。如果我们没有明确指定继承类,编译器缺省认为该类继承自System.Object类。System.Object类也可用小写的 object关键字表示,两者完全等同。自然C#中所有的类都继承了System.Object类的公共接口,剖析它们对我们理解并掌握C#中类的行为非 常重要。下面是仅用接口形式表示的System.Object类:

namespace System
{
public class Object
{
public static bool Equals(object objA,object objB){}
public static bool ReferenceEquals(object objA,object objB){}
public Object(){}
public virtual bool Equals(object obj){}
public virtual int GetHashCode(){}
public Type GetType(){}
public virtual string ToString(){}
protected virtual void Finalize(){}
protected object MemberwiseClone(){}
}

我们先看object的两个静态方法Equals(object objA,object objB),ReferenceEquals(object objA,object objB)和一个实例方法Equals(object obj)。在我们阐述这两个方法之前我们首先要清楚面向对象编程两个重要的相等概念:值相等和引用相等。值相等的意思是它们的数据成员按内存位分别相等。 引用相等则是指它们指向同一个内存地址,或者说它们的对象句柄相等。引用相等必然推出值相等。对于值类型关系等号“= =”判断两者是否值相等(结构类型和枚举类型没有定义关系等号“= =”,我们必须自己定义)。对于引用类型关系等号“= =”判断两者是否引用相等。值类型在C#里通常没有引用相等的表示,只有在非托管编程中采用取地址符“&”来间接判断二者的地址是否相等。

静 态方法Equals(object objA,object objB)首先检查两个对象objA和objB是否都为null,如果是则返回true,否则进行objA.Equals(objB)调用并返回其值。问 题归结到实例方法Equals(object obj)。该方法缺省的实现其实就是{return this= =obj;}也就是判断两个对象是否引用相等。但我们注意到该方法是一个虚方法,C#推荐我们重写此方法来判断两个对象是否值相等。实际上 Microsoft.NET框架类库内提供的许多类型都重写了该方法,如:System.String(string),System.Int32 (int)等,但也有些类型并没有重写该方法如:System.Array等,我们在使用时一定要注意。对于引用类型,如果没有重写实例方法Equals (object obj),我们对它的调用相当于this= =obj,即引用相等判断。所有的值类型(隐含继承自System.ValueType类)都重写了实例方法Equals(object obj)来判断是否值相等。

注意对于对象x,x.Equals(null)返回false,这里x显然不能为null(否则不能完成 Equals()调用,系统抛出空引用错误)。从这里我们也可看出设计静态方法Equals(object objA,object objB)的原因了--如果两个对象objA和objB都可能为null,我们便只能用object. Equals(object objA,object objB)来判断它们是否值相等了--当然如果我们没有改写实例方法Equals(object obj),我们得到的仍是引用相等的结果。我们可以实现接口IComparable(有关接口我们将在“第七讲 接口 继承与多态”里阐述)来强制改写实例方法Equals(object obj)。

对于值类型,实例方法Equals(object obj)应该和关系等号“= =”的返回值一致,也就是说如果我们重写了实例方法Equals(object obj),我们也应该重载或定义关系等号“= =”操作符,反之亦然。虽然值类型(继承自System.ValueType类)都重写了实例方法Equals(object obj),但C#推荐我们重写自己的值类型的实例方法Equals(object obj),因为系统的System.ValueType类重写的很低效。对于引用类型我们应该重写实例方法Equals(object obj)来表达值相等,一般不应该重载关系等号“= =”操作符,因为它的缺省语义是判断引用相等。

静态方法ReferenceEquals(object objA,object objB)判断两个对象是否引用相等。如果两个对象为引用类型,那么它的语义和没有重载的关系等号“= =”操作符相同。如果两个对象为值类型,那么它的返回值一定是false。

实 例方法GetHashCode()为相应的类型提供哈希(hash)码值,应用于哈希算法或哈希表中。需要注意的是如果我们重写了某类型的实例方法 Equals(object obj),我们也应该重写实例方法GetHashCode()--这理所应当,两个对象的值相等,它们的哈希码也应该相等。下面的代码是对前面几个方法的 一个很好的示例:

using System;
struct A
{
public int count;
}
class B
{
public int number;
}
class C
{
public int integer=0;
public override bool Equals(object obj)
{
C c=obj as C;
if (c!=null)
return this.integer==c.integer;
else
return false;
}
public override int GetHashCode()
{
return 2^integer;
}
}
class Test
{
public static void Main()
{
A a1,a2;
a1.count=10;
a2=a1;
//Console.Write(a1==a2);没有定义“= =”操作符
Console.Write(a1.Equals(a2));//True
Console.WriteLine(object.ReferenceEquals(a1,a2));//False
B b1=new B();
B b2=new B();
b1.number=10;
b2.number=10;
Console.Write(b1==b2);//False
Console.Write(b1.Equals(b2));//False
Console.WriteLine(object.ReferenceEquals(b1,b2));//False
b2=b1;
Console.Write(b1==b2);//True
Console.Write(b1.Equals(b2));//True
Console.WriteLine(object.ReferenceEquals(b1,b2));//True
C c1=new C();
C c2=new C();
c1.integer=10;
c2.integer=10;
Console.Write(c1==c2);//False
Console.Write(c1.Equals(c2));//True
Console.WriteLine(object.ReferenceEquals(c1,c2));//False
c2=c1;
Console.Write(c1==c2);//True
Console.Write(c1.Equals(c2));//True
Console.WriteLine(object.ReferenceEquals(c1,c2));//True
}
}

如我们所期望,编译程序并运行我们会得到以下输出:

True False 
False False False
True True True
False True False
True True True

实例方法GetType()与typeof的语义相同,它们都通过查询对象的元数据来确定对象的运行时类型,我们在“第十讲 特征与映射”对此作详细的阐述。

实例方法ToString()返回对象的字符串表达形式。如果我们没有重写该方法,系统一般将类型名作为字符串返回。

受保护的Finalize()方法在C#中有特殊的语义,我们将在“第

你可能感兴趣的:(编程,C++,c,C#,asp.net)