
第1篇 技术基础总结
第1章 .NET,你真的知道了吗
打工皇后吴士宏,原本是一个医院护士,硬是靠自己刻苦的努力,一门一门地啃,几门几门地考,最终通过自学考试拿到了英语专科的文凭。并且,凭着一台收音机,花了一年半的时间学完了许国璋英语三年的课程,就壮起胆子去IBM应聘。
那是1985年,站在长城饭店的玻璃转门外,吴士宏足足用了五分钟的时间来观察别人怎么从容地步入这扇神奇的大门。两轮的笔试和一次口试吴士宏都顺利通过了,面试进行得也很顺利。
最后,主考官问她:“你会不会打字?”
“会!”吴士宏条件反射般地说。
“那么你一分钟能打多少?”
“你的要求是多少?”
主考官说了一个数字,吴士宏马上承诺说可以。她环顾四周,发现现场并没有打字机,果然考官说下次再考打字。
实际上,吴士宏从未摸过打字机。面试结束,她飞也似地跑了出去,找亲友借了170元买了一台打字机,没日没夜地敲打了一个星期,双手疲乏得连吃饭都拿不住筷子了,但她竟奇迹般地达到了考官说的那个专业水准。
吴士宏就这样牢牢把握住了进入IBM的机会。从前台接待员做起,历任销售代表、销售经理、华南区总经理、网络计算战略研究员,直到IBM中国区渠道总经理,而后又出任微软(中国)公司的总经理,被誉为中国的“打工皇后”,创造了一段艰辛但是极其辉煌的职业生涯。
如何抓住转瞬即逝的机会,除了要有充分的自信,还需要有内在的积累。正所谓“艺高人胆大”,面试时所能迸发出的勇气,取决于日常对于基础知识的积累和对自己能力的判断。只有你的素质和经验积累到了一定水准,灵感火花才会迸发。
1.1 搞清自己是干什么的
出门在外,总会有人问你是做什么的,回答是:“我是做.NET 开发的”。有的人也许会继续问“那.NET 是什么呢?”。曾经有很多人问过我这个问题,刚入行时,我认为这只是一个开发工具,后来认为它是一个平台,一个软件环境,再后来慢慢觉得这是微软的一个新战略,但是现在我想我会这样回答这个问题:.NET 是一个概念,是一种构想,或者说是微软的一个梦想。
1.微软定义
Microsoft .NET是微软以Web Service为核心,为信息、人、系统、各种设备提供无缝连接的一组软件产品(Smart Client、服务器、开发工具)、技术(Web Service)或服务。除了将小型的、分散的构建模块应用互相连接起来,还将Internet上更大的应用连接起来,而不管应用程序所采用的是哪种操作系统、设备或编程语言。
根据微软的定义,.NET 的精髓的确在 Web Service。虽然没有使用 Web Service 的Windows Forms也是.NET的一部分。但我认为微软当初的想法,应该是通过Web Service将企业开发的模式从Client/Server或者Browser/Server转换到Web Service/Smart Client。
2.战略与梦想
在微软的产品发布会上,主持人曾经说过一句话:在未来,我们可以在任何时间、任何地点、使用任何设备获取信息(any time, any place and on any device)。但是这当然不是.NET的全部,仅仅能够获取信息是不够的,通过.NET,用户还将会获得由程序封装过的数据——也就是服务。关于这一点,我有如下的想象:有了.NET,全世界的互联网络会组成一个庞大的服务中心,而你的终端设备(当然不仅仅是你的台式机,还包括手机、PDA、平板 PC,甚至家用电器等)就是你的贴身智能秘书。你不需要自己获得信息去分析,只需将你的要求说(自然语言技术也是.NET 的一部分)给秘书听,她就会自动地去互联网络上查找相关的服务,经过自己的智能处理与整合,以最有效率的方式完成你交给她的任务。
很多人为这个前景欢欣鼓舞,其实这只是.NET体验的一小部分而已。.NET为开发人员提供了新的开发平台(.NET框架)、新的开发语言(C#)、新的开发工具(Visual Studio .NET)、新的开发方式(Web Service);为普通用户提供了Windows CE、Windows XP、Stinger、Xbox、Tablet PC、.NET My Services、MSN等产品;为企业提供了bCentral。在未来,最终大多数流行的 Microsoft 软件应用程序——包括 Office 和 Visual Studio .NET——将开始与 XML Web 服务实现交互,并把它们的主要功能作为Web 服务公开,以便其他开发人员可以利用。
(1)Microsoft .NET是一个平台,是Microsoft的Web服务平台。Web Service允许应用程序通过Internet进行通信和共享数据,而不管所采用的是哪种操作系统、设备或编程语言。Microsoft .NET平台提供创建Web Service并将这些服务集成在一起。这个平台包含广泛的产品系列,它们都基于XML和Internet行业标准构建,提供从开发、管理、使用到体验 Web服务的每一方面。现在,Microsoft 正在 5 个方面创建 .NET 平台,即工具、服务器、Web服务、客户端和 .NET体验。
(2)Microsoft .NET更是微软的一个网络战略,是微软意图全面占领互联网领域的最强有力的武器。在全球互联网络市场的抢夺战上,微软似乎慢了半拍,在浏览器方面差点败在Netscape 手中,还为此吃了老大的官司,在流媒体上又被 Real 斩于马下。随着互联网以前所未有的速度席卷全球,很多人都希望借此机会重新洗牌,但是惯于制定游戏规则的微软又怎会轻易将主动权交到别人的手中。同时,由于开放源码组织的不断壮大,微软紧抱源码不放的做法招致越来越多人的不满,很多人出于不同的考虑(安全、开放、免费)投靠了Linux阵营。对于微软来说,在产品上必须要变,而这个变,就是.NET 所带来的由一个软件公司向一个服务公司的转变。事实上,微软将来可能会变成一个全球最大的网络服务商(ASP)。Windows这个给微软带来令人眩目的财富和辉煌的十年视窗时代结束了,微软打算全面设计我们的未来,它将把它所有的产品全部重新改写为与.NET构造相一致的形态,以ASP(应用服务供应商)方式提供,这之后微软不再主要依靠授权和销售软件光盘赚钱,而是要通过互联网上运行的大量软件服务赚钱,从软件供应商走向ASP——这就是微软的新战略。
事实上,很早就有人提出过,对计算机发展和普及做出巨大贡献的软件行业已经到了这样一个转折点:留在终端的软件会越来越少,目前通过软件包发行的方式即将消失,而改为网上出租的形式获得利润,用户只要在本地发出请求,就可以在网上直接使用它,而这个软件的供应商会根据你使用的次数来收费。现在你明白微软为何拼了老本也要对Netscape赶尽杀绝了,一旦这个设想成为现实,集成诸多功能的浏览器就将取代现在操作系统的地位,成为终端上唯一需要预安装的软件。如果这个浏览器不是微软的产品,那么后果可想而知。
(3)Microsoft .NET还是微软的一个互联网之梦,是微软提出的下一代互联网构想。在不远的将来整个世界将由Internet连接在一起,而且不论何时何地都可以实现宽带访问。任何设备都将通过网络连接起来,贸易往来与信息交换都将以光速进行。所有设备都将在标准化或共享协议的基础上使用XML这样的公用语言,同时它们将在不同的操作系统和设备上执行众多软件。例如:即将或已经空空如也的冰箱能自动向当地的超市发送订单,然后再次“填满”它的“肚子”,甚至你在公司可以通过互联网查看你家冰箱里有什么;当你的食物放进微波炉后,它会根据食物的特点自动下载烹调时间。这是不是很像电影大片里的镜头,很酷?这个战略并不是微软公司所独有的,其他许多公司,像IBM和SUN都有这方面的设想。这是微软心中的一个梦想,它要通过.NET 来改变人们未来的生活方式。就像当年Windows 的出现使电脑从精英手中的玩具变成了普通大众身边不可或缺的工具一样,.NET极有可能会使互联网成为与人们生活密不可分的一部分。在未来的i时代,人们的生活、工作、学习、娱乐,都将得到.NET的帮助。在那个时候,.NET或许不会有人再提起,但是它带来的产品与概念,却将时刻伴随我们。
3.通俗的理解
平时,我们一般所说的.NET多指.NET Framework、Visual Studio.NET及开发出的应用程序(ASP.NET、WinForm等)。.NET Framework是一个开发和执行环境,允许不同的程序设计语言和库无缝结合共同创建基于 Windows 的应用程序,轻松地创建、管理、部署,并与其他网络系统集成。
让我们看一下.NET Framework的体系结构,如图1-1所示。

图1-1 .NET Framework的体系结构
从上图我们可以简要地了解.NET开发框架的几个主要组成部分:
● 首先是整个开发框架的基础和核心,即公共语言运行库(CLR)及它所提供的一组基础类库(FCL)。
● 在开发技术方面,.NET提供了全新的数据库访问技术ADO .NET,以及网络应用开发技术ASP .NET和Windows编程技术WinForm。
● 在开发语言方面,.NET提供了VB、VC++、C#、JScript等多种语言支持;而Visual Studio .NET则是全面支持.NET的开发工具。
简单地讲,.NET Framework 是一个与硬件无关的程序执行平台,我们使用其他如 C/C++/Delphi 开发的程序编译后是硬件识别的机器代码,而我们开发的.NET 应用程序编译后是.NET Framework识别的中间语言代码(Microsoft Intermediate Language,MSIL),这些代码不专用于任何一种操作系统。程序运行的时候,由Just-In-Time(JIT)编译器二次编译成专用于当前操作系统和目标机器结构的本机代码,通过托管的 CLR 环境和基类库在计算机中执行。所以,我们开发的.NET 应用程序必须要.NET Framework 的支持。这样.NET Framework提供统一的类型标准,从而达到跨语言(跨类库)。而且.NET上开发的应用程序是透过框架(.NET Framework)访问硬件,而不是直接访问硬件的,所以限制了指针等直接访问硬件功能的使用,达到安全性。我们在开发应用程序的时候就可以不考虑与系统相关的细节,把注意力放在代码的功能上就够了。
现在,让我们总结一下创建执行.NET应用程序时经历的几个步骤。
(1)使用某种.NET兼容语言(如C#)编写应用程序代码,如图1-2所示。

图1-2 用C#编写程序代码
(2)把应用程序代码编译为中间语言代码(MSIL),存储在程序集中,如图1-3所示。

图1-3 编译为中间语言代码
(3)在执行代码时(如果这是一个可执行文件就自动运行,或者在其他代码使用它时运行),首先必须使用JIT编译器将中间语言代码编译为本机代码。因为JIT编译器会确切地知道程序运行在什么类型的处理器上,可以利用该处理器提供的特性或特定的机器指令来优化最后的可执行代码,如图1-4所示。

图1-4 编译为本机代码
(4)在托管的CLR环境下运行本机代码,以及其他应用程序或过程,如图1-5所示。

图1-5 运行本机代码
趣味理解
.NET就像中国移动整个的网络和服务运营平台,.NET Framework 则是支持该平台运行的那些基础设施,而我们开发的软件就像运行在这个平台下的各种服务(语音通话、短信、彩信、GPRS的各种功能)和手机或移动设备。任何服务或手机都需要移动网络的支持才可以用。就像我们开发的软件需要.NET Framework的支持才能运行一样。
1.2 .NET的几个特性
1.一次编译,到处运行
首先要说的就是.NET Framework的平台无关性,因为我们开发的.NET应用程序编译后是一种中间语言代码(MSIL),剩下的由.NET Framework完成机器码的JIT编译实现。所以,我们并不需要关心最后运行的系统平台环境是什么。只要.NET Framework 支持的地方我们的程序就可以运行,而不需要考虑不同的硬件或系统需要什么指令。代码的移植和运行都是由.NET Framework 自动完成的。理论上实现了一次编译,到处运行的思想。举个简单的例子:以后我们写完的.NET应用程序,既可以在Windows平台上运行,也可以在Linux平台上运行,而不需要我们针对不同的系统做不同的版本。
注 意
.NET 的平台无关性目前只有一种可能。目前.NET 还只能用于 Windows 平台,但已经有人在实施用于其他平台的计划:Mono——实现Linux版.NET平台。
2.编程语言不再是面试的瓶颈
使用中间语言不仅支持平台无关性,还支持语言的互操作性。简单地讲,就是在 C#里可以直接调用VB.NET开发的组件里面的方法和类库。
趣味理解
传说中,远古的人类为了上天堂,召集了千军万马修筑通天塔,工程浩大但进展顺利。上帝知道后大为震惊,决定立即中止人类的计划。于是,他制造了很多不同的语言给人类使用。终于,由于语言不通,人类无法协调工作,建造通天塔的计划不得不半途而废。虽然上帝为了阻止人们建造通天塔而为人类设置了不同的语言,造成了交流的障碍,但这并不妨碍人们尝试各种方法突破语言障碍的努力。在计算机语言层出不穷的年代,微软的.NET 平台为使用不同语言的程序员提供了一个比较好的解决方案。
.NET Framework中设计了一个通用语言系统(Common Language System,CLS),定义了很多标准的数据类型,.NET Framework 支持的所有高级语言都必须同时支持该系统对数据类型的定义。
在编译成 MSIL 代码的同时,各种高级语言自身的数据类型都被转换成了 CLS 系统中的标准数据类型,例如,VB.NET中定义的Integer数据类型被转换成了System.Int32数据类型,C#中的int类型也被转换成了System.Int32数据类型。这样不同语言的变量就可以相互交换信息了,这就是.NET Framework支持混合语言编程的基本原理。
有时候看到很多软件公司招聘.NET 程序员的广告,要求程序员统一使用某种编程语言进行开发,笔者觉得既然是在.NET平台中开发,那么使用C#、VB.NET、.Delphi.NET或J#语言已经没有什么差别,.NET 平台将不同语言的代码统统编译成中间语言,在虚拟机上运行。于是一个程序的好坏已经不是由语言的“高低贵贱”所决定,而是由程序员的水平来决定的。因此招聘到什么水平的程序员,比招聘到使用什么语言的程序员更重要。对于一个经验丰富的程序员来说,选择使用什么语言写程序已经完全是个人的偏好了,这种偏好应该在支持跨语言开发的.NET平台下得到尊重和鼓励,以充分发挥程序员的个性和创造力。
3.自动内存管理,让我们放心编程
CLR对程序员影响最大的就是它的内存管理功能,以至于我们很有必要单独把它列出来阐述。它为应用程序提供了高性能的垃圾收集环境。垃圾收集器自动追踪应用程序操作的对象,程序员再也用不着和复杂的内存管理打交道。这对某些喜欢张口闭口底层编程的所谓高手来说,自动内存管理从来都是他们嘲笑的对象。的确,为通用软件环境设计的自动化内存管理器永远都抵不上自己为特定程序量身订制的手工制作。但现代软件业早已不再是几百行代码的作坊作业,动辄成千上万行的代码,大量的商业逻辑凸现的已不再是算法的灵巧,而是可管理性、可维护性的工程代码。.NET/C#不是为那样的作坊高手准备的,C 语言才是他们的尤物。在Microsoft.NET托管环境下,CLR负责处理对象的内存布局、管理对象的引用,以及释放系统不再使用的内存(自动垃圾收集)。这从根本上解决了长期以来软件的内存泄漏和无效内存引用问题,大大减轻了程序员的开发负担,提高了程序的健壮性。实际上我们在托管环境下根本找不到关于内存操作或释放的语言指令。
4.基类库——.NET开发的宝藏
Microsoft.NET 框架类库是一组广泛的、面向对象的可重用类的集合,为应用程序提供各种高级的组件和服务。它将程序员从繁重的编程细节中解放出来专注于程序的商业逻辑,为应用程序提供各种开发支持——不管是传统的命令行程序还是 Windows 图形界面程序,又或是面向下一代互联网分布式计算平台的ASP.NET和Web服务。
类是面向对象设计语言中非常重要的部分,好比是工业流水线上的模具,按照这些类就可以实例化为对象,在程序中直接使用。.NET Framework 中的类库提供了非常丰富的类,使得开发程序十分简单。诸如字符串处理、数据收集、数据库连接,以及文件访问等任务,在.NET Framework中都提供了完善的类可以直接使用。
.NET Framework类库在.NET Framework中的位置如图1-6所示。

图1-6 .NET Framework类库的位置
注 意
.NET的托管代码都必须使用CLR,但不一定都必须使用类库,基类库只是辅助实现功能而封装的大量组件和服务。但在实践中很少有不使用类库的程序。
1.3 万丈高楼平地起:面试者必会
在开始真正的项目开发之前,我们首先需要学习和使用一门开发语言。本节将介绍 C#的基础知识,并假定读者具备C#编程的基本知识,这是后续章节的基础。
1.3.1 C#介绍
C#是由微软开发的一种简单、现代、优雅、面向对象、类型安全、平台独立的新型组件编程语言,其语法风格源自C/C++家族,融合了VB的高效和C/C++的强大。对于Web开发而言,C#有点像Java,同时具有Delphi的一些优点。微软宣称,C#是开发.NET框架应用程序的最好语言。C#是专门为.NET应用而开发的语言。这从根本上保证了C#与.NET的完美结合。.NET 框架的各种优点在 C#中表现得淋漓尽致。所以,本书将以 C#语言作为蓝本进行讲解。
1.3.2 命名空间
命名空间是一种特殊的分类机制,它将与一个特定功能集有关的所有类型都分到一起,是.NET避免类名冲突的一种方式。
例如:A公司和B公司之间的程序相互调用,但各自公司的项目里都有一个类表示顾客,例如类Customer,那么使用时如何区分呢?命名空间就很好地解决了这个问题。
A公司的类可以这样命名:A.Project.Customer;
B公司的类可以这样命名:B.Project.Customer。
在C#中几乎所有的程序都使用System命名空间中的类。通过using System;将该命名空间下的类导入进来直接使用。
趣味理解
命名空间就像XX省.XX市.XX县.XX单位,也许在不同的省或市会有同名的县或单位,那么需要靠各自的归属来区分。有时候可以把命名空间理解成一个文件目录,也许会更容易理解一些,只不过文件目录是物理上的分组,而命名空间是类的逻辑上的一种分组。
1.3.3 C#语法格式要点
首先介绍简单的Hello World程序。
几乎大多数人学开发都是从Hello World开始的。例如下面的代码:
using System; //①引用其他命名空间 namespace LTP.ClassLib //②命名空间定义 { public class Welcome //③类定义 { void Main(string[] args) //④程序入口Main方法定义 { Console.WriteLine("Hello, World! Welcome to you!"); //⑤控制台输出一行信息 } } }
(1)C#的源文件其实就是以“.cs”作为扩展名的文本文件。我们不一定必须用VS.NET集成工具才可以编写C#程序,可以用记事本进行编辑代码,然后通过CSC.exe来编译C#的代码文件。
命令行示例如下。
● 编译File.cs以产生File.exe:
csc File.cs
● 编译File.cs以产生 File.dll:
csc /target:library File.cs
● 编译File.cs并创建My.exe:
csc /out:My.exe File.cs
● 编译当前目录中所有的 C# 文件,以产生 File2.dll 的调试版本。不显示任何徽标和警告:
csc /target:library /out:File2.dll /warn:0 /nologo /debug *.cs
● 将当前目录中所有的 C# 文件编译为 Something.xyz(一个后缀为.xyz 的 DLL 文件):
csc /target:library /out:Something.xyz *.cs
CSC.exe可以在“C:\WINDOWS\Microsoft.NET\Framework\相应版本号”下找到。
(2)C#语句以分号作为语句结尾。可以写多行,而不需要续行符号。
例如:
void Main(string[] args) { Console. WriteLine ("Hello, World! Welcome to you!"); }
(3)添加注释。采用“//”为单行注释;采用“/*内容*/”为多行注释。例如:
//这是单行的注释 /*这是多行的 注释信息示例*/
单行注释中的任何内容,即“//”后面所有内容都会被编译器忽略。多行注释中“/*”和“*/”之间的所有内容也会被忽略。显然不能在多行注释中包含“*/”组合,因为这会被当做注释的结尾。
(4)C#语句是区分大小写的,VB语句不区分大小写。例如:Welcome和welcome是不同的。
(5)类和方法的定义主体总是以“{”开始,以“}”结束,并且二者必须完全匹配。例如:
public class Welcome //③类定义 { void Main(string[] args) //④方法定义 { } }
(6)Console是一个类,表示控制台应用程序的标准输入流、输出流和错误流。
(7)C#可执行文件(Windows应用程序和Windows服务)都必须有一个Main方法,否则编译会报错。
1.3.4 变量
变量是用来描述一条信息的名称。在计算机中,变量代表存储地址。
(1)在C#中变量必须先定义后使用。
(2)变量声明语法为数据类型 变量名;,例如:
int n = 0; string s1 = "a";
(3)在C#中不允许变量只声明,不赋值。例如:int n;,会报警。
(4)一个语句可以声明和初始化多个变量。例如:int x=1 , y=20;。
1.3.5 类型推断
类型推断使用var关键字。声明变量的语法有些变化。编译器可以根据变量的初始化值“推断”变量的类型。
例如:
int n = 0;
就变成:
var n = 0;
即使n从来没有声明为int,编译器也可以确定只要n在其作用域内,就是一个int。编译后,上面两个语句是等价的。
代码示例: (示例位置:光盘\code\ch01\01)
class Program { static void Main(string[] args) { var name = "litianping"; var age = 30; var isRabbit = true; Type nameType = name.GetType(); Type ageType = age.GetType(); Type isRabbitType = isRabbit.GetType(); Console.WriteLine("name is type " + nameType.ToString()); Console.WriteLine("age is type " + ageType.ToString()); Console.WriteLine("isRabbit is type " + isRabbitType.ToString()); //age = "30"; //报错 Console.ReadKey(); } }
程序运行结果如图1-7所示。

图1-7 程序运行结果
注 意
变量必须初始化。否则,编译器就没有推断变量类型的依据。初始化器不能为空,且必须放在表达式中。不能把初始化器设置为一个对象,除非在初始化器中创建了一个新对象。声明了变量,推断出了类型后,变量的类型就不能改变了。
1.3.6 变量的作用域
一个变量的作用域是指能够使用该变量的程序区域。作用域既作用于方法,也作用于变量。一个标识符(不管它代表变量还是代表方法)的作用域是从声明该标识符的那个位置开始的。
一般情况下,确定作用域有以下规则:
● 在 C#中用“{”和“}”定义语句块,来界定局部变量的作用范围,局部变量存在于声明该变量的块语句或方法的“{”和“}”之间的作用域内。
● 只要类在某个作用域内,其字段也在该作用域内。
● 在for、while或类似语句中声明的局部变量存在于该循环体内。
1.局部变量作用域
方法主体的起始与结束大括号之间即建立了一个作用域。方法主体中声明的任何变量都在方法的作用域内有效;一旦方法结束,它们就会消失,而且只能由那个方法内部执行的代码来访问。这些变量称为局部变量,因为它们局限于声明它们的那个方法,不能在其他任何方法的作用域中使用。换言之,你不能使用局部变量在不同的方法之间共享信息。例如:
class Example { void MethodA() { int n; //声明一个局部变量 //…… } void MethodB() { n = 42; //错误– 变量越界 //…… } }
上述代码将编译失败,因为 MethodB 方法试图使用一个越界的变量 n。该变量只能由MethodA方法中的语句使用。
2.定义类作用域
类主体的起始和结束大括号之间也建立一个作用域。在类主体中(但不在一个方法中)声明的任何变量都在那个类的作用域内有效。在 C#术语中,开发者使用字段(Field)一词来描述由一个类定义的变量。和局部变量不同,字段可以在不同的方法之间共享信息。例如:
class Example { void MethodA() { n = 48; //ok //…… } void MethodB() { n = 42; //ok //…… } int n = 0; //定义类的一个字段 }
变量n是在类的内部及MethodA和MethodB方法的外部定义的。所以,n具有类的作用域,可由类中的所有方法使用。
注 意
在一个方法中,必须在使用一个变量前先声明它。但字段稍有不同,一个字段可以在使用它的方法之前定义它,也可以在使用它的方法之后定义它——在这种情况下,编译器将为你打点一切!
3.局部变量的作用域冲突
大型程序在不同部分为不同的变量使用相同的变量名是很常见的。只要变量的作用域是程序的不同部分,就不会有问题,也不会产生模糊性。但要注意,同名的局部变量不能在同一作用域内声明两次,所以不能使用下面的代码:
int n = 20; int n = 30; //…
4.字段和局部变量的作用域冲突
在某些情况下,可以区分名称和作用域相同的两个标识符。此时编译器允许声明第2个变量。原因是 C#在变量之间有一个基本的区分,它把声明为类型级的变量看做字段,而把在方法中声明的变量看作局部变量。例如:
代码示例: (示例位置:光盘\code\ch01\02)
class Program { int n = 0; //定义类的一个字段 static void Main(string[] args) { int n = 5; //声明一个局部变量 Console.WriteLine(n); //这里n的值是5,调用的是局部变量,所以输出结果是5 } }
即使在Main方法的作用域内声明了两个变量n,这段代码也会编译——n被定义在类级上,在该类删除前是不会超出作用域的(在本例中,当Main方法中断,程序结束时,才会删除该类)。此时,在Main方法中声明的新变量n隐藏了同名的类级变量,所以在运行这段代码时,会显示数字5。
但是,如果要引用类级变量,该怎么办?可以使用语法object.fieldname,在对象的外部引用类的字段或结构。在下面的例子中,我们访问静态方法中的一个静态字段,所以不能使用类的实例,只能使用类本身的名称。
代码示例: (示例位置:光盘\code\ch01\03)
static class Program { static int n = 0; //定义类的一个字段 static void Main(string[] args) { int n = 5; //声明一个局部变量 Console.WriteLine(Program.n); //这里输出结果是0 Console.ReadKey(); } }
如果要访问一个实例字段(该字段属于类的一个特定实例),就需要使用this关键字。
代码示例: (示例位置:光盘\code\ch01\04)
public partial class Form1 : Form { int n = 0; //定义类的一个字段 private void Form1_Load(object sender, EventArgs e) { int n = 5; //声明一个局部变量 MessageBox.Show(this.n.ToString()); } }
这里MessageBox显示的n的值是0,调用的是字段,而不是变量。
1.3.7 常量
顾名思义,常量是其值在使用过程中不会发生变化的变量。在声明和初始化变量时,在变量的前面加上关键字const,就可以把该变量指定为一个常量:
const int a = 100; //这个值不能被改变
常量具有如下特征:
● 必须在声明时初始化。指定了其值后,就不能再修改了。
● 其值必须能在编译时用于计算。因此,不能用从一个变量中提取的值来初始化常量。如果需要这么做,那么应使用只读字段(详见第2.2节)。
● 常量总是静态的。但不允许在常量声明中包含修饰符static。
在程序中使用常量至少有以下3个好处:
● 用易于理解的清楚的名称替代了含义不明确的数字或字符串,使程序更易于阅读。
● 使程序更易于修改。例如,在C#程序中有一个SalesTax常量,该常量的值为6%。如果以后销售税率发生变化,把新值赋给这个常量,就可以修改所有的税款计算结果,而不必查找整个程序,修改税率为6%的每个项。
● 更容易避免程序出现错误。如果要把另一个值赋给程序中的一个常量,而该常量已经有了一个值,那么编译器就会报告错误。
1.3.8 流程控制
人的一生不可能一帆风顺,实际生活中并非所有的事情都是按部就班地进行的,程序也一样。为了适应自己的需要,我们经常需要转移或改变程序的执行顺序,实现这些目的的语句叫做流程控制语句。
1.条件语句
1)if语句
if语句的语法格式:
if (bool表达式) statement1; else statement2;
当 bool 表达式为真时(条件满足),则执行 statement1,否则执行 statement2。如果statement1 或 statement2 是多行代码,则必须用花括号({…})把这些语句组合为一个语句块。如果statement1或statement2只有一行代码,那么可以省略花括号。
注 意
● bool表达式中等号记住用 ==,而不是 =,=是赋值符号。
if (seconds = 59) //错误 if (seconds == 59) //正确 if (59 == seconds) //推荐写法,不容易出错
● 尽量用“{ }”括起来,即使只有一行代码。如:
int seconds = 0; int minutes = 0; //…… if (59 == seconds) { } seconds = 0; minutes++; else { seconds++; }
2)switch语句
switch语句让程序根据表达式的值,从多个动作中作出选择(从逻辑过程看,和多分支语句if…else有些相似,但比if…else具有更好的可读性)。例如:
for (int n = 0; n < 10; n++) { switch (n) { case 2: Console.WriteLine("2的处理"); break; case 4: Console.WriteLine("4的处理"); break; default: Console.WriteLine("default的处理"); break; } }
注 意
● 每条分支语句都必须有break。
● 最好有default分支语句。
● case后的值必须是常数。
先看下边的代码:
switch (n) { case 2: Console.WriteLine("2的处理"); case 4: Console.WriteLine("4的处理"); break; default: Console.WriteLine("default的处理"); break; }
这个例子在“case 2”这部分会产生一个错误,因为程序会一直执行到“case 4”,也就是发生了穿越,但是在 C#中,是禁止穿越发生的。因此,你必须使用“break”、“goto”或者“return”来阻止穿越的发生,但是下边的情况例外:
switch (n) { case 2: case 4: Console.WriteLine("2和4的同样处理"); break; default: Console.WriteLine("default的处理"); break; }
该情况实现了在多个情况下执行同一操作。
在特殊情况下,要使程序可以进行这种穿越,实现它其实也很简单。可以使用“goto”语句来防止程序的穿越,其实也可以用它来实现穿越,只需使用它将程序跳转到另一个“case”即可。例如:
switch (n) { case 2: Console.WriteLine("2的处理"); if (DateTime.Now.Day == 6) { goto case 6; //直接跳转到case 6 } break; case 4: Console.WriteLine("4的处理"); break; case 6: Console.WriteLine("6的处理"); break; default: Console.WriteLine("default的处理"); break; }
2.循环语句
1)for循环
for循环重复执行一个语句或语句块,直到指定的表达式计算为false值。
for (变量初始化; 条件表达式; 计算表达式) { 语句; }
例如,下例中循环输出0到9的数字。
for (int n = 0; n < 10; n++) { Console.WriteLine(n); }
这里声明了一个int类型的变量n,并把它初始化为0,用做循环计数器。接着测试它是否小于10。因为这个条件等于true,所以执行循环中的代码,显示值为0。然后给该计数器加1,再次执行该过程。当n等于10时,循环停止。
2)while循环
while循环适合先判断后执行,根据条件循环指定的次数。
bool 表达式= false; while (!bool表达式) { //直到bool表达式为true时,停止循环 DoSomeWork(); bool表达式= 检查条件(); }
例如:
int n = 1; while (n < 100) { DoSomeWork(); n++; }
3)do…while循环
do…while循环是while循环的后测试版本。do…while循环适合于至少执行一次的循环体,先执行,后判断,直到满足条件为止。
int n=1; do { DoSomeWork(); n++; } while (n < 100);
通过使用break 语句,你可以迫使运行退出循环。如果你想跳过这一次循环,那么使用continue 语句。
int n=1; do { n++; if (10 == n) { continue; //当等于10时不再向下执行,跳到下一个循环 } if (50 == n) { break; //当等于50的时候,退出do循环 } } while (n < 100);
4)foreach循环
foreach循环适合于迭代列举数组和集合中的每个元素,而无须知道数组中的元素个数。但不能改变每项的值。例如,我们循环列出整数数组中所有的整数值。
foreach (int item in arrayList) { Console.WriteLine(item); }
3.跳转语句
C#提供了许多可以立即跳转到程序中另一行代码的语句。
1)goto语句
goto语句可以直接跳转到程序中用标签指定的另一行(标签是一个标识符,后跟一个冒号):
goto tag1; Console.WriteLine("这行代码不会被执行"); string s1 = "a";//这行代码不会被执行 tag1: Console.WriteLine("跳到这里继续执行");
goto语句有两个限制。不能跳转到像for循环这样的代码块中,也不能跳出类的范围,不能退出try…catch块后面的finally块(1.6.5节将介绍如何用try…catch…finally块处理异常)。
goto语句的名声不太好,在大多数情况下不允许使用它。一般情况下,使用它肯定不是面向对象编程的好方式。但是在有一个地方使用它是相当方便的——在 switch 语句的 case子句之间跳转,这是因为C#的switch语句在故障处理方面非常严格。前面介绍了其语法。
2)break语句
前面简要提到过break语句——在switch语句中使用它退出某个case语句。实际上,break也可以用于退出for、foreach、while或do…while循环,该语句会使控制流执行循环后面的语句。
如果该语句放在嵌套的循环中,则执行最内部循环后面的语句。如果break放在switch语句或循环外部,则会产生编译错误。
3)continue语句
continue语句类似于break,也必须在for、foreach、while或do…while循环中使用。但它只退出循环的当前迭代,开始执行循环的下一次迭代,而不是退出循环。
4)return语句
return语句用于退出类的方法,把控制权返回方法的调用者,如果方法有返回类型,则return语句必须返回这个类型的值,如果方法没有返回类型,那么应使用没有表达式的return语句。
1.3.9 字符串常见操作
在日常开发中几乎随时都会涉及字符串的处理,这里将一些常用的操作和遇到的问题整理一下。可以让大家在日后的开发中节省时间,提高开发效率。
1.取字符串长度
string str = "中国"; int Len = str.Length; //得到字符串str的长度
2.字符串转为比特码
例如:
byte[] bytStr = System.Text.Encoding.Default.GetBytes(str); //然后可得到比特长度 len = bytStr.Length;
3.字符串相加
System.Text.StringBuilder sb = new System.Text.StringBuilder(); sb.Append("中华"); sb.Append("人民"); sb.Append("共和国"); //和“+”等效,但StringBuilder性能更好一些 string str = "中华" + "人民" + "共和国";
4.截取字符串的一部分
语法:变量.Substring(起始位置,截取位数);。
例如:
string s1 = str.Substring(0,2);
5.查指定位置是否为空字符
语法:char.IsWhiteSpce(字串变量,位数)。
例如:
string str = "中国人民"; Response.Write(char.IsWhiteSpace(str, 2));
结果为True,第一个字符是0位,2是第3个字符,正好是一个空格。
6.查字符是否是标点符号
语法:char.IsPunctuation('字符')。
例如:
Response.Write(char.IsPunctuation('A'));
结果为false。
7.把字符转化为数字,查代码点
Response.Write((int)'中');
结果为20013。注意是单引号(int)'字符'。
8.把数字转为字符,查代码代表的字符
语法:(char)代码。
例如:
Response.Write((char)22269);
结果为“国”字。
9.清除字符串前后包含的空格
string str = " 中国"; str = str.Trim();
10.替换字符串:字符串变量.Replace(将原字符串替换为新的字符串)
string str = "中国"; str = str.Replace("国", "央"); //将“国”字换为“央”字 Response.Write(str);
结果为“中央”。
11.删除字符串最后一个字符的3种方法
字符串:string s = "1,2,3,4,5,"。
目标:删除最后一个“,”。
方法1 用得最多的是Substring:
s = s.Substring(0,s.Length - 1)
方法2 用 RTrim,这个我原来只知道用来删除最后的空格,也没有仔细看过其他的用法,后来才发现用它可以直接截去一些字符。
s = s.ToString().RT rim(',')
方法3 TrimEnd和RTrim差不多,区别是这个传递的是一个字符数组,而RTrim可以是任何有效的字符串。
s= s.TrimEnd(',') //如果要删除"5,",则需要这么写 char[] MyChar = {'5',','}; s = s.TrimEnd(MyChar); //s = "1,2,3,4" s = string.TrimEnd().Remove(string.Length - 2, 1)
类似函数:TrimStart、LTrim等。
12.Split的3种方法
日常会遇到一些类似于数组的数据是用分隔符“|”或者“,”进行隔开存放的,或者一些按规律获取部分字符串。
方法1 用单个字符来分隔:
string str = "aaa,bbb,ccc"; //得到逗号分隔的各个字符串 string[] sArray = str.Split(','); //string[] sArray = str.Split(new char[] { ',' });//这种写法也可以 foreach (string i in sArray) { Response.Write(i.ToString() + "<br>"); }
输出结果:
aaa bbb ccc
方法2 用多个字符来分隔:
string str = "aaajbbbscccjdddseee"; //得到以'j'或's'分隔的各个字符串 string[] sArray = str.Split(new char[2] { 'j', 's' }); foreach (string i in sArray) { Response.Write(i.ToString() + "<br>"); }
输出结果:
aaa bbb ccc ddd eee
方法3 用字符串分隔:
using System.Text.RegularExpressions; //先引用 string str = "aaajsbbbjsccc"; //得到以'js'分隔的各个字符串 string[] sArray = Regex.Split(str, "js", RegexOptions.IgnoreCase); foreach (string i in sArray) { Response.Write(i.ToString() + "<br>"); }
输出结果:
aaa bbb ccc
13.几种输出字符串的格式
12345.ToString("n"); //生成12,345.00 12345.ToString("C"); //生成¥12,345.00 12345.ToString("e"); //生成1.234500e+004 12345.ToString("f4"); //生成12345.0000 12345.ToString("x"); //生成3039 (16进制) 12345.ToString("p"); //生成1,234,500.00%
14.把123456789转换为12-345-6789的3种方法
方法1
string a = "123456789"; a = int.Parse(a).ToString("##-###-####");
方法2
string a = "123456789"; a = a.Insert(5, "-").Insert(2, "-");
方法3
using System.Text.RegularExpressions; //先引用 string a = "123456789"; Regex reg = new Regex(@"^(d{2})(d{3})(d{4})$"); a = reg.Replace(a, "$1-$2-$3");
15.输出21个A的简单做法
一般情况下会通过循环实现:
string str1 = ""; for (int n = 0; n < 21; n++) { str1 += "A"; }
看看下面的实现方法是不是更简单:
string str2 = new string('A', 21);
16.得到随机数的方法
Random r = new Random(); int n1 = r.Next(); //返回非负随机整数 Response.Write(n1 + "<br>"); int n2 = r.Next(10); //返回一个小于所指定最大值(10)的非负随机整数 Response.Write(n2 + "<br>"); int n3 = r.Next() % 10; //返回一个小于所指定最大值(10)的非负随机整数 Response.Write(n3 + "<br>"); int n4 = r.Next(1, 20); //返回一个指定范围(1~20)内的随机整数 Response.Write(n4 + "<br>"); double d5 = r.NextDouble();//得到一个介于0.0~1.0之间的随机整数 Response.Write(d5+"<br>");
17.Int32.TryParse()、Int32.Parse()、Convert.ToInt32()比较
这3个方法都是将字符串转换为整数数字。
string myString = "1234"; int myint = 0; //方法1 myint = Convert.ToInt32(myString); Response.Write(myint + "<br>"); //方法2 myint = Int32.Parse(myString); Response.Write(myint + "<br>"); //方法3 Int32.TryParse(myString, out myint); Response.Write(myint + "<br>");
3个方法实现了同样的效果。但是,如果我们把要转换的字符串myString设为空:
//string myString = "1234"; string myString = null; int myint = 0; //方法1 myint = Convert.ToInt32(myString); Response.Write(myint + "<br>"); //方法2 myint = Int32.Parse(myString); Response.Write(myint + "<br>"); //方法3 Int32.TryParse(myString, out myint); Response.Write(myint + "<br>");
你会发现:
● Convert.ToInt32()在null时不抛出异常而是返回零。
● Int32.Parse()会抛出异常。
● Int32.TryParse()不抛出异常,会返回true或false来说明解析是否成功。如果解析错误,则out调用方将会得到零值。
从性能上讲,Int32.TryParse()优于Int32.Parse(),而Int32.Parse()优于Convert.ToInt32()。
建议:在.NET 1.1下用Int32.Parse();在.NET 2.0下用Int32.TryParse()。
1.3.10 几个常用的数学函数
这里列出了一些会经常用到的但却不常被人熟悉的和容易混淆的数学计算函数,方便读者在做数据计算的项目中使用。
● 返回大于或等于指定数字的最小整数。例如:
double a = Math.Ceiling(0.00); //0 double b = Math.Ceiling(0.40); //1 double d = Math.Ceiling(0.60); //1 double e = Math.Ceiling(1.00); //1 double f = Math.Ceiling(1.10); //2
● 返回小于或等于指定数字的最大整数。例如:
double g = Math.Floor(1.00); //1 double k = Math.Floor(1.90); //1 double l = Math.Floor(2.00); //2 double m = Math.Floor(2.10); //2
● 返回一指定数字被另一指定数字相除的余数。例如:
double y = Math.IEEERemainder(5.0, 3.0); //-1.0
● 返回两个32位有符号整数的商,并将余数作为输出参数传递。例如:
int ys; double s = Math.DivRem(5, 3, out ys); //s=1; ys=2
● 生成两个32位数字的完整乘积。例如:
long cj = Math.BigMul(2, 3); // 6
● 求两个数的商(但除数与被除数必须类型相同,结果类型相同)。例如:
double p = double.Parse(r.ToString()) / double.Parse(rows.ToString());
1.4 .NET的面向对象之门
继承、委托、事件和反射是.NET 开发中很重要的知识点,同时也一直是初学者比较难理解的知识点,就像一道门,经常困惑着.NET的初学者们。这一节将讲述继承在.NET中的不同,以及委托、事件和反射在.NET中的实现。
1.4.1 继承——“子承父业”
继承是面向对象思想的主要特征之一,继承的特性让代码具有了很好的复用性,大大提高了开发的效率。继承就是在类之间建立一种相交关系,使得新定义的派生类的实例可以继承已有的基类的特征和能力,而且可以加入新的特性或者是修改已有的特性建立起类的新层次。关于面向对象思想继承的基本概念可以阅读第13.3.2节继承部分的内容,这里着重讲一下在C#中继承的一些特性。
1.C#中的继承规则
● 继承是可传递的。如果C从B中派生,B又从A中派生,那么C不仅继承了B中声明的成员,同样也继承了A中的成员。Object类是所有类的基类。
● 派生类是对基类的扩展。派生类可以添加新的成员,但不能移除已经继承的成员的定义。
● 构造函数和析构函数不能被继承。除此以外的其他成员都能被继承。基类中成员的访问方式只能决定派生类能否访问它们。
● 派生类如果定义了与继承而来的成员同名的新成员,那么就可以覆盖已继承的成员。但这并不是删除了这些成员,只是不能再访问这些成员。
● 类可以定义虚方法、虚属性及虚索引指示器,它的派生类能够重载这些成员,从而使类可以展示出多态性。
● 派生类只能从一个类中继承,可以通过接口来实现多重继承。
下面是一个子类继承父类的代码示例。
代码示例: (示例位置:光盘\code\ch01\05)
public class ParentClass { public ParentClass() { Console.WriteLine("父类构造函数"); } public void SayHello() { Console.WriteLine("我是父类"); } } public class ChildClass : ParentClass //ChildClass 继承 ParentClass,通过冒号分隔 { public ChildClass() { Console.WriteLine("子类构造函数"); } public static void Main() { ChildClass child = new ChildClass(); child.SayHello();//继承调用父类的方法,显示"我是父类" } }
程序运行结果如图1-8所示。

图1-8 程序运行结果
ChildClass child = new ChildClass();依次调用父类构造函数和子类构造函数,然后child调用父类的SayHello方法。
2.访问基类成员
在派生类中访问基类中的成员或方法一般有以下两种方法:
● C#通过base.<方法名>()的方式调用基类的方法成员。
● 通过显式类型转换。
代码示例: (示例位置:光盘\code\ch01\06)
public class ParentClass { public ParentClass() { Console.WriteLine("父类构造函数"); } public void SayHello() { Console.WriteLine("我是父类"); } } public class ChildClass : ParentClass { public ChildClass() { Console.WriteLine("子类构造函数"); } public void Say() { base.SayHello(); //方法一:通过base.<方法名>() 的方式调用基类的方法 } public static void Main() { //base.SayHello(); //注:从静态方法中使用base 关键字是错误的 ChildClass child = new ChildClass(); child.Say(); child.SayHello(); ((ParentClass)child).SayHello(); //方法二:显式类型转换 } }
程序运行结果如图1-9所示。

图1-9 程序运行结果
3.隐藏基类成员
有的时候同一功能需要重新在派生类里实现新的逻辑,而不想用基类的方法,即隐藏掉父类的成员方法。C#使用new修饰符来实现隐藏基类成员。
代码示例: (示例位置:光盘\code\ch01\07)
public class ParentClass { public ParentClass() { Console.WriteLine("父类构造函数"); } public void SayHello() { Console.WriteLine("我是父类"); } } public class ChildClass : ParentClass { public ChildClass() { Console.WriteLine("子类构造函数"); } public new void SayHello() { Console.WriteLine("我是子类"); //隐藏了基类的成员 } public static void Main() { ChildClass child = new ChildClass(); child.SayHello();//显示"我是子类" } }
程序运行结果如图1-10所示。

图1-10 程序运行结果
4.密封类和密封方法
想想看,如果所有的类都可以被继承,那么继承的滥用会带来什么后果?类的层次结构体系将变得十分庞大,大类之间的关系杂乱无章,对类的理解和使用都会变得十分困难。有时候,我们并不希望自己编写的类被继承。另一些时候,有的类已经没有再被继承的必要。
C#提供了一个密封类(sealed class)的概念,帮助开发人员来解决这一问题。密封类在声明中使用sealed修饰符,这样就可以防止该类被其他类继承。如果试图将一个密封类作为其他类的基类,那么C#将提示出错。所以,密封类中不可能有派生类。
using System; public sealed class ParentClass { public ParentClass() { Console.WriteLine("父类构造函数"); } public void SayHello() { Console.WriteLine("我是父类"); } } public class ChildClass : ParentClass//报错: 无法从密封类型“ParentClass”派生 { }
同理,密封方法就是在方法前加sealed 修饰符,实现该成员方法不被重载。
5.抽象类和抽象方法
前面讲了不想被继承的情况,还有一种相反的情况是就是为了继承而生的。基类并不具体实现任何执行代码,只是做个定义。
在C#中通过把类或方法声明为abstract来实现抽象类和抽象方法,抽象类不能实例化,抽象方法没有具体执行代码,必须在非抽象的派生类中重写。
代码示例: (示例位置:光盘\code\ch01\08)
public abstract class ParentClass { public ParentClass() { Console.WriteLine("父类构造函数"); } public abstract void SayHello(); } public class ChildClass : ParentClass { public ChildClass() { Console.WriteLine("子类构造函数"); } public override void SayHello() { Console.WriteLine("我是子类"); //隐藏了基类的成员 } public static void Main() { ChildClass child = new ChildClass(); child.SayHello();//显示"我是子类" } }
程序运行结果如图1-11所示。

图1-11 程序运行结果
注 意
如果类中包括抽象方法,则类必须声明为抽象类。
但是,如果不想把类声明为抽象类,但又想实现方法在基类里不具体实现,而是在派生类中重写实现功能,该怎么办呢?
这个时候,可以通过把方法声明为虚函数(virtual)的形式来实现方法的重写。
代码示例: (示例位置:光盘\code\ch01\09)
public class ParentClass { public ParentClass() { Console.WriteLine("父类构造函数"); } public virtual void SayHello() { //虚函数必须声明方法主体。抽象方法可以不需要 } } public class ChildClass : ParentClass { public ChildClass() { Console.WriteLine("子类构造函数"); } public override void SayHello() { Console.WriteLine("我是子类"); //隐藏了基类的成员 } public static void Main() { ChildClass child = new ChildClass(); child.SayHello(); //显示"我是子类" } }
程序运行结果如图1-12所示。

图1-12 程序运行结果
6.多重继承
C#不支持多重继承,C#中类的继承只可以是一个,即子类只能派生于一个父类,但C#允许类派生于多个接口。有时你必须继承多个类的特性的话,那么为了实现多重继承可以使用接口技术。
但是,有一个问题,如果在派生类里面实现的这两个或多个接口有相同名称的方法,那么派生类如何来实现呢?
注 意
要实现多个接口的相同名称的方法,必须在接口的实现部分注意以下两点。
● 有相同名称的方法在实现时,前面不能加public等关键词。每个方法前必须冠以相应的接口名。例如:
void IFace1.Say(){……}; void IFace2.Say(){……};
● 对于不同名称的方法,前面必须冠以“public”标识符。例如:
public void Hello()
以上两个规则缺一不可,否则会收到编译错误。
创建派生类的实例时,如果调用某个接口的实现,则必须将实例强制转换为相应接口类型。例如:
FacetoFace facetest = new FacetoFace(); ((IFace1)facetest).Say(); ((IFace2)facetest).Say();
代码示例: (示例位置:光盘\code\ch01\10)
namespace InterfaceTest { interface IFace1 { void Say(); void Hello(); } interface IFace2 { void Say(); void Goodbye(); } //<summary> //派生类继承多个接口 //</summary> class FacetoFace : IFace1, IFace2 { #region 实现IFace1 成员 void IFace1.Say() { Console.WriteLine("这是IFace1的Say方法"); } public void Hello() { Console.WriteLine("IFace1向你说Hello!"); } #endregion #region 实现IFace2 成员 void IFace2.Say() { Console.WriteLine("这是IFace2的Say方法"); } public void Goodbye() { Console.WriteLine("IFace2向你说GoodBye"); } #endregion } //调用 class Program { static void Main(string[] args) { FacetoFace facetest = new FacetoFace(); ((IFace1)facetest).Say(); //将实例强制转换为相应接口类型IFace1 ((IFace2)facetest).Say(); //将实例强制转换为相应接口类型IFace2 facetest.Hello(); facetest.Goodbye(); } } }
程序运行结果如图1-13所示。

图1-13 程序运行结果
7.继承与访问修饰符
访问修饰符是一些关键字,用于指定声明的成员或类型的可访问性。类的继承中有4个访问修饰符:public、protected、private和internal。使用这些访问修饰符可指定下列5个可访问性级别:public、protected、private、internal和protected internal,其意义如表1-1所示。
表1-1 5个可访问性级别的意义

这些访问修饰符详细区别请阅读第2.3节。
1.4.2 委托——“任务书”
委托和事件在 .NET Framework中的应用非常广泛,然而,较好地理解委托和事件对很多刚接触C#的人来说并不容易。它们就像一道坎儿,过了这个槛的人,觉得真是太容易了,而没有过去的人每次见到委托和事件就觉得心里迷糊,浑身不自在。下面就通过简洁的语言和通俗的例子来讲解一下什么是委托、如何实现委托,如何使用委托,以及事件的使用与处理。
1.示例代码
代码示例: (示例位置:光盘\code\ch01\11)
public class ClassPeople { public void SayChinese(string name) { Console.WriteLine("你好," + name); } public void DoWork(string name) { SayChinese(name); //传字符串参数 } } class Program { static void Main(string[] args) { ClassPeople cp = new ClassPeople(); cp.DoWork("李天平"); System.Console.ReadLine(); } }
在这段代码中,我们通过把字符串作为参数传给 DoWork()方法输出问候信息。例如,我们传递字符串“李天平”进去,在这个方法中,将调用SayChinese方法,向屏幕输出“你好,李天平”。
2.通过条件判断进行扩展
现在假设这个程序需要进行全球化,但是,外国人看不懂“你好”是什么意思,怎么办呢?好吧,我们再加个英文版的问候方法:
代码示例: (示例位置:光盘\code\ch01\12)
public class ClassPeople { public void SayChinese(string name) { Console.WriteLine("你好," + name); } public void SayEnglish(string name) { Console.WriteLine("Hello," + name); } public void DoWork(string name) { SayChinese(name);//传字符串参数 } } class Program { static void Main(string[] args) { ClassPeople cp = new ClassPeople(); cp.DoWork("李天平"); Console.ReadLine(); } }
虽然加了英文版的问候方法,但是,DoWork方法并不知道什么时候该调用哪个方法进行输出。所以,在调用之前,我们还需要再定义一个枚举来进行判断:
代码示例: (示例位置:光盘\code\ch01\13)
using System; namespace ConsoleApplication1 { public class ClassPeople { public void SayChinese(string name) { Console.WriteLine("你好," + name); } public void SayEnglish(string name) { Console.WriteLine("Hello," + name); } public enum Language { English, Chinese } public void DoWork(string name, Language lang) { switch (lang) { case Language.English: SayEnglish(name); break; case Language.Chinese: SayChinese(name); break; } } } class Program { static void Main(string[] args) { ClassPeople cp = new ClassPeople(); cp.DoWork("李天平", ClassPeople.Language.Chinese); cp.DoWork("litianping", ClassPeople.Language.English); System.Console.ReadLine(); } } }
程序运行结果如图1-14所示。

图1-14 条件判断输出结果
这样问题就解决了,可以根据传进来的语言枚举值和姓名字符串来输出相应语言的问候语了。不过,细想一下这个方法的可扩展性还是太差了,如果以后我们需要再添加韩文版、日文版,就不得不反复修改枚举和DoWork()方法,以适应新的需求。
那么,有没有更好的解决方案呢?
3.引入委托
我们可以先来看一下DoWork()方法的声明:
public void DoWork(string name, Language lang)
我们可以看到,在这个方法里,我们传入了string name参数,string是参数类型,name是参数变量。我们赋给它“李天平”,它就把“李天平”这个值传进去;我们赋给它“litianping”,它就把“litianping”这个值传进去。然后,根据Language类型判断用哪个方法进行输出处理。
我们假设 DoWork()可以接收一个参数变量,这个参数变量可以代表一个方法,那么当我们把这个参数变量赋值为SayEnglish()时,它就代表SayEnglish()方法去执行;当我们把这个参数变量赋值为 SayChinese()时,它就代表 SayChinese()方法去执行。我们将这个参数变量暂且命名为MakeSay,那么不是可以像给name赋值一样,在调用DoWork()方法的时候,给这个MakeSay参数也赋上值(SayEnglish或者SayChinese等)传递进去吗? 然后,我们在DoWork()方法内,也可以像使用别的参数一样使用MakeSay。由于MakeSay代表一个方法,所以它的使用方式应该和它被赋值的方法(SayEnglish或者SayChinese)是一样的,接收一个name参数。
例如:
void MakeSay(string name)
这样的话,我们的DoWork()方法就会演变成如下:
public void DoWork(string name, *** MakeSay) { MakeSay(name); //传字符串参数 }
仔细看参数部分,这里的“***”这个位置,通常应该是参数的类型。但到目前为止,我们还不知道这个参数的类型应该是什么。但是我们可以看到如果把那两个方法当做参数传进去,就不需要再做枚举判断了,直接用传进来的方法输出问候语就可以了。这样是不是就完全解决了上面的问题了呢。
对,你现在应该可以明白了,这个解决的方法是可行的,这就是委托。
string定义了name参数所代表的值的类型,委托就是定义MakeSay参数所代表的方法的类型。
在C#中,定义委托的语法是:
delegate void SayDelegate(string name);
delegate关键字用于声明一个引用类型,该引用类型可用于封装命名方法或匿名方法。
和上面的SayChinese(string name)或者SayEnglish(string name)方法定义对比一下,除了加入了delegate关键字以外,其余的完全一样。
现在,我们再回来看DoWork()方法,就会演变成如下:
public void DoWork(string name,SayDelegate MakeSay) { MakeSay(name); //传字符串参数 }
如你所见,委托 SayDelegate 出现的位置与 string 一样,string 是一个类型,那么SayDelegate应该也是一个类型,或者叫类(Class)。但是委托的声明方式和类完全不同,这是怎么回事呢?实际上,委托在编译的时候确实会编译成类。因为Delegate是一个类,所以在任何可以声明类的地方都可以声明委托。
现在,让我们看一下这个范例的完整代码:
代码示例: (示例位置:光盘\code\ch01\14)
using System; namespace ConsoleApplication1 { public delegate void SayDelegate(string name); public class ClassPeople { public void SayChinese(string name) { Console.WriteLine("你好," + name); } public void SayEnglish(string name) { Console.WriteLine("Hello," + name); } //注意此方法,它接受一个SayDelegate类型的方法作为参数 public void DoWork(string name, SayDelegate MakeSay) { MakeSay(name); } } class Program { static void Main(string[] args) { ClassPeople cp = new ClassPeople(); cp.DoWork("李天平", cp.SayChinese); cp.DoWork("litianping", cp.SayEnglish); System.Console.ReadLine(); } } }
输出结果和枚举的方式相同,但具有更好的扩展性,如图1-15所示。

图1-15 委托输出结果
4.委托总结
委托是一种特殊的对象类型,它定义了方法的类型,使得可以将方法当做另一个方法的参数来进行传递,其特殊之处在于,我们以前定义的所有对象都包含数据,而委托包含的只是方法的地址。这种将方法动态地赋给参数的做法,可以避免在程序中大量使用 if…else、switch语句,同时使得程序具有更好的扩展性。
趣味理解
以前,公司过年过节,总会发一些福利(如奖金、礼品等),都是把实实在在的物质给大家(相当于传具体类型参数值给方法)。今年全球金融危机,整个经济不景气,公司效益也不好,不能给大家发物质的东西了。怎么办呢?老板眼珠一转,心生一计,决定给大家发“任务”,例如,销售公司产品任务,谁销售多少公司产品,这个钱就归谁,这样,不但公司减轻了负担,而且也解决了福利问题,一举两得啊。
但只是公司这么一说是不安全的,没有法律保证,所以需要一个新的东西来实现—任务书。(这里的任务书就是委托)任务书只是一种特殊的福利类型,其特殊之处在于,我们以前拿到的所有东西都是具体的物质。而任务书包含的只是获得物质的任务的说明,需要通过另外的劳动才能得到。
5.多播委托
上面讲述了什么是委托,是不是开始有点感觉了?我们继续往下看什么是多播委托。
在上面的例子中,name 值是直接赋给 DoWork()方法的,即 DoWork("李天平", cp.SayChinese);,但在大多数情况下我们习惯于用变量来赋值,如:
代码示例: (示例位置:光盘\code\ch01\15)
class Program { static void Main(string[] args) { ClassPeople cp = new ClassPeople(); string name1 = "李天平"; string name2 = "litianping"; cp.DoWork(name1, cp.SayChinese); cp.DoWork(name2, cp.SayEnglish); System.Console.ReadLine(); } }
而既然委托 SayDelegate 和类型 string 的地位一样,都是定义了一种参数类型,那么,我们是不是也可以这么使用委托呢?
代码示例: (示例位置:光盘\code\ch01\16)
class Program { static void Main(string[] args) { ClassPeople cp = new ClassPeople(); string name1 = "李天平"; string name2 = "litianping"; SayDelegate delegate1=cp.SayChinese; SayDelegate delegate2=cp.SayEnglish; cp.DoWork(name1, delegate1); cp.DoWork(name2, delegate2); System.Console.ReadLine(); } }
按【F5】键运行,没有问题,程序一样地输出结果。
但是,有一点需要说明,即委托不同于string的一个特性:它可以将多个方法赋给同一个委托,或者说将多个方法绑定到同一个委托,这就是多播委托。当调用这个委托的时候,将依次调用其所绑定的方法。在这个例子中,语法如下:
代码示例: (示例位置:光盘\code\ch01\17)
class Program { static void Main(string[] args) { ClassPeople cp = new ClassPeople(); string name1 = "李天平"; SayDelegate delegate1 = cp.SayChinese;//先给委托类型的变量赋值 cp.SayEnglish; //给此委托变量再绑定一个方法 cp.DoWork(name1, delegate1);//将先后调用SayChinese和SayEnglish 两个方法 System.Console.ReadLine(); } }
程序运行结果如图1-16所示。

图1-16 多播输出结果
调用了一次委托,输出了两个版本的结果。
注 意
第1次用的“=”,是赋值的语法;第2次用的“+=”,是绑定的语法。如果第1次就使用“+=”,将出现“使用了未赋值的局部变量”的编译错误。如果第2次还使用“=”,那么就会覆盖掉第1次的赋值
我们也可以使用下面的代码来这样简化这一过程:
SayDelegate delegate1 = new SayDelegate(cp.SayChinese); delegate1 += cp.SayEnglish; //给此委托变量再绑定一个方法
既然可以给委托绑定一个方法,那么也应该可以取消对方法的绑定,很容易想到,这个语法就是“-=”,如:
delegate1 -= cp.SayEnglish; //取消对SayEnglish方法的绑定
1.4.3 事件——“年终分红”
基于Windows的应用程序也是基于消息的。这说明应用程序是通过Windows来通信的, Windows又是使用预定义的消息与应用程序来通信的。这些消息是包含各种信息的结构,应用程序和Windows使用这些信息决定下一步的操作。.NET把这些传送来的消息封装在事件中。如果需要响应某个消息,就应处理对应的事件。一个常见的例子是用户单击了窗体中的按钮后,Windows就会给按钮消息处理程序发送一个WM_MOUSECLICK消息。对于.NET开发人员来说,这就是按钮的Click事件。
在开发基于对象的应用程序时,需要使用另一种对象通信方式。在一个对象中发生了有趣的事情时,就需要通知其他对象发生了什么变化。这里又要用到事件。就像.NET Framework把Windows消息封装在事件中那样,也可以把事件用做对象之间的通信介质。
委托就用做应用程序接收到消息时封装事件的方式。在上一节介绍委托时,仅讨论了理解事件如何工作及所需要的内容。但Microsoft设计C#事件的目的是让用户无须理解底层的委托就可以使用它们。
注 意
这里的术语“事件”有两种不同的含义。第一,表示发生了某件有趣的事情;第二,表示 C#语言中已定义的一个对象,即处理通知过程的对象。在使用第 2个含义时,我们常常把事件表示为 C#事件,或者在其含义很容易从上下文中看出时,就表示为事件。
趣味理解
在上节委托的趣味理解中我们讲到了公司过年时把任务书发给大家当福利。其实年终分红这样一个过程就是一个事件:年终分红事件。任务书好比委托,用它来传递消息给这个事件的接收者(员工),接收者再去处理这个事件(销售产品)。
所以下面开始从客户软件的角度讨论事件,主要考虑的是需要编写什么代码来接收事件通知,而无须担心后台上究竟发生了什么,从中可以看出事件的处理十分简单。之后,编写一个生成事件的示例,介绍事件和委托之间的关系。
1.从接收器的角度讨论事件
事件接收器是指在发生某些事情时被通知的任何应用程序或对象。当然,有事件接收器就有事件发送器。发送器的作用是引发事件。发送器可以是应用程序中的另一个对象或程序集,在系统事件中,例如鼠标单击或键盘按键,发送器就是.NET 运行库。注意,事件的发送器并不知道接收器是谁。这就使事件非常有用。
现在,在事件接收器的某个地方有一个方法,它负责处理事件。在每次发生已注册的事件时,就执行这个事件处理程序。此时就要使用委托了。由于发送器对接收器一无所知,所以无法设置两者之间的引用类型,而是使用委托作为中介。发送器定义接收器要使用的委托,接收器将事件处理程序注册到事件中。连接事件处理程序的过程称为封装事件。封装 Click事件的简单例子有助于说明这个过程。(完整代码示例位置:光盘\code\ch01\18)
首先创建一个简单的Windows窗体应用程序,把一个按钮控件从工具箱拖放到窗体上。在属性窗口中把按钮重命名为buttonOne。在代码编辑器中把下面的代码添加到Form1构造函数中:
public Form1() { InitializeComponent(); buttonOne.Click += new EventHandler(Button_Click); }
注 意
在Visual Studio中,在输入“+=”运算符之后,就只需按下“Tab”键两次,编辑器就会完成剩余的输入工作。在大多数情况下这很不错。但在这个例子中,不使用默认的处理程序名,所以应自己输入文本。
这将告诉运行库,在引发 buttonOne 的 Click 事件时,应执行 Button_Click 方法。EventHandler 是事件用于把处理程序(Button_Click)赋予事件(Click)的委托。注意使用+=运算符把这个新方法添加到委托列表中。这类似于本章前面介绍的多播委托示例。也就是说,可以为事件添加多个事件处理程序。由于这是一个多播委托,所以要遵循添加多个方法的所有规则,但是不能保证调用方法的顺序。
下面在窗体上再添加一个按钮,把它重命名为buttonTwo。把buttonTwo的Click事件也连接到同一个Button_Click方法上,如下所示:
buttonOne.Click += new EventHandler(Button_Click); buttonTwo.Click += new EventHandler(Button_Click);
利用委托推断,可以编写下面的代码。编译器会生成与前面相同的代码。
buttonOne.Click += Button_Click; buttonTwo.Click += Button_Click;
EventHandler 委托已在.NET Framework 中定义了。它位于 System 命名空间内,所有在.NET Framework 中定义的事件都使用它。如前所述,委托要求添加到委托列表中的所有方法都必须有相同的签名。显然事件委托也有这个要求。下面是Button_Click方法的定义:
private void Button_Click(object sender, EventArgs e) { }
这个方法有几个重要的地方。
● 首先,它总是返回void。事件处理程序不能有返回值。
● 其次是参数。只要使用EventHandler委托,参数就应是object和EventArgs。
第1个参数是引发事件的对象,在这个例子中是buttonOne或buttonTwo,这取决于被单击的按钮。把一个引用发送给引发事件的对象,就可以把同一个事件处理程序赋予多个对象。例如,可以为几个按钮定义一个按钮单击处理程序,接着根据sender参数确定单击了哪个按钮。
第2个参数EventArgs是包含有关事件的其他有用信息的对象。这个参数可以是任意类型,只要它派生自 EventArgs 即可。MouseDown 事件使用MouseDownEventArgs,它包含所使用按钮的属性、指针的X和Y坐标,以及与事件相关的其他信息。注意,其命名模式是在类型的后面加上 EventArgs。本章的后面将介绍如何创建和使用基于EventArgs的定制对象。
注 意
方法的命名应按照约定,事件处理程序应遵循“object_event”的命名约定。object就是引发事件的对象,而event就是被引发的事件。从可读性来看,应遵循这个命名约定。
在Button_Click处理程序中添加了一些代码,以完成一些工作。记住有两个按钮使用同一个处理程序。所以首先必须确定是哪个按钮引发了事件,接着调用应执行的操作。在本例中,只是在窗体的一个标签控件上输出一些文本。把一个标签控件从工具箱拖放到窗体上,并将其命名为labelInfo,然后在Button_Click方法中编写如下代码:
private void Button_Click(object sender, Eventargs e) { if (((Button)sender).Name == "buttonOne") labelInfo.Text = "buttonOne被单击"; else labelInfo.Text = "buttonTwo被单击"; }
由于sender参数作为对象发送,所以必须把它的数据类型转换为引发事件的对象类型,在本例中就是Button。本例使用Name属性确定是哪个按钮引发了对象,也可以使用其他属性。例如Tag属性就可以处理这种情形,因为它可以包含任何内容。为了了解事件委托的多播功能,给buttonTwo的Click事件添加另一个方法。窗体的构造函数如下所示:
buttonOne.Click += new EventHandler(Button_Click); buttonTwo.Click += new EventHandler(Button_Click); buttonTwo.Click += new EventHandler(button2_Click);
如果让Visual Studio创建存根(stub),就会在源文件的末尾得到如下方法。但是,必须添加对MessageBox.Show()函数的调用:
private void button2_Click(object sender, EventArgs e) { MessageBox.Show("这个仅仅出现在buttonTwo单击事件中"); }
如果使用匿名方法,那么就不需要 Button_Click方法和 Button2_Click 方法了。事件的代码如下:
public Form1() { InitializeComponent(); buttonOne.Click += (sender, e) => labelInfo.Text = "buttonOne被单击"; buttonTwo.Click += (sender, e) => labelInfo.Text = "buttonTwo被单击"; buttonTwo.Click += (sender, e) => { MessageBox.Show("这个仅仅出现在buttonTwo单击事件中"); }; }
在运行这个例子时,单击buttonOne会改变标签上的文本。单击buttonTwo不仅会改变文本,还会显示消息框。
注 意
不能保证标签文本在消息框显示之前改变,所以不要在处理程序中编写具有依赖性的代码。
我们已经学习了许多概念,但要在接收器中编写的代码量是很小的。编写事件接收器常常比编写事件发送器要频繁得多。至少在Windows用户界面上,Microsoft已经编写了所有需要的事件发送器(它们都在.NET基类中,在Windows.Forms命名空间中)。
2.创建事件
接收事件并响应它们仅是事件的一个方面。为了使事件真正发挥作用,还需要在代码中创建和引发事件。下面的例子将介绍如何创建、引发、接收和取消事件。
示例描述:
在一个窗体中引发“另一个类正在监听”的事件。在引发事件后,接收对象就确定是否执行一个过程,如果该过程未能继续,那么就取消事件。本例的目标是确定当前时间的秒数是大于30还是小于30。如果秒数小于30,那么就用一个表示当前时间的字符串设置一个属性;如果秒数大于 30,那么就取消事件,把时间字符串设置为空。(完整代码示例位置:光盘\code\ch01\19)
首先,我们创建一个窗体,并添加一个按钮和一个标签。把按钮命名为buttonRaise,标签命名为labelInfo。然后,我们就可以创建事件和相应的委托了。在窗体类的类声明部分,添加如下阴影部分中的三行代码:
public partial class Form1 : Form { public delegate void ActionEventHandler(object sender, ActionCancelEventArgs ev); public static event ActionEventHandler Action; public Form1() { InitializeComponent(); } }
这两行代码的作用是什么?
首先,我们声明了一种新的委托类型 ActionEventHandler。必须创建一种新委托,而不使用.NET Framework预定义的委托,其原因是后面要使用定制的EventArgs类。方法签名必须与委托匹配。有了一个要使用的委托后,下一行代码就定义事件。在本例中定义了Action事件,定义事件的语法要求指定与事件相关的委托。还可以使用在.NET Framework 中定义的委托,从EventArgs类中派生出了近100个类,应该可以找到一个自己能使用的类。但本例使用的是定制的EventArgs类,所以必须创建一个与之匹配的新委托类型。
在一行代码中定义事件是 C#中的一个缩写方式,它可以定义添加和删除处理程序的方法,声明委托的一个变量。除了编写一行代码之外,还可以用下面的代码达到相同的效果。声明一个事件类型的变量及添加和删除事件处理程序的方法。在定义添加和删除事件处理程序的方法时,其语法非常类似于属性。变量值的定义也类似于添加和删除事件处理程序。
private static ActionEventHandler action; public static event ActionEventHandler Action { add { action += value; } remove { action -= value; } }
基于 EventArgs 的新类 ActionCancelEventHandler 实际上派生自 CancelEventArgs,而CancelEventArgs派生自EventArgs。CancelEventArgs添加了Cancel属性,该属性是一个布尔值,它通知sender对象,接收器希望取消或停止事件的处理。在ActionEventHandler类中,还添加了 Message 属性,这是一个字符串属性,包含事件处理状态的文本信息。下面是ActionCancelEventHandler类的代码:
public class ActionCancelEventArgs : System.ComponentModel.CancelEventArgs { public ActionCancelEventArgs() : this(false) { } public ActionCancelEventArgs(bool cancel): this(false, String.Empty) { } public ActionCancelEventArgs(bool cancel, string message) : base(cancel) { this.Message= message; } }
public string Message { get; set; }
可以看出,所有基于EventArgs的类都负责在发送器和接收器之间来回传送事件的信息。在大多数情况下,EventArgs 类中使用的信息都由事件处理程序中的接收器对象使用的。但是,有时事件处理程序可以把信息添加到EventArgs类中,使之可用于发送器。这就是本例使用EventArgs类的方式。注意在EventArgs类中有两个可用的构造函数。这种额外的灵活性增加了该类的可用性。
目前声明了一个事件,定义了一个委托,并创建了EventArgs类。下一步需要引发事件。真正需要做的是用正确的参数调用事件,例如:
ActionCancelEventArgs e = new ActionCancelEventArgs(); Action(this, e);
这非常简单。创建新的 ActionCancelEventArgs 类,并把它作为一个参数传递给事件。但是,这有一个小问题。如果事件不会在任何地方使用,该怎么办?如果还没有为事件定义处理程序,该怎么办?Action事件实际上是空的。如果试图引发该事件,那么就会得到一个空引用异常。如果要派生一个新的窗体类,并使用该窗体,把 Action 事件定义为基事件,则只要引发了 Action 事件,就必须执行其他一些操作。目前,我们必须在派生的窗体中激活另一个事件处理程序,这样才能访问它。为了使这个过程容易一些,并捕获空引用错误,就必须创建一个方法 OnEventName,其中 EventName 是事件名。在这个例子中,有一个OnAction方法,下面是OnAction方法的完整代码:
protected void OnAction(object sender, ActionCancelEventArgs ev) { if (Action != null) { Action(sender, ev); } }
代码并不多,但完成了需要的工作。把该方法声明为protected,就只有派生类可以访问它。事件在引发之前还会进行空引用测试。如果派生一个包含该方法和事件的新类,就必须重写 OnAction 方法,然后连接事件。为此,必须在重写代码中调用 base.OnAction()。否则就不会引发该事件。在整个.NET Framework中都用这个命名约定,并在.NET SDK文档中对这一命名规则进行了说明。
注意传送给 OnAction 方法的两个参数。它们看起来很熟悉,因为它们与需要传送给事件的参数相同。如果事件需要从另一个对象中引发,而不是从定义方法的对象中引发,就需要把访问修饰符设置为 internal 或 public,而不能设置为 protected。有时让类只包含事件声明,这些事件从其他类中调用是有意义的。仍可以创建 OnEventName 方法,但此时它们是静态方法。
目前,我们已经引发了事件,还需要一些代码来处理它。在项目中创建一个新类BusEntity。本项目的目的是检查当前时间的秒数,如果它小于 30,那么就把一个字符串值设置为时间;如果它大于30,那么就把字符串设置为空,并取消事件。下面是代码:
public class BusEntity { string time = String.Empty; public BusEntity() { Form1.Action += new Form1.ActionEventHandler(Form1_Action); } private void Form1_Action(object sender, ActionCancelEventArgs e) { e.Cancel = !DoActions(); if (e.Cancel) e.Message = "不是正确的时间"; } private bool DoActions() { bool retVal = false; DateTime tm = DateTime.Now; if (tm.Second < 30) { time = "当前时间是:" + DateTime.Now.ToLongTimeString(); retVal = true; } else time = ""; return retVal; } public string TimeString { get { return time; } } }
在构造函数中声明了Form1.Action事件的处理程序。注意其语法非常类似于前面Click事件的语法。由于声明事件使用的模式都是相同的,所以语法也应保持一致。还要注意如何获取Action事件的引用,而无须在BusEntity类中引用Form1。在Form1类中,将Action事件声明为静态,这并不是必须的,但这样更易于创建处理程序。我们可以把事件声明为public,但接着需要引用Form1的一个实例。
在构造函数中编写事件时,调用添加到委托列表中的方法Form1_Action,并遵循命名标准。在处理程序中,需要确定是否取消事件。DoActions 方法根据前面描述的时间条件返回一个布尔值,并把_time字符串设置为正确的值。
之后,把DoActions的返回值赋给ActionCancelEventArgs的Cancel属性。EventArgs类一般仅在事件发送器和接收器之间来回传递值。如果取消了事件(ev.Cancel = true),那么Message属性就设置为一个字符串值,以说明事件为什么被取消。
再次查看buttonRaise_Click事件处理程序的代码,即可看出Cancel属性的使用方式:
BusEntity busEntity = new BusEntity(); private void buttonRaise_Click(object sender, EventArgs e) { ActionCancelEventArgs cancelEvent = new ActionCancelEventArgs(); OnAction(this, cancelEvent); if (cancelEvent.Cancel) labelInfo.Text = cancelEvent.Message; else labelInfo.Text = busEntity.TimeString; }
注意,创建了 ActionCancelEventArgs 对象。接着引发了事件 Action,并传递了新建的ActionCancel EventArgs对象。在调用 OnAction方法引发事件时,BusEntity对象中 Action事件处理程序的代码就会执行。如果还有其他对象注册了事件Action,那么它们也会执行。记住,如果其他对象也处理这个事件,那么它们就会看到同一个 ActionCancelEventArgs 对象。如果需要确定是哪个对象取消了事件,而且有多个对象取消了事件,则需要在ActionCancel EventArgs类中包含某种基于列表的数据结构。
在与事件委托一起注册的处理程序执行完毕后,就可以查询 ActionCancelEventArgs 对象,确定它是否被取消了。如果是,则labelInfo包含Message属性值;如果事件没有被取消,则labelInfo就会显示当前时间。
本节基本上说明了如何利用事件和事件中基于EventArgs的对象在应用程序中传递信息。
1.4.4 反射——“解剖”
反射(Reflection)是.NET中的重要机制,通过反射,可以在运行时获得.NET中每一个类型(包括类、结构、委托、接口和枚举等)的成员,包括方法、属性、事件,以及构造函数等。还可以获得每个成员的名称、限定符和参数等。有了反射,即可对每一个类型了如指掌。只要获得了构造函数的信息,即可直接创建对象,即使这个对象的类型在编译时还不知道。
趣味理解
反射好比人体解剖,可以将一个完整的人(Dll)解剖,得到里面的部位(成员),从而可以用这些部位去做一些事情。甚至可以通过获取人的 DNA 重新克隆一个完全相同的人(创建对象),重新得到一个新的人,让他去执行一些任务,呵呵,是不是很神奇,又有点像美国科幻大片了!
反射技术示例:
下面是反射技术的示例,我们可以在程序中得到动态实例化对象,获得对象的属性,并调用对象的方法。
我们建一个类库项目ClassLib,并添加如下代码。
代码示例: (示例位置:光盘\code\ch01\ClassLib)
using System; namespace ClassLib { public class ClassPerson { public ClassPerson() : this(null) { } public ClassPerson(string strname) { name = strname; } private string name=null; private string sex; private int age; public string Name { set { name = value; } get { return name; } } public string Sex { set { sex = value; } get { return sex; } } public int Age { set { age = value; } get { return age; } } public void SayHello() { if (name == null) { System.Console.WriteLine("Hello World"); } else { System.Console.WriteLine("Hello," + name); } } } }
编译以上代码,项目会自动生成一个 ClassLib.dll 文件。下面我们可以通过反射来获取ClassLib.dll中上面的对象信息。
首先,添加using System.Reflection的命名空间的引用,然后把上面生成的ClassLib.dll复制到本项目的程序集(bin\Debug)目录下。
代码示例: (示例位置:光盘\code\ch01\20)
using System; using System.Reflection; //引用命名空间 namespace ConsoleApplication1 { class Program { static void Main(string[] args) { Console.WriteLine("列出程序集中的所有类型"); Assembly ass = Assembly.LoadFrom("ClassLib.dll"); Type classPerson = null; Type[] mytypes = ass.GetTypes(); foreach (Type t in mytypes) { Console.WriteLine(t.Name ); if (t.Name == "ClassPerson") { classPerson = t; } } Console.WriteLine("列出ClassPerson类中的所有方法"); MethodInfo[] mif = classPerson.GetMethods(); foreach (MethodInfo mf in mif) { Console.WriteLine(mf.Name ); } Console.WriteLine("实例化ClassPerson,并调用SayHello方法"); Object obj = Activator.CreateInstance(classPerson); Object objName = Activator.CreateInstance(classPerson, "litianping"); MethodInfo msayhello = classPerson.GetMethod("SayHello"); msayhello.Invoke(obj, null); //空参数实例构造 msayhello.Invoke(objName, null); //带参数实例构造 System.Console.ReadLine(); } }
}
程序运行结果如图1-17所示。

图1-17 反射输出结果
1.5 .NET开发几把小刀
孔乙己自己知道不能和他们谈天,便只好向孩子说话。有一回对我说道,“你读过书么?”我略略点一点头。他说,“读过书,……我便考你一考。茴香豆的茴字,怎样写的?”我想,讨饭一样的人,也配考我么?便回过脸去,不再理会。孔乙己等了许久,很恳切地说道,“不能写罢?……我教给你,记着!这些字应该记着。将来做掌柜的时候,写账要用。”我暗想我和掌柜的等级还很远呢,而且我们掌柜也从不将茴香豆上账;又好笑,又不耐烦,懒懒地答他道,“谁要你教,不是草头底下一个来回的回字么?”孔乙己显出极高兴的样子,将两个指头的长指甲敲着柜台,点头说,“对呀对呀!……回字有四样写法,你知道么?”我愈不耐烦了,努着嘴走远。孔乙己刚用指甲蘸了酒,想在柜上写字,见我毫不热心,便又叹一口气,显出极惋惜的样子。
看到这段文字也许你会想起中学课文中学过的孔乙己的“茴香豆”的“茴”字的4种写法,如此严谨的习惯,孔先生如果活在当下能是个不错的程序员。下面我们也学学孔先生,来研究一下.NET中几个技术点中变幻的“刀法”,这几种“刀法”看似简单,但却容易被许多人忽略。掌握好这些不同的“刀法”有助于我们编写代码更快速而简洁,提高工作的效率。
1.5.1 using之多变身
1.using指令
格式:using 命名空间名字;
这样可以在程序中直接用命令空间中的类型,而不必指定类型的详细命名空间,类似于Java的import,这个功能也是最常用的,几乎每个cs的程序都会用到。
例如:using System; 一般都会出现在*.cs中。
2.using别名
格式:using 别名 = 包括详细命名空间信息的具体的类型;
这种做法有个好处就是如果同一个 cs 引用了两个不同的命名空间,但两个命名空间都包括了一个相同名字的类型,当需要用到这个类型的时候,每个地方就都要用详细命名空间的办法来区分这些相同名字的类型。而用别名的方法会更简洁,用到哪个类就给哪个类做别名声明就可以了。
namespace NameSpace1 { public class MyClass { public override string ToString() { return "这是在NameSpace1.MyClass"; } } } namespace NameSpace2 { class MyClass { public override string ToString() { return "这是在NameSpace2.MyClass"; } } } using System; using aClass = NameSpace1.MyClass; using bClass = NameSpace2.MyClass; namespace testUsing { class Program { static void Main(string[] args) { //等同:NameSpace1.MyClass my1 = new NameSpace1.MyClass(); aClass my1 = new aClass(); Console.WriteLine(my1); //等同:NameSpace2.MyClass my2 = new NameSpace2.MyClass(); bClass my2 = new bClass(); Console.WriteLine(my2); Console.Read(); } } }
3.using定义范围
即时释放资源,在范围结束时处理对象。当在某个代码段中使用了类的实例,而希望无论因为什么原因,只要离开了这个代码段就自动调用这个类实例的Dispose方法释放资源。例如:
using (Class1 cls1 = new Class1(), cls2 = new Class1()) { //使用对象cls1, cls2 } //调用Dispose 释放对象实例cls1 和cls2
到达using语句末尾或者中途引发了异常并且控制离开了语句块,即触发cls1和cls2的Dispose方法释放资源。
1.5.2 @符号的妙用
1.字符串转义符
由于“\”(单个反斜杠)在C#中是特殊符号,表示转义字符。所以如果要表示普通字符串“\”,则需要“\\”才可以。例如:
string s_FilePath ="C:\\Program Files\\Microsoft.NET\\test.txt";
通过@符号,可以实现将“\”当普通字符使用,例如:
string s_FilePath =@"C:\Program Files\Microsoft.NET\test.txt";
2.用@表示的跨行字符串
//以下是引用片段: string s_MultiRows = @"Line1 Line2 Line3"; string s_JavaScript = @" "; //从上一行跨到这行,中间并没有行结束符或字符串连接符号
3.保留关键字标识符
在 C#规范中,@可以作为标识符(类名、变量名、方法名等)的第一个字符,以允许C#中保留关键字作为自己定义的标识符。像class、static、bool等都是C#的保留关键字,并不能当做普通标识符来命名,而通过@符号前缀却可以把这些本来是 C#的保留关键字当做普通字符使用。例如:
class @class { public static void @static(bool @bool) { if (@bool) System.Console.WriteLine("true"); else System.Console.WriteLine("false"); } } class Class1 { static void Main() { @class.@static(true); } }
注 意
@虽然出现在标识符中,但不作为标识符本身的一部分。因此,以上示例定义了一个名为class的类,并包含一个名为static的方法,以及一个参数名为bool的形参。
这样便对跨语言的移植带来了便利。因为某个单词在C#中作为保留关键字,但是在其他语言中也许不是。
1.5.3 预处理指令,有你更轻松
预处理指令的开头都有符号#,在预处理指令里面,估计#region name、#endregion可能是大家使用得最多的,我也常用它们来进行代码分块,在一个比较长的 cs 文件中,这么做确实是一件可以让你使代码更清晰的好办法。
在C#中还有好几对预处理指令,可能很多人就用得比较少了。
1.#define和#undef
#define 定义一个符号,这有点类似于声明一个变量,但这个变量并没有真正的值,只是存在而已。这个符号不是实际代码的一部分,而只在编译器编译代码时存在。在 C#代码中它没有任何意义。
例如:#define DEBUG
#undef正好相反,是删除前面定义的符号。
例如:#undef DEBUG
如果符号不存在,#undef 就没有任何作用。同样,如果符号已经存在,#define 也不起作用。
必须把#define和#undef命令放在C#源代码的开头,放在声明要编译的任何对象的代码之前。
#define 本身并没有什么用,但与其他预处理器指令(特别是#if)结合使用时,它的功能就非常强大了。
注 意
预处理器指令不用分号结束,一般一行上只有一个命令。这是因为对于预处理器指令,C#不再要求命令用分号结束。如果它遇到一个预处理器指令,那么就会假定下一个命令在下一行上。
2.#if、#eif、#else和#endif
这些指令告诉编译器是否要编译某个代码块。
例如:
#define DEBUG using System; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { int n = 1; #if DEBUG //如果前面定义了DEBUG则执行,否则不执行 Console.WriteLine(n); #endif Console.ReadKey(); } } }
这段代码会像往常那样编译,但 Console.WriteLine(n);命令包含在#if 子句内。这句代码只有在前面的#define命令定义了符号DEBUG时才会被执行。当编译器遇到#if语句后,将先检查相关的符号是否存在,如果符号存在,则编译#if 块中的代码。否则,编译器会忽略所有的代码,直到遇到匹配的#endif指令为止。一般在调试时定义符号DEBUG,把与调试相关的代码放在#if子句中。在完成了调试后,就把#define语句注释掉,所有的调试代码都会奇迹般地消失,可执行文件也会变小,最终用户不会被这些调试信息弄糊涂(显然,要做更多的测试,确保代码在没有定义DEBUG的情况下也能工作)。这项技术在C\C++编程中非常普通,称为条件编译(Conditional Compilation)。
#elif (=else if)和#else指令可以用在#if块中,其含义非常直观。也可以嵌套#if块:
#define W2K using System; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { int n = 1; #if W2K //如果前面定义了DEBUG则执行,否则不执行 Console.WriteLine(n); #elif XP //do something else #else //do other something #endif Console.ReadKey(); } } }
3.#warning 和#error
如果编译器遇到#warning指令,则产生警告信息,给用户显示#warning后面的文本,之后继续编译。
如果编译器遇到#error指令,则给用户显示后面的文本,作为一个错误信息,然后立即退出编译。
例如:
public int GetNum2() { int n = 1; n++; #warning "正式发布时,别忘了去掉这一句调试信息" Console.WriteLine(n); return n; }
4.#region和#endregion
#region和#endregion指令用于把一段代码标记为一个指定名称的代码块,如下所示:
class Program { static void Main(string[] args) { int n = 1; string str = "test"; #region 输出信息 Console.WriteLine(n); Console.WriteLine(str); #endregion } }
1.6 Visual Studio.NET 2008 实战
我们已经熟悉了一些C#的基础知识,下面学习使用Visual Studio.NET 2008开发工具进行项目开发的一些实战经验和技巧知识。
Microsoft Visual Studio.NET是一个全面集成的开发环境,用于编写、调试代码,把代码编译为程序集进行发布。Visual Studio.NET历经2002→2003→2005→2008几个版本,功能已变得异常强大。Microsoft Visual Studio.NET已经成为目前最流行的Windows平台应用程序开发环境。Visual Studio.NET 2008是目前的最新版本。在Visual Studio.NET 2008中不但可以创建桌面应用程序(WinForm)和Web应用程序(ASP.NET),还可以创建SOA、Windows Service、Office和智能设备应用程序等。值得一提的是,与过去Visual Studio多半绑特定.NET版本的策略不同,VS 2008中,开发人员新增项目时可以选定不同的.NET版本,VS 2008会根据版本的不同变更参考的函数库。
1.6.1 如何创建ASP.NET项目
1.建立应用程序型网站
首先,打开VS.NET,选择菜单“文件”→“新建项目”命令,在如图1-18所示左边的“项目类型”面板中选择“Visual C#”,在右边的面板中选择“ASP.NET Web应用程序”。

图1-18 新建应用程序型网站
单击“确定”按钮,生成的项目文件列表如图1-19所示。

图1-19 应用程序型网站项目
注 意
在VS 2005中,默认设置是不支持这种方式的,需要安装VS 2005 SP1才可以支持该项目类型。如果VS2005不安装SP1,打开应用程序型的项目,则会提示“此安装不支持该项目类型”。这是我遇到的初学者遇到的最多的问题。具体安装可以参考:http://bbs.maticsoft.com/showtopic-3.aspx。
2.建立项目型网站
打开VS.NET,选择菜单“文件”→“新建网站”命令,在“新建网站”对话框中选择“ASP.NET网站”,如图1-20所示。

图1-20 新建网站
单击“确定”按钮,生成项目文件列表,如图1-21所示。

图1-21 网站文件列表
3.ASP.NET Web应用程序和ASP.NET网站的区别
Web Application模型的优点如下:
● 网站编译速度快。
● 生成的程序集如下:
Web Site生成随机的程序集名,需要通过插件Web Deployment才可以生成单一程序集。
Web Application可指定网站项目生成单一程序集,因为是独立的程序集,所以和其他项目一样可以指定应用程序集的名字、版本、输出位置等信息。
● 可以将网站拆分成多个项目以方便管理。
● 可以从项目中和源代码管理中排除一个文件。
● 支持VSTS的Team Build方便每日构建。
● 更强大的代码检查功能,并且检查策略受源代码控制。
● 可以直接升级使用原来用VS 2003构建的大型系统。
Web Site模型的优点如下:
● 动态编译该页面,马上可以看到效果,不用编译整个站点(主要优势)。
● 同上,可以使错误的部分和使用的部分互不干扰(可以要求只有编译通过才能签入)。
● 可以在每个页面生成一个程序集(不建议采用这种方式)。
● 可以把一个目录当做一个 Web 应用来处理,直接复制文件就可以发布,不需要项目文件。
● 可以把页面也编译到程序集中(一般用不到,Web Application 也可以通过 Web Deployment插件来实现)。
4.两种编程模型的互相转换
VS 2005 SP1和VS 2008都内置了转换程序,可以非常方便地从Web Site转换到Web Application。首先用VS 2008新建一个Web应用程序,然后把你的网站内容复制进去,在解决方案资源管理器里选中你的网站单击鼠标右键,在弹出的快捷菜单中选择“转换为 Web应用程序”命令即可。
未查到有专门的反向转换工具,但经过比较后发现如果转换也非常简单。删除所有*.designer.cs,将*.aspx、*.ascx、*.master 页面文件中的 Codebehind="FileList.aspx.cs" 批量替换成CodeFile="FileList.aspx.cs"。
相对而言,大网站比较适合用Web Application项目,小网站比较适合用Web Site项目。
注 意
VS 2005默认设置不支持ASP.NET Web应用程序类型,需要单独安装VS 2005的补丁SP1才可以支持ASP.NET Web应用程序。
VS 2008安装后默认设置是同时支持这两种类型的。
1.6.2 如何创建Windows项目
打开VS.NET,选择菜单“文件”→“新建项目”命令,在左边的“项目类型”面板中选择“Visual C#”选项,在右边的面板中选择“Windows 窗体应用程序”选项,如图1-22所示。

图1-22 创建Windows项目
单击“确定”按钮,生成项目文件列表,如图1-23所示。

图1-23 Windows项目文件列表
有关Windows窗体编程的详细知识可以直接阅读第4章。
1.6.3 Visual Studio.NET 2008操作与使用技巧
我们每天都使用VS.NET进行开发,无论你是久闯江湖的老手还是刚刚上道的菜鸟,在使用VS.NET进行开发的时候,有没有想过怎样利用VS提供的一些便利功能,来提高我们工作的质量和效率呢?下面总结了VS.NET中一些常用的技巧与功能,以帮助大家充分有效地利用工具进行开发。
1.加速你的开发环境
我们使用VS开发工具VS 2003到VS 2008时很多人都有类似的体会,巨慢的启动过程总是让我们忍无可忍,永远感觉到自己硬件的不足。但是穷人的孩子也要吃饭啊,有没有办法加快它的启动速度呢?答案当然是可以的。
1)禁用起始页
在默认情况下,起始页会提供最近的工程列表,但它是以 Web 页面的方式出现的,也就是说,它启动了IE的一个实例,这是VS 2008启动变慢的首因。
我们可以从“最近项目列表”中启动项目,那么就完全可以禁用掉起始页,方法很简单,依次选择“工具”→“选项”命令,如图1-24所示,在“启动时”下拉列表中选择“显示空环境”选项。

图1-24 禁用起始页
2)去掉启动屏
每次启动VS 2008的时候都会先显示启动画面窗口,其实我们可以选择“开始”→“运行”命令,在弹出的“运行”对话框中以命令行方式启动VS 2008,是加速起始过程的另一秘诀,我们经常使用“cmd”、“regedit”之类的命令,其实还有VS的启动命令“devenv”,秘诀就在这里,输入“devenv /nosplash”,看看发生了什么,起始屏不见了直接启动,是不是有飞快的感觉了!我们可以将这个命令参数在VS.NET的快捷图标方式里面使用。
选中快捷方式图标,然后单击鼠标右键,在弹出的快捷菜单中选择“属性”命令,如图1-25所示,在“目标”命令行中加入“/nosplash”参数即可。

图1-25 去掉启动屏参数
3)关闭动态帮助
另外一个显著减慢VS 2008运行速度的关键就是“动态帮助”,并且“动态帮助”不仅仅是减慢程序的启动速度,它还会一直影响VS 2008的整个使用过程,甚至有时会慢得让人难以忍受。一个简单的做法,是在VS 2008退出之前,关闭“动态帮助”窗口,这样他就不会在以后的开发过程中自动出现了。
2.快捷操作,高效开发
1)自定义快捷工具栏
VS 2008默认安装工具栏里并没有“重新生成解决方案”等这些常用的按钮,如图1-26所示。

图1-26 默认工具栏按钮
为了日常开发操作方便,可以将常用的命令放到工具栏上,以减少操作时间。
选择菜单“工具”→“自定义”命令,在弹出的对话框中选择“命令”选项卡,如图1-27和图1-28所示。

图1-27 选择按钮命令

图1-28 选择按钮命令
直接用鼠标单击,然后拖到工具栏上适当的位置即可。
增加按钮后的工具,就变成我们自定义的工具栏,如图1-29所示。

图1-29 增加快捷命令的工具栏
这样,每次编译和生成项目时,操作起来会方便许多。
2)多文件查看
在VS.NET中,可以同时查看2个或多个文件,只需在打开选项卡中把想要查看的文件拖至IDE的右边或下边,此时会出现一个虚线框,如图1-30所示,然后释放鼠标,即可以垂直或水平平铺的方式查看文档,如图1-31所示。

图1-30 拖动鼠标出现虚线框

图1-31 多文件查看
3)拆分代码窗口
当需要对同一文档的不同部分代码进行查看的时候,可以通过拆分代码窗口来查看代码的不同部分。将鼠标移动到代码窗口右上角滚动条的上方,出现双向箭头时向下拖至你想拆分的位置,或者选择菜单“窗口”→“拆分”命令;即可通过移动滚动条来查看代码的不同部分,如图1-32所示。

图1-32 拆分代码窗口
4)管理重复使用的代码片断
在写代码时,有一些常用的代码片断会在不同的项目或解决方案中重复使用,如文件创建说明、数据库连接字符串等。可以使用工具箱利用以下方法进行重用:
● 选择要复用的代码片断。
● 将选择的代码拖到工具箱的常规卡上,工具箱在会显示“文本……”(可以通过单击鼠标右键,在弹出的快捷菜单中选择“重命名”命令进行改名)。
● 使用时,在插入代码的位置单击鼠标,然后在工具箱上双击要插入的代码(也可直接把代码片断拖到要插入的位置),如图1-33所示。

图1-33 管理代码片断
5)使用渐进式搜索
代码的搜索方法,VS.NET可以使用编辑菜单中的“搜索”命令进行特定字符串的搜索,也可以使用“查找符号”命令来查找特定的方法或属性,但“渐进式搜索”可能就少有人知了,“渐进式搜索”可以根据你输入的字符在当前打开的代码中进行查找。使用“Ctrl+I”组合键,在代码窗口中会出现一个向下的箭头加望远镜图案,状态栏显示“渐进式搜索”字样,输入要查找的字符,系统会自动定位至字符出现的位置。查找下一个字符出现位置可以再按“Ctrl+I”组合键;查找上一个字符出现位置可以按“Shift+Ctrl+I”组合键继续进行查找。
1.6.4 常见开发调试技巧
“世界上没有不出错误的软件”,即使人都不能避免犯错误,因此任何人编写软件时,都会犯一些错误。“错误是令人讨厌的”,正如做人一样,错误在所难免,但改了错误还是好同志嘛。所以,我们要学会如何跟踪错误,找到错误,以便改正错误,这样我们的程序才会越来越完美。
1.设置断点,跟踪调试
日常开发用得最多的就是设置断点,逐语句跟踪调试。在需要设置断点的语句行按“F9”键,当前行代码变为红色背景选中,该行即设置了一个断点,如图1-34所示。

图1-34 设置断点
当程序执行到该语句时便停下来,如图1-35所示,把控制权交给调试程序。

图1-35 调试程序
通过按“F10”键逐进程调试,按“F11”键逐语句调试。
条件断点:在满足一定条件的情况下,程序才停下来等待调试。如图1-36所示,设置断点的语句行,单击鼠标右键,在弹出的快捷菜单中选择“断点”→“条件”命令,出现如图1-37所示的“断点条件”对话框。

图1-36 条件断点

图1-37 设置断点条件
输入断点的条件,如n==5,单击“确定”按钮,运行程序,当n的值等于5的时候,程序就会在断点位置停下来等待调试。
注 意
这里是n==5,不是n=5,否则就成了赋值,程序永远也停不下来了。
当程序设置了很多断点时,可以通过按“Ctrl+Shift+F9”组合键来删除所有断点。
2.页面跟踪
在页面顶部添加Trace="true" 指令,如图1-38所示,即启动该页面的跟踪。

图1-38 页面跟踪设置
添加 TraceMode="SortByCategory" 指令可以根据类别进行排序。(完整代码示例位置:光盘\code\ch01\21)
运行效果如图1-39所示。

图1-39 页面跟踪效果
我们发现页面显示除了正常信息以外,在页面底部还增加了一堆调试信息。
下面讲解关于自定义页面跟踪信息的知识。
使用Trace.Write和Trace.Warn方法,可以向页面跟踪信息中添加自己想输出的跟踪信息。例如:
protected void Page_Load(object sender, EventArgs e) { string strID=Request.Params["id"]; Trace.Write("Page_Load中", "strID的值是:"+strID); Trace.Warn("Page警告信息", "PageLoad被执行"); }
跟踪效果如图1-40所示。

图1-40 自定义页面跟踪
当然,不一定非要通过页面设置来开启跟踪,也可以通过CS代码中的Trace.IsEnabled=true或false来开启或关闭当前页面的跟踪。
3.调试客户端脚本
在ASP.NET开发中,客户端脚本可以提高Web程序与客户的交互能力,降低客户端与服务的数据传输。能够在很大程度上提高用户的使用体验。但客户端脚本难以调试却是开发人员很头疼的一件事情。然而,其实有很多的方法可以帮助我们解决这样一种状况,来很好地完成我们的客户端脚本的开发。
下面就用一步步的操作来描述一下怎样利用VS.NET中的调试器来调试JavaScript。
(1)启用客户端脚本调试。
打开IE,选择菜单“工具”→“Internet选项”命令,在弹出的对话框中单击“高级”选项卡,取消勾选“禁用脚本调试”复选框,如图1-41所示。

图1-41 禁用脚本调试
单击“确定”按钮,并关掉当前所有IE窗口。
(2)打开VS.NET 2008,创建或打开一个ASP.NET项目,在需要调试的页面脚本上设置断点,如图1-42所示。

图1-42 脚本调试
(3)在VS.NET 2008中按“F5”或“F10”键启动调试,当脚本运行到断点时会停下来,我们就可以一步步地进行调试了,如图1-43所示。

图1-43 单步调试
注 意
在VS 2005中是无法在第(2)步直接设置断点的,需要先启动调试运行要调试的应用程序页面,然后在IE页面选择菜单“查看”→“脚本调试程序”→“打开”命令,如图1-44所示。

图1-44 打开脚本调试
这时回到VS 2005设置断点,重新刷新页面即可进行调试。
4.应用程序级跟踪
应用程序级的跟踪允许查看整个应用程序的跟踪,每个页面不再需要单独设置Trace="true" 指令,但它却收集了每个请求页面的跟踪信息,用户却看不到任何内容。
首先设置web.config,添加<trace enabled="true"/>配置。
<configuration>
<system.web>
<trace enabled="true"/>
</system.web>
</configuration>
可以通过根目录/Trace.axd来查看跟踪信息,如图1-45所示。

图1-45 应用程序级跟踪
注 意
此文件实际并不存在,它是一个特殊的被ASP.NET截获的URL,它会给出请求的列表。
单击请求的查看详细信息链接,就会显示相应页面的跟踪信息细节。
1.6.5 错误异常处理方法
异常是一些程序在非正常情况下发生的错误。错误的出现并不一定总是程序员的原因,与缺陷(bug)不同,bug 是程序员的疏漏,它们应该在产品发布前被更正;尽管一个 bug可能引发异常的抛出,你不应该完全依靠异常来处理你的bug,它至多是你测试的手段,你应该自己尽量更正那些bug。类似的,错误(error)是由用户操作引起的,例如在一个应该输入数字的地方输入了一个字母;虽然它也可能引发异常,但你应该通过校验代码来过滤这些错误。无论何时,在可能的情况下错误都应该是能预料和能被预防的。即使你改了所有的bug和列举了所有可能的用户错误,你仍会遇到无法预料和阻止的异常,如内存耗尽、网络中断、文件丢失等。无论编程技术有多高,程序都必须能处理可能发生的错误或异常。你无法预防异常,但你能处理它们,以避免它们使你的程序崩溃。
1.使用try-catch-finally捕获异常
try { 获取并使用资源,可能出现异常的代码 } catch { 捕获异常,处理异常 } finally { 无论何种情况下都会被执行的代码, 如释放资源,关闭一个文件等等。 }
注 意
即使代码抛出异常,并在catch块中添加了显式的return语句,finally块中的代码仍会被执行。finally块只需要一个try块,catch 块的存在与否对其并没有影响。使用break、continue、return语句退出finally块都是非法的。
例如:
try { string filepath = this.textBoxfilepath.Text; long isize; fs = File.Open(filepath, FileMode.Open); isize = fs.Length; } catch { MessageBox.Show("打开文件失败!"); } finally { fs.Close(); }
这段代码运行时,当有异常出现(如文件不存在)时程序会显示我们自定义的一条简单的“打开文件失败!”警告,因为try块捕获住了异常并立即转到catch块中的代码继续执行。
注 意
如果catch块中没有退出的代码(如return、throw),则catch块后的代码将继续得到执行。并且try 块后面至少需要包含一个 catch 块或一个finally块。
你可以在一个try块后添加多个catch 块,以便对不同的异常情况作出不同的处理。以下的代码就列举了对几个不同的异常进行不同的处理的情况。
try { string filepath = this.textBoxfilepath.Text; long isize; fs = File.Open(filepath, FileMode.Open); isize = fs.Length; } //截获因I/O 错误或指定类型的安全错误而拒绝访问的异常 catch (UnauthorizedAccessException uex) { MessageBox.Show(uex.Message); } catch (FileNotFoundException fex) //截获访问磁盘上不存在的文件失败时引发的异常 { MessageBox.Show(fex.Message); } //截获调用不支持的方法或读取或写入不支持调用功能的流的异常 catch (NotSupportedException nex) { MessageBox.Show(nex.Message); } catch (ArgumentException aex) //截获在向方法提供的其中一个参数无效时引发的异常 { MessageBox.Show(aex.Message); } catch //截获所有其他异常 { MessageBox.Show("打开文件失败!"); } finally { fs.Close(); }
注 意
catch块的次序必须十分小心,比如一个DivideByZeroException 异常继承自ArithmeticException异常,如果你先捕获后者,则当除数为0时抛出的异常就会进入ArithmeticException块而永远不会进入DivideByZeroException块。事实上,当这种情况出现时,编译器会发现 DivideByZeroException 块不能被执行,并会报告一个编译错误。
2.ASP.NET异常处理
ASP.NET中除了使用以上方法截获错误以外,还可以采用以下3种方法截获其他一些意想不到的错误。
1)页面级错误处理
在单独页面中的错误。可以在 page_error 事件中添加处理逻辑代码,通过Server.GetLastError()获取当前页面的错误。
代码示例: (示例位置:光盘\code\ch01\22)
protected void Page_Error(object sender, EventArgs e) { string errMsg=""; Exception currentError = Server.GetLastError(); errMsg += "系统发生错误:<br/>" + "错误地址:" + Request.Url.ToString() + "<br/>" + "错误信息:" + currentError.Message.ToString() + "<br/>"; Response.Write(errMsg); Server.ClearError();//要注意这句代码的使用,清除异常。 }
注 意
如果少掉了Server.ClearError()方法的话,仍然会引发Application_Error的错误处理。
2)应用程序级错误处理
截获整个应用程序中运行的错误,可以在项目的 Global.asax 文件中的 application_error中添加处理逻辑。
代码示例: (示例位置:光盘\code\ch01\22)
protected void Application_Error(object sender, EventArgs e) { Exception ex = Server.GetLastError(); string errmsg = ""; string Particular = ""; if (ex.InnerException != null) { errmsg = ex.InnerException.Message; Particular = ex.InnerException.StackTrace; } else { errmsg = ex.Message; Particular = ex.StackTrace; } AddLog(errmsg, Particular); Server.ClearError();//处理完及时清理异常 }
3)应用程序配置
可以在web.config中配置一些常见错误的处理方法。
<system.web> <customErrors mode="On" defaultRedirect="GenericErrorPage.htm"> <error statusCode="403" redirect="NoAccess.htm" /> <error statusCode="404" redirect="FileNotFound.htm" /> </customErrors> </system.web>
其中各元素的用途如下。
● mode:具有On、Off、RemoteOnly 3种状态。
On:表示始终显示自定义的信息。
Off:表示始终显示详细的ASP.NET错误信息。
RemoteOnly:表示只对不在本地Web服务器上运行的用户显示自定义信息。
● defaultRedirect:用于出现错误时重定向的URL地址(可选)。
● statusCode:指明错误状态码,表明一种特定的出错状态。
● redirect:错误重定向的URL。
3.WinForm应用程序全局异常处理
在WinForm应用程序中,除了采用try…catch…finally截获局部异常以外,如何实现全局的异常处理呢?在WinForm中没有ASP.NET中的Application_Error事件,但我们可以通过声明委托的方式来实现全局的异常截获。
在Main()函数中定义线程异常事件和委托。
代码示例: (示例位置:光盘\code\ch01\23)
using System; using System.Windows.Forms; using System.Threading; namespace WindowsFormsApplication1 { static class Program { //<summary> //应用程序的主入口点 //</summary> [STAThread] static void Main() { //定义线程异常事件 ThreadExceptionHandler handler = new ThreadExceptionHandler(); Application.ThreadException += new ThreadExceptionEventHandler(handler.Application_ThreadException); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } }
定义异常处理类ThreadExceptionHandler,用于处理应用程序的异常。
internal class ThreadExceptionHandler { //实现错误异常事件 public void Application_ThreadException(object sender, ThreadExceptionEventArgs e) { try { //如果用户单击“Abort”按钮则退出应用程序 DialogResult result = ShowThreadExceptionDialog(e.Exception); if (result == DialogResult.Abort) Application.Exit(); } catch { try { MessageBox.Show("严重错误", "严重错误", MessageBoxButtons.OK, MessageBoxIcon.Stop); } finally { Application.Exit(); } } } //输出错误信息 private DialogResult ShowThreadExceptionDialog(Exception ex) { string errorMessage = "错误信息:\n\n" + ex.Message + "\n\n" + ex.GetType() +"\n\nStack Trace:\n" +ex.StackTrace; return MessageBox.Show(errorMessage, "Application Error", MessageBoxButtons.AbortRetryIgnore, MessageBoxIcon.Stop); } }
当我们单击按钮模拟抛出异常时,程序会自动执行Application_ThreadException事件,弹出信息警告窗口。
//手动测试产生一个异常 private void button1_Click(object sender, EventArgs e) { throw new InvalidOperationException("无效的操作异常"); }
运行效果如图1-46所示。

图1-46 WinForm应用程序全局异常
读者也可以在这里自定义错误信息或想要进行的其他处理操作代码。
本章常见技术面试题
✧ 什么是委托?委托和事件是什么关系?
✧ 什么是反射?
✧ C#中是否可以从多个类中继承?如何实现多重继承?
✧ 什么是密封类?
✧ using关键字有几种用途?
✧ #warning和#error分别的用途是什么?
✧ ASP.NET Web 应用程序和ASP.NET 网站的区别是什么?
常见面试技巧之面试前的准备
✧ 简历就像人的脸,第一印象很重要。
√ 个人信息,简洁明了,节省考官时间。
√ 明确求职意向,简历内容有所侧重,有的放矢。
√ 摆事实,讲经验,体现自己能做什么,避免过多修饰词形容自己能怎样。项目经验着重描述,附加项目中用到的思想和技术。
√ 简短总结一下自己的优点和特点,但不要太泛泛,太普通。比如“吃苦耐劳”这样的俗话不如一句“可以接受加班”来得实在。
√ 显示你的稳定度和生涯规划方向,跳槽经历不要过多。
√ 列举相关技术关键词,让简历容易被搜索和注意到。
√ 避免超过两页,冗长的简历让人看起来很烦。
√ 最好能有一个 Blog 在简历中被提到。个人博客无疑是你最好的简历,你可以将日常对技术的研究、对事物的看法,以及自己的一些思想记录下来。一方面可以帮助自己总结过去;另一方面也可以让别人更好地了解你。在同等条件下可以给面试官留下更深的印象。但千万不要为了找工作而临时建立一个博客,那样会感觉更糟。所以,从现在开始养成写博客的习惯很重要。
✧ 面试前提前对公司有所了解。
兵法曰:“知己知彼,百战不殆!”要提高面试成功率,在接到面试通知之后,就得花心思去了解对方公司的老底。
√ 首先要了解一下自己所要应聘的公司和职位,这个公司是做什么的、经营哪些业务、用到哪些技术、在市场上处于什么位置、公司所处的行业发展前景如何、主要竞争对手等情况,以便在面试时与面试官有更多的共同话题,同时给面试官留下更好的印象。
√ 了解公司发展历史、兼并收购情况、公司领导层对公司未来3~5年的发展规划和信心。主要是看这家企业是否有发展前景,未来属于发展期、平稳期,还是衰退期,企业文化是否符合你的性格口味,薪资福利是否能满足自己的期望等。
√ 了解这个职位需要做什么、需要什么样的知识、技能和经验、对应聘者有什么要求,以便自己有针对性地做好准备。更好地应对公司所需要技能的展示准备。
✧ 面试前晚上的准备。
√ 记得把以前填写的简历表和笔试题目重新看一遍,尤其是开放型试题的回答更加要小心复习,避免在面试过程中出现前后回答不一致的情况,给公司留下不诚实的印象。
√ 查找交通路线,以免面试迟到。接到面试通知后,应搞清楚乘车路线,并要留出充裕的时间去乘车。
√ 整理文件包,带上必备用品。面试前,应把文凭、身份证、报名照、钢笔、证明文件等带齐,以供考官查看。此外,应带上一定数量的现金以备不时之需,有晕车症的应带上药品。
√ 准备好面试当天穿的行头,然后洗澡洗头,早点睡觉。以最佳状态迎接第二天的面试。第一印象真的很重要!
本章小结
本章主要介绍了关于.NET 的一些基础知识和 C#的基本语法,旨在让大家对.NET 的很多关键要点有所了解,同时列出了日常开发中所遇到的一些问题和知识要点。这些基础知识是做项目开发的基本功,也是面试时考得最多的内容。人生也正像这样一本书,万事开头难,还有很多的内容需要学习和了解,而更多的内容还需要我们自己在日后的学习中不断积累,不断领悟。