2005年06月22日

flash5就开始支持asfunction了。这个功能不错:asfunction 协议是特定于 Flash 的一个附加协议,可使链接调用动作脚本函数。
  下面是用法示例: 

function myFunction (param) {
  trace("Testing asfunction with parameter: " + param);
}

在支持html的文本字段中输入:"<A HREF="asfunction:myFunction,argument_1">Click here</A>" 

运行之,字符串Testing asfunction with parameter:argument_1会在output窗口中出现。

可惜,asfunction只能够支持一个参数,如果函数有多个参数调用,只有一个办法:写一个函数专门接受从hyperlink中传递过来的asfunction协议,参数字符串中包括函数名,和该函数的参数,解析字符串后再调用指定的函数。

hyperlink中:asfunction:myFunction,argument_1,argument_2,argument_3

函数:

function myFunction(param){
  argumentArray = new Array;
  argumentArray = param.split(",");
  for (i=0; i< argumentArray.length; ++i){
    trace("Function argument " + i + " = " + argumentArray);
  }

结果:
 Function argument 0 =  argument_1Function argument 1 =  argument_2Function argument 2 =  argument_3

再看看下面的这段文档,整个思路应该很明朗了:
您可以构造一个包含输入入口字段的 SWF 文件,该字段允许用户输入要调用的函数名,以及要传递给该函数的零个或多个参数。如果按“调用”按钮,则将使用 apply 方法调用该函数,并指定相应的参数。

在此示例中,用户在名为 functionName 的输入文本字段中指定函数名。在名为 numParameters 的输入文本字段中指定参数数量。在文本字段中最多可指定 10 个参数,这些文本字段名为
parameter1、parameter2,直到 parameter10。

on (release) {
  callTheFunction();
}

function callTheFunction()
{
   var theFunction = eval(functionName.text);
   var n = Number(numParameters);
   var parameters = [];
   for (var i = 0; i < n; i++) {
      parameters.push(eval("parameter" + i));
   }
   theFunction.apply(null, parameters);
}


由于Flash 的WebService Connector和WebService Class都使用异步触发的模式,所以我们常常面临着同一个问题,当一个功能需要一系列的WebService, 而且它们之间还相互依赖时如何保证一系列的service能够按照一定的次序被同步地被调用。

拿论坛应用打个比方,如果以RIA的方式实现,当用户登陆成功后,Flash中已经有了用户信息,其订阅的论坛,以及每个论坛中的标题。假设我们拥有了以下三个web service.

1 - user= login(username, password);
2 - forums = getForums(user.id);
3 - subjects = getSubjects(forumId);

整个登陆动作需要完成以上述的三个过程,而每个服务必须在前一个服务结束后才能被调用。

一个比较愚蠢的方法,就是在每个web service connector中的onResults()里呼叫下一个service connector的trigger()。问题在于,在登陆成功后,程序运行时我们可能只需要用getForums()而不希望在服务结束后再次呼叫getSubjects()。换句话说就是除了在登陆过程中这三种服务有相互依赖的关系,其他时候他们是独立的服务。

一个比较聪明的办法是利用ACT Pattern (Asynchronous Completion Token), 关于此模式的详细情况请参见

http://www.cs.wustl.edu/~schmidt/PDF/ACT.pdf

再ACT pattern中,在呼叫每个异步服务时,你也提供一个ACT token object, 这个Object包含了id,caller state and result action。

当异步服务完成后,被叫方呼叫ACT中的action,而这个action可以根据其id和state 就可以判断出呼叫时的状态执行下一步动作。

更有趣的是,你可以把ACT模式和Chain Of Responsibility模式混用,让ACT自动执行一系列的动作,更新一系列的状态。而你要做的只是呼叫前建立正确的呼叫链就可以了。

以下是一个简单的范例

//simple service wrapper
class Service
{
  var _serviceConnector;
  var _act:Act;

  function Service(connector)
  {
    _serviceConnector = connector;
    _serviceConnector.addEventListener("resuts",this);
  }

  function invoke(params, act)
  {
    _act = act;
    _serviceConnector.params = params;
    _serviceConnector.trigger();
  }

  function results(evt)
  {
    if (_act != undefined)
      _act.execute(evt.target.results);
  }
}

// ACT class
class Act
{
  var id;
  var state;
  var execute:Function;
}

// invoke the service
var loginService;
var getForumsService;
var getSubjectService;

var act = new Act();
act.id="login";
act.state = this;
act.execute = function(results)
{
  if (id== "login")
  {
    id = "form";
    state.user = results;
    state.getForumsService.invoke(user.id, this);
  }

  if (id == "forum")
  {
    id = "subject";
    state.forum = results;
    state.getSubjectService.invoke(forum.id,this);
  }

  if (id == "subject")
  {
    // all accomplished
    state.subjects = results;
    state.finishLogin();
  } 
}


loginService.invoke({username:un,passwordwd}, act);

以上AS代码纯属示范,没有经过任何测试。具体应用时你因该写和你应用相关的ACT.

      前 言

      看了几篇关于“回归C/S”的文章,作为一名多年开发B/S的程序员,不免热血沸腾,深受鼓舞!曾经,我是B/S结构的忠实拥护者,同时也为了所谓的“零部署”陷入过技术泥潭。正当为B/S烦愁的时候,RIA走进了我的视线… …

      什么是RIA

      Internet已经日益成为应用程序开发的默认平台。用户对应用程序复杂性要求日增,但现在的Web应用程序对完成复杂应用方面却始终跟不上步伐。用户与今天中等复杂程度的Web应用程序交互时,其体验并不能令人满意。Web模型是基于页面的模型,缺少客户端智能机制。而且,它几乎无法完成复杂的用户交互(如传统的C/S应用程序和桌面应用程序中的用户交互)。这样的技术使得Web应用程序难以使用,支持成本高,并且在很多方面无法发挥效应。

      为了提高用户体验,出现了一种新类型的Internet应用程序。那就是Rich Internet Applications(RIA)。这些应用程序结合了桌面应用程序的反应快、交互性强的优点与Web应用程序的传播范围广及容易传播的特性。RIA简化并改进了Web应用程序的用户交互。这样,用户开发的应用程序可以提供更丰富、更具有交互性和响应性的用户体验。
 

                  基于主机模式→C/S模式→B/S模式→RIA模式
      
      我们的行业经历了几次系统架构方面的重要转变,在此过程中,客户端的表现功能有起有落。上图介绍了每个阶段的计算功能所带来的应用程序体验方面的变化,这一过程从大型机开始,到RIA的出现为止。

      随着各企业组织认识到RIA模型可产生显著的商业利润、提高生产率及降低成本的优势后,这种模型的发展势头越来越猛烈。这些应用程序结合了桌面应用程序的反应快、交互性强的优点与Web应用程序的传播范围广及容易传播的特性。系统架构发展的下一步是RIA,它最大程度地提高了广泛性和丰富性。

      论传统B/S之不足

      过程复杂性
      过程复杂性是由于需要表达一个多步骤或多选项任务或互动作用所引起的。在HTML里,一个多步骤的任务可以在单页内表达出来。但是由于HTML的互动性有限,便可能产生一份很长的页面,使用户感到混乱、笨拙而难以使用。为了避免这种难以忍受的用户体验,便需将任务在表面上看来“自然”的部分处区分成多个步骤,甚至需多个网页共同完成。这种以网页为主的用户界面通常需要反复翻转网页,以解决在顺序步骤中有牵连性的改变。其结果是缓慢、不自然、混乱而且令人感到懊恼的用户体验。
    
      配置复杂性
      许多Web应用程序允许用户配置自己所要的定制产品——可以是皮包或是计算机,甚至是汽车等产品。但是配置产品是一项很困难的过程,因为在向用户展示所有有效的产品选项组合时,应用程序必须能够表达出有关的复杂性,尤其是当用户可以从数十、数百或数千选项中定制出一个产品时。表达这些复杂性包括指出所需条件、有效和无效组合、一些导致问题的元素以及它们的适当解决方法;为每一项个人选择提供费用信息以及费用总计(一旦有所更改);还有最重要的是容许用户观看最后结果。这些是传统Web应用程序相当难以表现的。

      规模复杂性
      今天,网站内的搜索工具大多是文本性质,间中夹着一些锦上添花的图像。当用户输入他或她的数码照相机准则,有可能是价格、以像素等,网站便接着回复数页符合准则的产品,而大部分都是说明文本。反之,另一种方法则是使用视觉化来简化搜索空间(也就是提供立即和动态的视觉反馈)。在一个视觉化选择照相机的网站,其搜索过程可能如下:网站从一个包含所有照相机种类图像的单屏幕开始。当用户通过复选框、游标或数据输入域来选择筛选准则时,所有不符合准则的照相机图像将被删除,只余下符合准则的照相机可在屏幕上看到。因此,在把选择聚焦至符合准则的数部照相机的过程中,用户可经历一个截然不同,而且和现实生活中的购物经验更相似的体验。

      反馈复杂性
      高度互动性的应用程序如游戏,能使反馈变得复杂,也即是指用户行动和快速移动或情节不断改变的屏幕元素之间的反馈环路。传统的HTML页面一向来都可以说是无法表达这类复杂性。它所需要的是拥有高度互动性和局部智能型的客户端应用程序,以便可以在无需刷新全页或干扰与服务器之间的通信的情况下,响应用户的输入和改变它们的状态或界面。放弃如今依赖服务器的客户机将使用户体验更吸引,同时也解决了反馈复杂性的问题。Web应用程序必须拥有表达复杂性的能力,以容许用户视看复杂的数据、配置多选项的产品、搜索大型数据集以及容许用户与数据之间的互动交换。

      真正的RIA

    为了解决如今的问题,理想中的Web应用程序应该能够:
1、 利用无处不在的客户机
2、 在多种硬件平台上毫无更改的操作互联网
3、 无论低或高带宽的连接都可毫无妨碍的执行
4、 将处理能力复原给客户(而不仅是提供能力而已)
5、 提供吸引人的高度互动的用户界面
6、 表达过程、数据配置、规模和反馈复杂性
7、 无缝的利用声音、视像、图像和文本
8、 容许用户在线和离线工作以支持移动工作流程
9、 容许客户自行决定要在何时存取何种内容和数据(异步内容检索)
10、 存取多种中间层服务(.NET或Java)和后端数据存储
11、 采用新崛起的标准如XML和SOAP,为演进中的Web Service为主的网络提供动态高效的前端应用
12、 与遗旧的应用程序和系统集成
13、 容许在现有Web应用程序和环境内逐步添加新功能以充分利用现有网络应用投资
 

                                 结 构
      RIA本身有能力提供这类Web应用解决方案。如上图,RIA将桌面型计算机软件应用的最佳用户界面功能性与Web应用程序的普遍采纳和低成本部署以及互动多媒体通信的长处集于一体,终于成就了一种可以提供更直观、响应性和有效的用户体验应用程序。它所具备的桌面型计算机长处包括了在确认和格式编排方面提供互动用户界面;在无刷新页面之下提供快捷的界面响应时间;提供通用的用户界面特性如拖放式(drag and drop)以及在线和离线操作能力。Web网的长处如立即部署、跨越平台可用性、采用逐步下载来检索内容和数据、拥有杂志式布局的网页以及充分利用被广泛采纳的互联网标准。通信的长处则包括双向互动声音和图像。

      客户机在RIA内的作用不仅是展示页面,它可以在幕后与用户请求异步地进行计算、递送和检索数据、重新画出屏幕的一部分和密切综合使用声音和图像,这一切都可以在不依靠客户机连接的服务器或后端的情况下进行。

      RIA提供一个强劲的技术平台,使客户机的能力复原到差不多与桌面型计算机软件应用或传统的C/S系统中的客户机能力相似。它适合传统的N层开发过程,同时也能够和遗旧的环境集成以延展现有的应用程序而无需进行修改。它也可以作为基础网络服务的互动表现层,允许用户在线和离线工作。RIA有能力解决各种复杂性,使需要复杂性的应用得以开发并且减少开发成本,同时在很多时候这类应用之所以能够成形主要是拜RIA所赐。

      RIA方案—基于Flash的Flex

      Flex简介
      Macromedia公司被公认为新兴的RIA市场的领导者。今天98%的浏览器上都使用Macromedia Flash客户端软件,因此几乎每个人都可以使用基于Flash的RIA。Macromedia Flex是Macromedia的新服务器产品,它使企业应用程序开发人员能够全面访问RIA的功能。Flex具有基于标准的架构,与当前企业开发人员的工具、方法和设计模式互补。

      Flex应用程序与传统的HTML应用程序的主要区别在于Flex应用程序处理最适合在客户端运行,如字段校验、数据格式、分类、过滤、工具提示、合成视频、行为及效果等。Flex 可使开发人员更好地交付应用程序,这种应用程序使用户可以迅速反应、在不同状态与显示间流畅过渡,并提供毫无中断的连续的工作流。
 

                              Flex 应用程序框架
如上图所示,Flex应用程序框架由MXML、ActionScript 2.0及Flex类库构成。开发人员利用 MXML及ActionScript 2.0编写Flex应用程序。利用MXML定义应用程序用户界面元素,利用ActionScript 2.0定义客户逻辑与程序控制。Flex类库中包括Flex组件、管理器及行为等。利用基于Flex 组件的开发模型,开发人员可在程序中加入预建的组件、创建新组件或是将预建的组件加入复合组件中。

      这里重点介绍一下MXML。与HTML一样,都是标记语言,它描述了反映内容与功能的用户界面。与HTML不同的是,MXML 可对表示层逻辑与用户界面和服务器端数据绑定提供声明抽象。MXML可将表示与业务逻辑的问题彻底分开,以实现最大程度地提高开发人员的生产率及应用程序的重复使用率。

      Flex的不足
      目前Macromedia最新推出了Flex 1.0 Updater,但它代号为“Brady”的IDE还没有正式推出,目前还在进行Beta 3测试。抛开IDE不说,笔者认为Flex目前还很不成熟,还不利于在实际项目中使用。

例如,Flex自带的ZipCodeValidator,里面只提供了美国和加拿大的邮编规则,没有其他选择,也无法个性化它。看来只有自己来定义Validator了,但这样一来,和在JS中写正则表达式有什么区别(代码量和JS差不多)?用户需要的是国际化的ZipCodeValidator,这样才能提高工作效率。

      一句话概括
      现在的Flex才是1.0版本,很多地方都不完善,只好自定义才能完成特定的要求。期待着Brady以及Flex后续版本的推出!

RIA方案—基于JS的Bindows

      Bindows简介
      “Bindows把javascript发挥到了第九层!”——网友这样评价Bindows。


 
                         运行中的Bindows
    
      的确如此,Erik等编写这个框架已经将javascript的OOP和基于IE6的DHTML发挥到极点!Bindows 0.93发布的时候已经将IE内置的功能开发得淋漓尽致了,包括Filter、XMLHTTP、Web Service、VML。javascript用于客户端界面的显示和处理,XMLHTTP用于客户端与服务器的信息传输。javascript在客户端的表现力不容置疑,看看www.bindows.net所表示出来的能力,利用javascript几乎可以实现Windows应用程序所能干的大部分事情,XMLHTTP一直以来常被用于实现“无刷新”的Web页面,它和javascript配合,可以完成数据从服务器和客户端的传输。
    
      Bindows的不足
      Erik喜欢那种一次全部载入的方式来实现脚本库,使用过Bindows会发现,在窗口的加载期,需要一个漫长的等待过程,甚至浏览器的进程会产生无响应的情况。按照V0.93,脚本文件的大小是600多K,在一个普通的Web应用中,我们更多时候不会用到Bindows的全部功能,这点Bindows根本没有遵循“用多少去多少”的准则。另外,过多的JS会使CPU占用率陡然增加,产生潜在问题。

      内部大量利用了IE6的技术,没有考虑到非微软平台的浏览器,限制了Bindows的流行。在图表方面,大量采用了VML技术,在IE5,IE5.5这两个版本,VML引擎不是那么的成熟,很多地方的显示不够流畅,会受到带宽和硬件的限制,过分绚丽的图形最终会给用户带来崩溃。“图形方面我是采用VML的,当初太偏执,如果使用SVG来实现可能好许多的,也就是那段日子,我花了非常多的时间去折腾web方面开发。”——有网友这样说。

      一句话概括
      在技术的角度上,从Bindows是可以学到不少东西的,但好像它的学术价值大于它的商业价值。

RIA是Rich Internet Applications的缩写,翻译成中文为富因特网应用程序(Macromedia中文网站翻译为Rich Internet应用程序)

传统网络程序的开发是基于页面的、服务器端数据传递的模式,把网络程序的表示层建立于HTML页面之上,而HTML是适合于文本的,传统的基于页面的系统已经渐渐不能满足网络浏览者的更高的、全方位的体验要求了,这就是被Macromedia公司称之为的“体验问题”("Experience Matters"),而富因特网应用程序(Rich Internet Applications,缩写为RIA)的出现也就是为了解决这个问题。

富因特网应用程序的发展阶段图如下:


富因特网应用程序是下一代的将桌面应用程序的交互的用户体验与传统的Web应用的部署灵活性和成本分析结合起来的网络应用程序。富因特网应用程序中的富客户技术通过提供可承载已编译客户端应用程序(以文件形式,用HTTP传递)的运行环境,客户端应用程序使用异步客户/服务器架构连接现有的后端应用服务器,这是一种安全、可升级、具有良好适应性的新的面向服务模型,这种模型由采用的Web服务所驱动。结合了声音、视频和实时对话的综合通信技术使富因特网应用程序(RIA)具有前所未有的网上用户体验。

“富”的概念包含两方面,分别是数据模型的丰富和用户界面的丰富。数据中的“富”意思是用户界面可以显示和操作更为复杂的嵌入在客户端的数据模型,它可以操作客户端的计算和非同步的发送接收数据。这种模式相对于传统的HTML页面的优点是程序运行于客户端并且程序更多的是和用户进行交互同时更少的和服务器进行交互。平衡客户端和服务器端的复杂的数据模型可以让你有更大的空间去创建更高效和更具有交互性的网络应用程序。“富”同样也描述了全面提升的用户界面,HTML只给用户提供了非常有限的界面控制元素,而富因特网应用程序(RIA)的用户界面提供了灵活多样的界面控制元素,这些控制元素可以很好的与数据模型相结合。传统的因特网模型使用线性的设计,提供给用户一些选择然后用户发送选择结果给服务器,这种单一的模式不符合应用程序的灵活交互的要求和用户的意愿。频繁的服务器请求和页面刷新有很多的缺点包括页面打开缓慢和降低网络带宽。如果采用富客户界面,可以从以前的服务器响应影响整个界面,转移到只有收到请求的应用程序部分才会做出相应的变化。这本质上意味着界面被分解成许多独立的模块,这些模块都会对收到的信息做出相应的反应,有些会和服务器端进行交互,有些是这些模块之间的通信。

2005年06月13日



<script language="JavaScript">
<!–
var g_selProvince;
var g_selCity;
var Provinces=new Array(
new Array("110000","北京市"),
new Array("120000","天津市"),
new Array("130000","河北省"),
new Array("140000","山西省"),
new Array("150000","内蒙古自治区"),
new Array("210000","辽宁省"),
new Array("220000","吉林省"),
new Array("230000","黑龙江省"),
new Array("310000","上海市"),
new Array("320000","江苏省"),
new Array("330000","浙江省"),
new Array("340000","安徽省"),
new Array("350000","福建省"),
new Array("360000","江西省"),
new Array("370000","山东省"),
new Array("410000","河南省"),
new Array("420000","湖北省"),
new Array("430000","湖南省"),
new Array("440000","广东省"),
new Array("450000","广西壮族自治区"),
new Array("460000","海南省"),
new Array("500000","重庆市"),
new Array("510000","四川省"),
new Array("520000","贵州省"),
new Array("530000","云南省"),
new Array("540000","西藏自治区"),
new Array("610000","陕西省"),
new Array("620000","甘肃省"),
new Array("630000","青海省"),
new Array("640000","宁夏回族自治区"),
new Array("650000","新疆维吾尔自治区"),
new Array("710000","台湾省"),
new Array("810000","香港特别行政区"),
new Array("820000","澳门特别行政区")
);

var Citys=new Array(
new Array("110100","北京"),
new Array("120100","天津"),
new Array("130101","石家庄"),
new Array("130201","唐山"),
new Array("130301","秦皇岛"),
new Array("130701","张家口"),
new Array("130801","承德"),
new Array("131001","廊坊"),
new Array("130401","邯郸"),
new Array("130501","邢台"),
new Array("130601","保定"),
new Array("130901","沧州"),
new Array("133001","衡水"),
new Array("140101","太原"),
new Array("140201","大同"),
new Array("140301","阳泉"),
new Array("140501","晋城"),
new Array("140601","朔州"),
new Array("142201","忻州"),
new Array("142331","离石"),
new Array("142401","榆次"),
new Array("142601","临汾"),
new Array("142701","运城"),
new Array("140401","长治"),
new Array("150101","呼和浩特"),
new Array("150201","包头"),
new Array("150301","乌海"),
new Array("152601","集宁"),
new Array("152701","东胜"),
new Array("152801","临河"),
new Array("152921","阿拉善左旗"),
new Array("150401","赤峰"),
new Array("152301","通辽"),
new Array("152502","锡林浩特"),
new Array("152101","海拉尔"),
new Array("152201","乌兰浩特"),
new Array("210101","沈阳"),
new Array("210201","大连"),
new Array("210301","鞍山"),
new Array("210401","抚顺"),
new Array("210501","本溪"),
new Array("210701","锦州"),
new Array("210801","营口"),
new Array("210901","阜新"),
new Array("211101","盘锦"),
new Array("211201","铁岭"),
new Array("211301","朝阳"),
new Array("211401","锦西"),
new Array("210601","丹东"),
new Array("220101","长春"),
new Array("220201","吉林"),
new Array("220301","四平"),
new Array("220401","辽源"),
new Array("220601","浑江"),
new Array("222301","白城"),
new Array("222401","延吉"),
new Array("220501","通化"),
new Array("230101","哈尔滨"),
new Array("230301","鸡西"),
new Array("230401","鹤岗"),
new Array("230501","双鸭山"),
new Array("230701","伊春"),
new Array("230801","佳木斯"),
new Array("230901","七台河"),
new Array("231001","牡丹江"),
new Array("232301","绥化"),
new Array("230201","齐齐哈尔"),
new Array("230601","大庆"),
new Array("232601","黑河"),
new Array("232700","加格达奇"),
new Array("310100","上海"),
new Array("320101","南京"),
new Array("320201","无锡"),
new Array("320301","徐州"),
new Array("320401","常州"),
new Array("320501","苏州"),
new Array("320600","南通"),
new Array("320701","连云港"),
new Array("320801","淮阴"),
new Array("320901","盐城"),
new Array("321001","扬州"),
new Array("321101","镇江"),
new Array("330101","杭州"),
new Array("330201","宁波"),
new Array("330301","温州"),
new Array("330401","嘉兴"),
new Array("330501","湖州"),
new Array("330601","绍兴"),
new Array("330701","金华"),
new Array("330801","衢州"),
new Array("330901","舟山"),
new Array("332501","丽水"),
new Array("332602","临海"),
new Array("340101","合肥"),
new Array("340201","芜湖"),
new Array("340301","蚌埠"),
new Array("340401","淮南"),
new Array("340501","马鞍山"),
new Array("340601","淮北"),
new Array("340701","铜陵"),
new Array("340801","安庆"),
new Array("341001","黄山"),
new Array("342101","阜阳"),
new Array("342201","宿州"),
new Array("342301","滁州"),
new Array("342401","六安"),
new Array("342501","宣州"),
new Array("342601","巢湖"),
new Array("342901","贵池"),
new Array("350101","福州"),
new Array("350201","厦门"),
new Array("350301","莆田"),
new Array("350401","三明"),
new Array("350501","泉州"),
new Array("350601","漳州"),
new Array("352101","南平"),
new Array("352201","宁德"),
new Array("352601","龙岩"),
new Array("360101","南昌"),
new Array("360201","景德镇"),
new Array("362101","赣州"),
new Array("360301","萍乡"),
new Array("360401","九江"),
new Array("360501","新余"),
new Array("360601","鹰潭"),
new Array("362201","宜春"),
new Array("362301","上饶"),
new Array("362401","吉安"),
new Array("362502","临川"),
new Array("370101","济南"),
new Array("370201","青岛"),
new Array("370301","淄博"),
new Array("370401","枣庄"),
new Array("370501","东营"),
new Array("370601","烟台"),
new Array("370701","潍坊"),
new Array("370801","济宁"),
new Array("370901","泰安"),
new Array("371001","威海"),
new Array("371100","日照"),
new Array("372301","滨州"),
new Array("372401","德州"),
new Array("372501","聊城"),
new Array("372801","临沂"),
new Array("372901","菏泽"),
new Array("410101","郑州"),
new Array("410201","开封"),
new Array("410301","洛阳"),
new Array("410401","平顶山"),
new Array("410501","安阳"),
new Array("410601","鹤壁"),
new Array("410701","新乡"),
new Array("410801","焦作"),
new Array("410901","濮阳"),
new Array("411001","许昌"),
new Array("411101","漯河"),
new Array("411201","三门峡"),
new Array("412301","商丘"),
new Array("412701","周口"),
new Array("412801","驻马店"),
new Array("412901","南阳"),
new Array("413001","信阳"),
new Array("420101","武汉"),
new Array("420201","黄石"),
new Array("420301","十堰"),
new Array("420400","沙市"),
new Array("420501","宜昌"),
new Array("420601","襄樊"),
new Array("420701","鄂州"),
new Array("420801","荆门"),
new Array("422103","黄州"),
new Array("422201","孝感"),
new Array("422301","咸宁"),
new Array("422421","江陵"),
new Array("422801","恩施"),
new Array("430101","长沙"),
new Array("430401","衡阳"),
new Array("430501","邵阳"),
new Array("432801","郴州"),
new Array("432901","永州"),
new Array("430801","大庸"),
new Array("433001","怀化"),
new Array("433101","吉首"),
new Array("430201","株洲"),
new Array("430301","湘潭"),
new Array("430601","岳阳"),
new Array("430701","常德"),
new Array("432301","益阳"),
new Array("432501","娄底"),
new Array("440101","广州"),
new Array("440301","深圳"),
new Array("441501","汕尾"),
new Array("441301","惠州"),
new Array("441601","河源"),
new Array("440601","佛山"),
new Array("441801","清远"),
new Array("441901","东莞"),
new Array("440401","珠海"),
new Array("440701","江门"),
new Array("441201","肇庆"),
new Array("442001","中山"),
new Array("440801","湛江"),
new Array("440901","茂名"),
new Array("440201","韶关"),
new Array("440501","汕头"),
new Array("441401","梅州"),
new Array("441701","阳江"),
new Array("450101","南宁"),
new Array("450401","梧州"),
new Array("452501","玉林"),
new Array("450301","桂林"),
new Array("452601","百色"),
new Array("452701","河池"),
new Array("452802","钦州"),
new Array("450201","柳州"),
new Array("450501","北海"),
new Array("460100","海口"),
new Array("460200","三亚"),
new Array("510101","成都"),
new Array("513321","康定"),
new Array("513101","雅安"),
new Array("513229","马尔康"),
new Array("510301","自贡"),
new Array("500100","重庆"),
new Array("512901","南充"),
new Array("510501","泸州"),
new Array("510601","德阳"),
new Array("510701","绵阳"),
new Array("510901","遂宁"),
new Array("511001","内江"),
new Array("511101","乐山"),
new Array("512501","宜宾"),
new Array("510801","广元"),
new Array("513021","达县"),
new Array("513401","西昌"),
new Array("510401","攀枝花"),
new Array("500239","黔江土家族苗族自治县"),
new Array("520101","贵阳"),
new Array("520200","六盘水"),
new Array("522201","铜仁"),
new Array("522501","安顺"),
new Array("522601","凯里"),
new Array("522701","都匀"),
new Array("522301","兴义"),
new Array("522421","毕节"),
new Array("522101","遵义"),
new Array("530101","昆明"),
new Array("530201","东川"),
new Array("532201","曲靖"),
new Array("532301","楚雄"),
new Array("532401","玉溪"),
new Array("532501","个旧"),
new Array("532621","文山"),
new Array("532721","思茅"),
new Array("532101","昭通"),
new Array("532821","景洪"),
new Array("532901","大理"),
new Array("533001","保山"),
new Array("533121","潞西"),
new Array("533221","丽江纳西族自治县"),
new Array("533321","泸水"),
new Array("533421","中甸"),
new Array("533521","临沧"),
new Array("540101","拉萨"),
new Array("542121","昌都"),
new Array("542221","乃东"),
new Array("542301","日喀则"),
new Array("542421","那曲"),
new Array("542523","噶尔"),
new Array("542621","林芝"),
new Array("610101","西安"),
new Array("610201","铜川"),
new Array("610301","宝鸡"),
new Array("610401","咸阳"),
new Array("612101","渭南"),
new Array("612301","汉中"),
new Array("612401","安康"),
new Array("612501","商州"),
new Array("612601","延安"),
new Array("612701","榆林"),
new Array("620101","兰州"),
new Array("620401","白银"),
new Array("620301","金昌"),
new Array("620501","天水"),
new Array("622201","张掖"),
new Array("622301","武威"),
new Array("622421","定西"),
new Array("622624","成县"),
new Array("622701","平凉"),
new Array("622801","西峰"),
new Array("622901","临夏"),
new Array("623027","夏河"),
new Array("620201","嘉峪关"),
new Array("622102","酒泉"),
new Array("630100","西宁"),
new Array("632121","平安"),
new Array("632221","门源回族自治县"),
new Array("632321","同仁"),
new Array("632521","共和"),
new Array("632621","玛沁"),
new Array("632721","玉树"),
new Array("632802","德令哈"),
new Array("640101","银川"),
new Array("640201","石嘴山"),
new Array("642101","吴忠"),
new Array("642221","固原"),
new Array("650101","乌鲁木齐"),
new Array("650201","克拉玛依"),
new Array("652101","吐鲁番"),
new Array("652201","哈密"),
new Array("652301","昌吉"),
new Array("652701","博乐"),
new Array("652801","库尔勒"),
new Array("652901","阿克苏"),
new Array("653001","阿图什"),
new Array("653101","喀什"),
new Array("654101","伊宁"),
new Array("710001","台北"),
new Array("710002","基隆"),
new Array("710020","台南"),
new Array("710019","高雄"),
new Array("710008","台中"),
new Array("211001","辽阳"),
new Array("653201","和田"),
new Array("542200","泽当镇"),
new Array("542600","八一镇"),
new Array("820000","澳门"),
new Array("810000","香港")
);

function FillProvinces(selProvince)
{
    selProvince.options[0]=new Option("请选择","000000");
    for(i=0;i<Provinces.length;i++)
    {
        selProvince.options[i+1]=new Option(Provinces[i][1],Provinces[i][0]);
    }
    selProvince.options[0].selected=true;
    selProvince.length=i+1;
}

function FillCitys(selCity,ProvinceCode)
{
    //if the province is a direct-managed city, like Beijing, shanghai, tianjin, chongqin,hongkong, macro
        //need not "请选择选项"
        if(ProvinceCode=="110000"||ProvinceCode=="120000"||ProvinceCode=="310000"
                 ||ProvinceCode=="810000"||ProvinceCode=="820000"||ProvinceCode=="500000")
             count=0;
        else
                {selCity.options[0]=new Option("请选择",ProvinceCode);
                count=1;}
    for(i=0;i<Citys.length;i++)
    {
        if(Citys[i][0].toString().substring(0,2)==ProvinceCode.substring(0,2))
        {
            selCity.options[count]=new Option(Citys[i][1],Citys[i][0]);
            count=count+1;
        }
    }
    selCity.options[0].selected=true;
    selCity.length=count;
}

function Province_onchange()
{
    FillCitys(g_selCity,g_selProvince.value);
}

function InitCitySelect(selProvince,selCity)
{
    //alert("begin");
    g_selProvince=selProvince;
    g_selCity=selCity;
    selProvince.onchange=Function("Province_onchange();");
    FillProvinces(selProvince);
    Province_onchange();
}
function InitCitySelect2(selProvince,selCity,CityCode)
{
    InitCitySelect(selProvince,selCity)
    for(i=0;i<selProvince.length;i++)
    {
        if(selProvince.options[i].value.substring(0,2)==CityCode.substring(0,2))
        {
            selProvince.options[i].selected=true;
        }
    }
    Province_onchange();
    for(i=0;i<selCity.length;i++)
    {
        if(selCity.options[i].value==CityCode)
        {
            selCity.options[i].selected=true;
        }
    }
}
//–>
</script>
<form name="profile" method="post" action="where.asp">
 <SELECT id=province size=1 name=province>
   <OPTION selected></OPTION>
 </SELECT>
 <SELECT id=city size=1 name=city>
  <OPTION selected></OPTION>
 </SELECT>
<SCRIPT language=javascript>
InitCitySelect(document.profile.province,document.profile.city);
</SCRIPT>
<input type="submit">
</form>

2005年06月07日

第九章 配置和调度

在上一章,你学到如何创建一个通用语言运行时(CLR)组件,且如何在一个简单的测试应用程序中使用它。虽然CLR组件就要准备装载了,但你还是应该思考以下技术之一:
。条件编译
。文档注释
。代码版本化

9.1 条件编译 

没有代码的条件编译功能,我就不能继续工作。条件编译允许执行或包括基于某些条件的代码;例如,生成应用程序的一个查错(DEBUG)版本、演示(DEMO)版本或零售(RELEASE)版本。可能被包括或被执行的代码的例子为许可证代码、 屏幕保护或你出示的任何程序。

在C#中,有两种进行条件编译的方法:
。预处理用法
。条件属性

9.1.1 预处理用法

在C++中,在编译器开始编译代码之前,预处理步骤是分开的。在C#中,预处理被编译器自己模拟—— 没有分离的预处理。它只不过是条件编译。

尽管C#编译器不支持宏,但它具有必需的功能,依据符号定义的条件,排除和包括代码。以下小节介绍了在C#中受支持的各种标志,它们与在C++中看到的相似。
。定义符号
。依据符号排除代码
。引起错误和警告

9.1.1.1 定义符号

你不能使用随C#编译器一起的预处理创建“define 标志:符号:定义 ”宏,但是,你仍可以定义符号。根据某些符号是否被定义,可以排除或包括代码。

第一种定义符号的办法是在C#源文件中使用 #define标志:
#define DEBUG

这样定义了符号DEBUG,且范围在它所定义的文件内。请注意,必须要先定义符号才能使用其它语句。例如,以下代码段是不正确的:

using System;
#define DEBUG

编译器将标记上述代码为错误。你也可以使用编译器定义符号(用于所有的文件):
csc /define:DEBUG mysymbols.cs
如果你想用编译器定义多种符号,只需用分号隔开它们:
csc /define:RELEASE;DEMOVERSION mysymbols.cs

在C#源文件中,对这两种符号的定义分为两行 #define 标志。

有时,你可能想要取消源文件中(例如,较大项目的源文件)的某种符号。可以用 #undef 标志取消定义:
#undef DEBUG
#define的“定义标志:符号: 定义”规则同样适用于#undef: 它的范围在自己定义的文件之内,要放在任何语句如using语句之前。这就是全部有关用C#预处理定义符号和取消定义符号所要了解的知识。以下小节说明如何使用符号有条件地编译代码。

9.1.1.2 依据符号包括和排除代码

最重要的“if标志:符号:包括代码”方式的目的为,依据符号是否被定义,有条件地包括和排除代码。清单9.1 包含了已出现过的源码,但这次它依据符号被有条件地编译。

清单 9.1 利用 #if 标志有条件地包括代码



1: using System;
2: 
3: public class SquareSample
4: {
5: public void CalcSquare(int nSideLength, out int nSquared)
6: {
7: nSquared = nSideLength * nSideLength;
8: }
9: 
10: public int CalcSquare(int nSideLength)
11: {
12: return nSideLength*nSideLength;
13: }
14: }
15: 
16: class SquareApp
17: {
18: public static void Main()
19: {
20: SquareSample sq = new SquareSample();
21: 
22: int nSquared = 0;
23: 
24: #if CALC_W_OUT_PARAM
25: sq.CalcSquare(20, out nSquared);
26: #else 
27: nSquared = sq.CalcSquare(15);
28: #endif
29: Console.WriteLine(nSquared.ToString());
30: }
31: }


注意,在这个源文件中没有定义符号。当编译应用程序时,定义(或取消定义)符号:


csc /define:CALC_W_OUT_PARAM square.cs


根据“ if标志:符号:包括代码”的符号定义,不同的 CalcSquare 被调用了。用来对符号求值的模拟预处理标志为#if、 #else和 #endif。它们产生的效果就象C#相应的if 语句那样。你也可以使用逻辑“与”(&&)、逻辑“或”(&brvbar;&brvbar;)以及“否”(!)。它们的例子显示在清单9.2 中。

清单 9.2 使用#elif 在#if标志中创建多个分支



1: // #define DEBUG
2: #define RELEASE
3: #define DEMOVERSION
4: 
5: #if DEBUG
6: #undef DEMOVERSION
7: #endif
8: 
9: using System;
10: 
11: class Demo
12: {
13: public static void Main()
14: {
15: #if DEBUG
16: Console.WriteLine("Debug version");
17: #elif RELEASE && !DEMOVERSION
18: Console.WriteLine("Full release version");
19: #else
20: Console.WriteLine("Demo version");
21: #endif
22: }
23: }


在这个“if标志:符号:包含代码”例子中,所有的符号都在C#源文件中被定义。注意第6行#undef语句增加的那部分。由于不编译DEBUG代码的DEMO版本(任意选择),我确信它不会被某些人无意中定义了,而且总当DEBUG被定义时,就取消DEMO版本的定义。

接着在第15~21行,预处理符号被用来包括各种代码。注意#elif标志的用法,它允许你把多个分支加到#if 标志。该代码运用逻辑操作符“&&”和非操作符“!”。也可能用到逻辑操作符“&brvbar;&brvbar;”,以及等于和不等于操作符。

9.1.1.3 引起错误并警告

另一种可能的“警告 标志错误 标志”预处理标志的使用,是依据某些符号(或根本不依据,如果你这样决定)引起错误或警告。各自的标志分别为 #warning和#error,而清单9.3 演示了如何在你的代码中使用它们。
清单 9.3 使用预处理标志创建编译警告和错误



1: #define DEBUG
2: #define RELEASE
3: #define DEMOVERSION
4: 
5: #if DEMOVERSION && !DEBUG
6: #warning You are building a demo version
7: #endif
8: 
9: #if DEBUG && DEMOVERSION
10: #error You cannot build a debug demo version
11: #endif
12: 
13: using System;
14: 
15: class Demo
16: {
17: public static void Main()
18: {
19: Console.WriteLine("Demo application");
20: }
21: }


在这个例子中,当你生成一个不是DEBUG版本的DEMO版本时,就发出了一个编译警告(第5行~第7行)。当你企图生成一个DEBUG DEMO版本时,就引起了一个错误,它阻止了可执行文件的生成。对比起前面只是取消定义令人讨厌的符号的例子,这些代码告诉你,“警告 标志错误 标志”企图要做的工作被认为是错误的。这肯定是更好的处理办法。

9.1.1.4 条件属性

C++的预处理也许最经常被用来定义宏,宏可以解决一种程序生成时的函数调用,而却不能解决另一种程序生成时的任何问题。这些例子包括 ASSERT和TRACE 宏,当定义了DEBUG符号时,它们对函数调用求值,当生成一个RELEASE版本时,求值没有任何结果。

当了解到宏不被支持时,你也许会猜测,条件功能已经消亡了。幸亏我可以报道,不存在这种情况。你可以利用条件属性,依据某些已定义符号来包括方法。:

[conditional("DEBUG")]
public void SomeMethod() { }

仅当符号DEBUG被定义时,这个方法被加到可执行文件。并且调用它,就象SomeMethod();

当该方法不被包括时,它也被编译器声明。功能基本上和使用C++条件宏相同。在例子开始之前,我想指出,条件方法必须具有void的返回类型,不允许其它返回类型。然而,你可以传递你想使用的任何参数。

在清单9.4 中的例子演示了如何使用条件属性重新生成具有C++的TRACE宏一样的功能。为简单起见,结果直接输出到屏幕。你也可以根据需要把它定向到任何地方,包括一个文件。

清单 9.4 使用条件属性实现方法



1: #define DEBUG
2: 
3: using System;
4: 
5: class Info
6: {
7: [conditional("DEBUG")]
8: public static void Trace(string strMessage)
9: {
10: Console.WriteLine(strMessage);
11: }
12: 
13: [conditional("DEBUG")]
14: public static void TraceX(string strFormat,params object[] list)
15: {
16: Console.WriteLine(strFormat, list);
17: }
18: }
19: 
20: class TestConditional
21: {
22: public static void Main()
23: {
24: Info.Trace("Cool!");
25: Info.TraceX("{0} {1} {2}","C", "U", 2001);
26: }
27: }



在Info类中,有两个静态方法,它们根据DEBUG符号被有条件地编译:Trace,接收一个参数,而TraceX则接收n个参数。Trace的实现直接了当。然而,TraceX实现了一个你从没有见过的关键字:params。

params 关键字允许你指定一个方法参数,它实际上接收了任意数目的参数。其类似C/C++的省略参数。注意,它必须是方法调用的最后一个参数,而且在参数列表中,你只能使用它一次。毕竟,它们的局限性极其明显。

使用params 关键字的意图就是要拥有一个Trace方法,该方法接收一个格式字符串以及无数个置换对象。幸好,还有一个支持格式字符串和对象数组的 WriteLine方法(第16行)。

这个小程序产生的哪一个输出完全取决于DEBUG是否被定义。当DEBUG符号被定义时,方法都被编译和执行。如果DEBUG不被定义,对Trace和TraceX的调用也随之消失。条件方法是给应用程序和组件增加条件功能的一个真正强大的手段。用一些技巧,你就可以根据由逻辑“或”(&brvbar;&brvbar;)以及逻辑“与”(&&)连接起来的多个符号,生成条件方法。然而,对于这些方案,我想给你推荐C#文档。

9.2 在XML中的文档注释

很多程序员根本不喜欢的一项任务就是写作,包括写注释和写文档。然而,有了C#,你就找到改变老习惯的好理由:你可以用代码的注释自动生成文档。

由编译器生成的输出结果是完美的XML。它可以作为组件文档的输入被使用,以及作为显示帮助并揭示组件内部细节的工具。例如, Visual Studio 7 就是这样一种工具。

这一节专门为你说明如何最好地运用C#的文档功能。该例子涉及的范围很广,所以你不能有这样的借口,说它过于复杂,以至很难领会如何加入文档注释。文档是软件极其重要的一部分,特别是要被其他开发者使用的组件的文档。在以下小节中,文档注解用来说明RequestWebPage 类。我已分别在以下几小节中做出解释:
。描述一个成员
。添加备注和列表
。提供例子
。描述参数
。描述属性
。编译文档


9.2.1 描述一个成员

第一步,为一个成员添加一个简单的描述。你可以用 <summary> 标签这样做:

/// <summary>This is …. </summary>

每一个文档注释起始于由三个反斜杠组成的符号“///”。你可以把文档注释放在想要描述的成员之前:

/// <summary>Class to tear a Webpage from a Webserver</summary>

public class RequestWebPage

使用<para>和 </para>标签,为描述添加段落。用<see>标签引用其它已有了注释的成员。

/// <para>Included in the <see cref="RequestWebPage"/> class</para>

增加一个链接到RequestWebPage类的描述。注意,用于标签的语法是XML语法,这意味着标签大写化的问题,而且标签必须正确地嵌套。当为一个成员添加文档时,另一个有趣的标签是<seealso> 。它允许你描述可能使读者非常感兴趣的其它话题。

/// <seealso cref="System.Net"/>

前面的例子告诉读者,他可能也想查阅System.Net 名字空间的文档。你一定要给超出当前范围的项目规定一个完全资格名。作为许诺,清单9.5 包含 RequestWebPage类中正在工作的文档的所有例子。看一下如何使用标签以及嵌套如何为组件产生文档。

清单 9.5 利用 <summary>, <see>, <para>, and <seealso> 标签描述一个成员



1: using System;
2: using System.Net;
3: using System.IO;
4: using System.Text;
5: 
6: /// <summary>Class to tear a Webpage from a Webserver</summary>
7: public class RequestWebPage
8: {
9: private const int BUFFER_SIZE = 128;
10: 
11: /// <summary>m_strURL stores the URL of the Webpage</summary>
12: private string m_strURL;
13: 
14: /// <summary>RequestWebPage() is the constructor for the class 
15: /// <see cref="RequestWebPage"/> when called without arguments.</summary>
16: public RequestWebPage()
17: {
18: }
19: 
20: /// <summary>RequestWebPage(string strURL) is the constructor for the class
21: /// <see cref="RequestWebPage"/> when called with an URL as parameter.</summary>
22: public RequestWebPage(string strURL)
23: {
24: m_strURL = strURL;
25: }
26: 
27: public string URL
28: {
29: get { return m_strURL; }
30: set { m_strURL = value; }
31: }
32: 
33: /// <summary>The GetContent(out string strContent) method:
34: /// <para>Included in the <see cref="RequestWebPage"/> class</para>
35: /// <para>Uses variable <see cref="m_strURL"/></para>
36: /// <para>Used to retrieve the content of a Webpage. The URL
37: /// of the Webpage (including http://) must already be 
38: /// stored in the private variable m_strURL. 
39: /// To do so, call the constructor of the RequestWebPage 
40: /// class, or set its property <see cref="URL"/> to the URL string.</para>
41: /// </summary>
42: /// <seealso cref="System.Net"/>
43: /// <seealso cref="System.Net.WebResponse"/>
44: /// <seealso cref="System.Net.WebRequest"/>
45: /// <seealso cref="System.Net.WebRequestFactory"/>
46: /// <seealso cref="System.IO.Stream"/> 
47: /// <seealso cref="System.Text.StringBuilder"/>
48: /// <seealso cref="System.ArgumentException"/>
49: 
50: public bool GetContent(out string strContent)
51: {
52: strContent = "";
53: // …
54: return true;
55: }
56: }



9.2.2 添加备注和列表

<remarks> 标签是规定大量文档的地方。与之相比, <summary>只仅仅规定了成员的简短描述。你不限于只提供段落文本(使用<para>标签)。例如,你可以在备注部分包含bulleted(和有限偶数)列表(list):

/// <list type="bullet">
/// <item>Constructor 
/// <see cref="RequestWebPage()"/> or
/// <see cref="RequestWebPage(string)"/>
/// </item>
/// </list>

这个list有一项(item),且该item引用了两个不同的构造函数描述。你可以根据需要,任意往list item中添加内容。另一个在备注部分很好用的标签是<paramref>。例如,你可以用<paramref>来引用和描述传递给构造函数的参数:

/// <remarks>Stores the URL from the parameter /// <paramref name="strURL"/> in 
/// the private variable <see cref="m_strURL"/>.</remarks>
public RequestWebPage(string strURL)

在清单9.6中,你可以看到所有的这些以及前面的标签正在起作用。

清单9.6 为文档添加一个备注和bullet list



1: using System;
2: using System.Net;
3: using System.IO;
4: using System.Text;
5: 
6: /// <summary>Class to tear a Webpage from a Webserver</summary>
7: /// <remarks>The class RequestWebPage provides:
8: /// <para>Methods:
9: /// <list type="bullet">
10: /// <item>Constructor 
11: /// <see cref="RequestWebPage()"/> or
12: /// <see cref="RequestWebPage(string)"/>
13: /// </item>
14: /// </list>
15: /// </para>
16: /// <para>Properties:
17: /// <list type="bullet">
18: /// <item>
19: /// <see cref="URL"/>
20: /// </item>
21: /// </list>
22: /// </para>
23: /// </remarks>
24: public class RequestWebPage
25: {
26: private const int BUFFER_SIZE = 128;
27: 
28: /// <summary>m_strURL stores the URL of the Webpage</summary>
29: private string m_strURL;
30: 
31: /// <summary>RequestWebPage() is the constructor for the class 
32: /// <see cref="RequestWebPage"/> when called without arguments.</summary>
33: public RequestWebPage()
34: {
35: }
36: 
37: /// <summary>RequestWebPage(string strURL) is the constructor for the class
38: /// <see cref="RequestWebPage"/> when called with an URL as parameter.</summary>
39: /// <remarks>Stores the URL from the parameter <paramref name="strURL"/> in
40: /// the private variable <see cref="m_strURL"/>.</remarks>
41: public RequestWebPage(string strURL)
42: {
43: m_strURL = strURL;
44: }
45: 
46: /// <remarks>Sets the value of <see cref="m_strURL"/>.
47: /// Returns the value of <see cref="m_strURL"/>.</remarks>
48: public string URL
49: {
50: get { return m_strURL; }
51: set { m_strURL = value; }
52: }
53: 
54: /// <summary>The GetContent(out string strContent) method:
55: /// <para>Included in the <see cref="RequestWebPage"/> class</para>
56: /// <para>Uses variable <see cref="m_strURL"/></para>
57: /// <para>Used to retrieve the content of a Webpage. The URL
58: /// of the Webpage (including http://) must already be 
59: /// stored in the private variable m_strURL. 
60: /// To do so, call the constructor of the RequestWebPage 
61: /// class, or set its property <see cref="URL"/> to the URL string.</para>
62: /// </summary>
63: /// <remarks>Retrieves the content of the Webpage specified in 
64: /// the property<see cref="URL"/> and hands it over to the out 
65: /// parameter <paramref name="strContent"/>.
66: /// The method is implemented using:
67: /// <list>
68: /// <item>The <see cref="System.Net.WebRequestFactory.Create"/>method.</item>
69: /// <item>The <see cref="System.Net.WebRequest.GetResponse"/> method.</item>
70: /// <item>The <see cref="System.Net.WebResponse.GetResponseStream"/>method</item>
71: /// <item>The <see cref="System.IO.Stream.Read"/> method</item>
72: /// <item>The <see cref="System.Text.StringBuilder.Append"/> method</item>
73: /// <item>The <see cref="System.Text.Encoding.ASCII"/> property together with its
74: /// <see cref="System.Text.Encoding.ASCII.GetString"/> method</item>
75: /// <item>The <see cref="System.Object.ToString"/> method for the 
76: /// <see cref="System.IO.Stream"/> object.</item>
77: /// </list>
78: /// </remarks>
79: /// <seealso cref="System.Net"/>
80: public bool GetContent(out string strContent)
81: {
82: strContent = "";
83: // …
84: return true;
85: }
86: }



9.2.3 提供例子

要想说明一个对象和方法的用法,最好的办法是提供优秀源代码的例子。因此,不要诧异文档注释也有用于声明例子的标签: <example> and <code>。 <example>标签包含了包括描述和代码的整个例子,而 <code> 标签仅包含了例子的代码(令人惊讶)。
清单9.7 说明如何实现代码例子。包括的例子用于两个构造函数。你必须给GetContent方法提供例子。

清单.7 利用例子解释概念



1: using System;
2: using System.Net;
3: using System.IO;
4: using System.Text;
5: 
6: /// <summary>Class to tear a Webpage from a Webserver</summary>
7: /// <remarks> … </remarks>
8: public class RequestWebPage
9: {
10: private const int BUFFER_SIZE = 12 


第八章 用C#写组件

这一章关于用C#写组件。你学到如何写一个组件,如何编译它,且如何在一个客户程序中使用它。更深入一步是运用
名字空间来组织你的应用程序。

这章由两个主要大节构成:
。你的第一个组件
。使用名字空间工作

8.1 你的第一个组件 

到目前为止,在本书中提到的例子都是在同一个应用程序中直接使用一个类。类和它的使用者被包含在同一个执行文
件中。现在我们将把类和使用者分离到组件和客户,它们分别位于不同的二进制文件中(可执行文件)。
尽管你仍然为组件创建一个 DLL,但其步骤与用C++写一个COM组件差别很大。你很少涉及到底层结构。以下小节说明
了如何构建一个组件以及使用到它的客户:

。构建组件
。编译组件
。创建一个简单的客户应用程序

8.1.1 构建组件

因为我是一个使用范例迷,我决定创建一个相关Web的类,以方便你们使用。它返回一个Web网页并储存在一个字符串
变量中,以供后来重用。所有这些编写都参考了.NET框架的帮助文档。

类名为RequestWebPage;它有两个构造函数—— 一个属性和一个方法。属性被命名为URL,且它储存了网页的Web地
址,由方法GetContent返回。这个方法为你做了所有的工作(见清单8.1)。

清单 8.1 用于从Web服务器返回HTML网页的RequestWebPage 类



1: using System;
2: using System.Net;
3: using System.IO;
4: using System.Text;
5: 
6: public class RequestWebPage
7: {
8: private const int BUFFER_SIZE = 128;
9: private string m_strURL;
10: 
11: public RequestWebPage()
12: {
13: }
14: 
15: public RequestWebPage(string strURL)
16: {
17: m_strURL = strURL;
18: }
19: 
20: public string URL
21: {
22: get { return m_strURL; }
23: set { m_strURL = value; }
24: }
25: public void GetContent(out string strContent)
26: {
27: // 检查 URL
28: if (m_strURL == "")
29: throw new ArgumentException("URL must be provided.");
30: 
31: WebRequest theRequest = (WebRequest) WebRequestFactory.Create(m_strURL);
32: WebResponse theResponse = theRequest.GetResponse();
33: 
34: // 给回应设置字节缓冲区
35: int BytesRead = 0;
36: Byte[] Buffer = new Byte[BUFFER_SIZE];
37: 
38: Stream ResponseStream = theResponse.GetResponseStream();
39: BytesRead = ResponseStream.Read(Buffer, 0, BUFFER_SIZE);
40: 
41: //使用 StringBuilder 以加速分配过程
42: StringBuilder strResponse = new StringBuilder("");
43: while (BytesRead != 0 ) 
44: {
45: strResponse.Append(Encoding.ASCII.GetString(Buffer,0,BytesRead));
46: BytesRead = ResponseStream.Read(Buffer, 0, BUFFER_SIZE);
47: }
48: 
49: // 赋给输出参数
50: strContent = strResponse.ToString();
51: }
52: }



本应该利用无参数构造函数完成工作,但我决定在构造函数中初始化URL,这可能会很有用。当后来决定要改变URL
时——为了返回第二个网页,例如,通过URL属性的get和set访问标志使它被公开了。

有趣的事始于GetContent方法。首先,代码对URL实行十分简单的检查,如果它不适合,就会引发一个
ArgumentException 异常。之后,我请求WebRequestFactory ,以创建一个基于传递给它的URL的WebRequest对象。
因为我不想发送cookies、附加头和询问串等,所以立即访问WebResponse(第32行)。如果你需要请求上述任何的功
能,必须在这一行之前实现它们。

第35和36行初始化一个字节缓冲区,它用于从返回流中读数据。暂时忽略StringBuilder 类,只要返回流中仍然有要
读的数据,while循环就会简单地重复。最后的读操作将返回零,因此结束了该循环。
现在我想回到StringBuilder类。为什么用这个类的实例而不是简单地把字节缓冲区合并到一个字符串变量?看下面这
个例子:


strMyString = strMyString + "some more text"; 


这里很清楚,你正在拷贝值。常量 "some more text" 以一个字符串变量类型被加框,且根据加法操作创建了一个新
的字符串变量。接着被赋给了 strMyString。有很多次拷贝,是吗?
但你可能引起争论


strMyString += "some more text";


不要炫耀这种行为。对不起,对于C#这是一个错误的答案。其操作完全与所描述的赋值操作相同。

不涉及该问题的另外的途径是使用StringBuilder类。它利用一个缓冲区进行工作,接着,在没有发生我所描述的拷贝
行为的情况下,你进行追加、插入、删除和替换操作。这就是为什么我在类中使用它来合并那些读自缓冲区中的内容。
该缓冲区把我带进了这个类中最后重要的代码片段——第45行的编码转换。它只不过涉及到我获得请求的字符集。

最后,当所有的内容被读入且被转换时,我显式地从 StringBuilder请求一个字符串对象并把它赋给了输出变量。一
个返回值仍然会导致另外的拷贝操作。

8.1.2 编译组件

到目前为止,你所做的工作与在正常应用程序的内部编写一个类没有什么区别。所不同的是编译过程。你必须创建一
个库而不是一个应用程序:
csc /r:System.Net.dll /t:library /out:wrq.dll webrequest.cs
编译开关/t:library 告诉C#编译,要创建一个库而不是搜寻一个静态 Main方法。同样,因为我正在使用 
System.Net名字空间,所以必须引用 (/r:)它的库,这个库就是System.Net.dll。
你的库命名为 wrq.dll,现在它准备用于一个客户应用程序。因为在这章中我仅使用私有组件工作,所以你不必把库
拷贝到一个特殊的位置,而是拷贝到客户应用程序目录。

8.1.3 创建一个简单的客户应用程序

当一个组件被写成且被成功地编译时,你所要做的就是在客户应用程序中使用它。我再次创建了一个简单的命令行应
用程序,它返回了我维护的一个开发站点的首页(见清单8.2)。

清单 8.2 用 RequestWebPage 类返回一个简单的网页



1: using System;
2: 
3: class TestWebReq
4: {
5: public static void Main()
6: {
7: RequestWebPage wrq = new RequestWebPage();
8: wrq.URL = "http://www.alphasierrapapa.com/iisdev/";
9: 
10: string strResult;
11: try
12: {
13: wrq.GetContent(out strResult);
14: }
15: catch (Exception e)
16: {
17: Console.WriteLine(e);
18: return;
19: }
20: 
21: Console.WriteLine(strResult);
22: }



注意,我已经在一个try catch语句中包含了对 GetContent的调用。其中的一个原因是GetContent可能引发一个 
ArgumentException异常。此外,我在组件内部调用的.NET框架类也可以引发异常。因为我不能在类的内部处理这些异常,所以我必须在这里处理它们。

其余的代码只不过是简单的组件使用——调用标准的构造函数,存取一个属性,并执行一个方法。但等一下:你需要
注意何时编译应用程序。一定要告诉编译器,让它引用你的新组件库DLL:csc /r:wrq.dll wrclient.cs

现在万事俱备,你可以测试程序了。输出结果会滚屏,但你可以看到应用程序工作。使用了常规的表达式,你也可以
增加代码,以解析返回的HTML,并依据你个人的喜好,提取信息。我预想会使用到这个类新版本的SSL(安全套接字层),用于ASP+网页中的在线信用卡验证。

你可能会注意到,没有特殊的using 语句用于你所创建的库。原因是你在组件的源文件中没有定义名字空间。

8.2 使用名字空间工作

你经常使用到名字空间,例如System 和System.Net。C#利用名字空间来组织程序,而且分层的组织使一个程序的成员
传到另一个程序变得更容易。

尽管不强制,但你总要创建名字空间,以清楚地识别应用程序的层次。.NET框架会给出构建这种分层的良好思想。
以下的代码片段显示了在C#原文件中简单的名字空间 My.Test(点号表示一个分层等级)的声明:



namespace My.Test
{
//这里的任何东西属于名字空间
}


当你访问名字空间中的一个成员时,也有必要使用名字空间标识符完全地验证它,或者利用using标志把所有的成员引
入到你当前的名字空间。本书前面的例子演示了如何应用这些技术。

在开始使用名字空间之前,只有少数有关存取安全的词。如果你不增加一个特定的存取修饰符,所有的类型将被默认
为internal 。当你想从外部访问该类型时,使用 public 。不允许其它的修饰符。

这是关于名字空间充分的理论。让我们继续实现该理论——以下小节说明了当构建组件应用程序时,如何使用名字空

。在名字空间中包装类
。在客户应用程序中使用名字空间
。为名字空间增加多个类

8.2.1 在名字空间中包装类
既然你知道了名字空间的理论含义,那么让我们在现实生活中实现它吧。在这个和即将讨论到的例子中,自然选择到
的名字空间是Presenting.CSharp。为了不使你厌烦,仅仅是把RequestWebPage包装到Presenting.CSharp中,我决定写一个类,用于 Whois查找(见清单8.3)。

清单 8.3 在名字空间中实现 WhoisLookup类



1: using System;
2: using System.Net.Sockets;
3: using System.IO;
4: using System.Text;
5: 
6: namespace Presenting.CSharp
7: {
8: public class WhoisLookup 
9: {
10: public static bool Query(string strDomain, out string strWhoisInfo)
11: {
12: const int BUFFER_SIZE = 128;
13: 
14: if ("" == strDomain)
15: throw new ArgumentException("You must specify a domain name.");
16: 
17: TCPClient tcpc = new TCPClient();
18: strWhoisInfo = "N/A";
19: 
20: // 企图连接 whois 服务器
21: if (tcpc.Connect("whois.networksolutions.com", 43) != 0)
22: return false;
23: 
24: // 获取流
25: Stream s = tcpc.GetStream();
26: 
27: // 发送请求
28: strDomain += "\r\n";
29: Byte[] bDomArr = Encoding.ASCII.GetBytes(strDomain.ToCharArray());
30: s.Write(bDomArr, 0, strDomain.Length); 
31: 
32: Byte[] Buffer = new Byte[BUFFER_SIZE];
33: StringBuilder strWhoisResponse = new StringBuilder("");
34: 
35: int BytesRead = s.Read(Buffer, 0, BUFFER_SIZE);
36: while (BytesRead != 0 ) 
37: {
38: strWhoisResponse.Append(Encoding.ASCII.GetString(Buffer,0,BytesRead));
39: BytesRead = s.Read(Buffer, 0, BUFFER_SIZE);
40: }
41: 
42: tcpc.Close();
43: strWhoisInfo = strWhoisResponse.ToString();
44: return true;
45: }
46: }
47: }


名字空间在第6行被声明,而且它用第7行和第47行的大括弧括住了WhoisLookup类。要声明自己新的名字空间,实际要
做的就是这些。

在WhoisLookup类中当然具有一些有趣代码,特别是由于它说明了使用C#进行socket编程是多么的容易。在static 
Query method中经过 not-so-stellar域名检查之后,我实例化了TCPClient类型的一个对象,它用来完成具有 Whois服务器的43端口上的所有通讯。
在第21行建立了服务器连接:
if (tcpc.Connect("whois.networksolutions.com", 43) != 0)
因为连接失败是预料到的结果,所以这个方法不能引发一个异常。(你还记住异常处理的“要”和“不要”吗?) 返
回值是一个错误代码,而返回零则说明连接成功。

对于 Whois 查找,我必须首先发出一些信息给服务器——我要查找的域名。要完成此项工作,首先获得一个引用给当
前TCP连接的双向流(第25行)。接着附加上一个回车/换行对 给域名,以表示询问结束。重新以字节数组打包,向Whois 服务器发送一个请求(第30行)。

余下的代码和RequestWebPage类极其相似。在该类中,我再次利用一个缓冲区从远程服务器读入回应。当缓冲区完成
读入后,连接被断开。返回的回应被转给了调用者。我明确地调用 Close 方法的原因是我不想等待垃圾收集器毁坏连接。

连接时间不要过长,以免占用TCP端口这种稀有资源。

在可以使用.NET 组件中的类之前,你必须把它作为一个库来编译。尽管现在有了一个已定义的名字空间,该编译命令
仍然没有变:csc /r:System.Net.dll /t:library /out:whois.dll whois.cs

注意,如果你想该库按与C#源文件相同的方法命名,就没有必要规定 /out:开关。规定该开关是一个良好的习惯,因
为很多项目不会只由单个源文件组成。如果你规定了多个源文件,该库以名单中的第一个命名。

8.2.2 在客户应用程序中使用名字空间

由于你使用了名字空间开发组件,所以客户也要引入名字空间
using Presenting.CSharp;
或者给名字空间中的成员使用完全资格名(fully qualified name),例如
Presenting.CSharp.WhoisLookup.Query(…);

如果你不期望在名字空间中引入的成员之间出现冲突,using 标志( directive)是首选,特别是由于你具有很少的
类型时。使用组件的客户程序样本在清单8.4中给出。

清单 8.4 测试 WhoisLookup 组件



1: using System;
2: using Presenting.CSharp;
3: 
4: class TestWhois
5: {
6: public static void Main()
7: {
8: string strResult;
9: bool bReturnValue;
10: 
11: try
12: {
13: bReturnValue = WhoisLookup.Query("microsoft.com", out strResult);
14: }
15: catch (Exception e)
16: {
17: Console.WriteLine(e);
18: return;
19: }
20: if (bReturnValue)
21: Console.WriteLine(strResult);
22: else
23: Console.WriteLine("Could not obtain information from server.");
24: }
25: }


第2行利用using 标志引入了Presenting.CSharp名字空间。现在,我无论什么时候引用WhoisLookup ,都可以忽略名
字空间的完全资格名了。

该程序对 microsoft.com 域进行一次Whois 查找——你也可以用自己的域名代替microsoft.com 。允许命令行参数传
递域名,可使客户的用途更广。清单8.5 实现了该功能,但它不能实现适当的异常处理(为了使程序更短)。

清单 8.5 传递命令行参数给Query 方法



1: using System;
2: using Presenting.CSharp;
3: 
4: class WhoisShort
5: {
6: public static void Main(string[] args)
7: {
8: string strResult;
9: bool bReturnValue;
10: 
11: bReturnValue = WhoisLookup.Query(args[0], out strResult);
12: 
13: if (bReturnValue)
14: Console.WriteLine(strResult);
15: else
16: Console.WriteLine("Lookup failed.");
17: }
18: }



你所必须做的就是编译这个应用程序:
csc /r:whois.dll whoisclnt.cs
接着可以使用命令行参数执行该应用程序。例如,以 microsoft.com参数执行
whoisclnt microsoft.com

当查询运行成功时,就会出现 microsoft.com的注册信息。(清单8.6 显示了输出的简略版本) 这是一个很方便的
小程序,通过组件化的途径写成的,花不到一个小时。如果用C++编写,要花多长时间?很幸运,我再也想不起当第一次用C++这样做时,花了多长的时间。

清单 8.6 有关 microsoft.com (简略) 的Whois 信息



D:\CSharp\Samples\Namespace>whoisclient


Registrant:
Microsoft Corporation (MICROSOFT-DOM)
1 microsoft way
redmond, WA 98052
US
Domain Name: MICROSOFT.COM

Administrative Contact:
Microsoft Hostmaster (MH37-ORG) msnhst@MICROSOFT.COM
Technical Contact, Zone Contact:
MSN NOC (MN5-ORG) msnnoc@MICROSOFT.COM
Billing Contact:
Microsoft-Internic Billing Issues (MDB-ORG) msnbill@MICROSOFT.COM

Record last updated on 20-May-2000.
Record expires on 03-May-2010.
Record created on 02-May-1991.
Database last updated on 9-Jun-2000 13:50:52 EDT.

Domain servers in listed order:

ATBD.MICROSOFT.COM 131.107.1.7
DNS1.MICROSOFT.COM 131.107.1.240
DNS4.CP.MSFT.NET 207.46.138.11
DNS5.CP.MSFT.NET 207.46.138.12



8.2.3 增加多个类到名字空间

使WhoisLookup和RequestWebPage 类共存于同一个名字空间是多么的美妙。既然WhoisLookup已是名字空间的一部分,
所以你只须使RequestWebPage 类也成为该名字空间的一部分。必要的改变很容易被应用。你只需使用名字空间封装RequestWebPage 类就可以了:



namespace Presenting.CSharp
{
public class RequestWebPage 
{

}
}


尽管两个类包含于两个不同的文件,但在编译后,它们都是相同名字空间的一部分:
csc /r:System.Net.dll /t:library /out:presenting.csharp.dll whois.cs webrequest.cs

你不必要按照名字空间的名字给DLL命名。然而,这样做会有助你更容易你记住,当编译一个客户应用程序时要引用哪
一个库。

8.3 小结
在这一章中,你学到了如何构建一个可以在客户程序中使用的组件。最初,你不必关心名字空间,但后面第二个组件
中介绍了该特性。名字空间在内外部均是组织应用程序的好办法。
C#中的组件很容易被构建,而且只要库和应用程序共存于相同的目录,你甚至不必进行特殊的安装。当要创建必须被
多个客户使用的类库时,步骤就有所改变——而下一章将会告诉你为什么。 

第七章 异常处理

通用语言运行时(CLR)具有的一个很大的优势为,异常处理是跨语言被标准化的。一个在C#中所引发的异常可以在Visual Basic客户中得到处理。不再有 HRESULTs 或者 ISupportErrorInfo 接口。尽管跨语言异常处理的覆盖面很广,但这一章完全集中讨论C#异常处理。你稍为改变编译器的溢出处理行为,接着有趣的事情就开始了:你处理了该异常。要增加更多的手段,随后引发你所创建的异常。

7.1 校验(checked)和非校验(unchecked)语句

当你执行运算时,有可能会发生计算结果超出结果变量数据类型的有效范围。这种情况被称为溢出,依据不同的编程语言,你将被以某种方式通知——或者根本就没有被通知。(C++程序员听起来熟悉吗?)

那么,C#如何处理溢出的呢? 要找出其默认行为,请看我在这本书前面提到的阶乘的例子。(为了方便其见,前面的例子再次在清单 7.1 中给出)

清单 7.1 计算一个数的阶乘



1: using System;
2: 
3: class Factorial
4: {
5: public static void Main(string[] args)
6: {
7: long nFactorial = 1;
8: long nComputeTo = Int64.Parse(args[0]);
9: 
10: long nCurDig = 1;
11: for (nCurDig=1;nCurDig <= nComputeTo; nCurDig++)
12: nFactorial *= nCurDig;
13: 
14: Console.WriteLine("{0}! is {1}",nComputeTo, nFactorial);
15: }
16: }


当你象这样使用命令行执行程序时

factorial 2000

结果为0,什么也没有发生。因此,设想C#默默地处理溢出情况而不明确地警告你是安全的。
通过给整个应用程序(经编译器开关)或于语句级允许溢出校验,你就可以改变这种行为。以下两节分别解决一种方案。

7.1.1 给溢出校验设置编译器

如果你想给整个应用程序控制溢出校验,C#编译器设置选择是正是你所要找的。默认地,溢出校验是禁用的。要明确地要求它,运行以下编译器命令:



csc factorial.cs /checked+


现在当你用2000参数执行应用程序时,CLR通知你溢出异常。

按OK键离开对话框揭示了异常信息:



Exception occurred: System.OverflowException
at Factorial.Main(System.String[])


现在你了解了溢出条件引发了一个 System.OverflowException异常。下一节,在我们完成语法校验之后,如何捕获并处理所出现的异常?

7.1.2 语法溢出校验

如果你不想给整个应用程序允许溢出校验,仅给某些代码段允许校验,你可能会很舒适。对于这种场合,你可能象清单7.2中显示的那样,使用校验语句。

清单 7.2  阶乘计算中的溢出校验



1: using System;
2: 
3: class Factorial
4: {
5: public static void Main(string[] args)
6: {
7: long nFactorial = 1;
8: long nComputeTo = Int64.Parse(args[0]);
9: 
10: long nCurDig = 1;
11: 
12: for (nCurDig=1;nCurDig <= nComputeTo; nCurDig++)
13: checked { nFactorial *= nCurDig; }
14: 
15: Console.WriteLine("{0}! is {1}",nComputeTo, nFactorial);
16: }
17: }


甚至就如你运用标志 checked-编译了该代码,在第13行中,溢出校验仍然会对乘法实现检查。错误信息保持一致。

显示相反行为的语句是非校验(unchecked )。甚至如果允许了溢出校验(给编译器加上checked+标志),被unchecked 语句所括住的代码也将不会引发溢出异常:



unchecked
{
nFactorial *= nCurDig;
}



7.2  异常处理语句

既然你知道了如何产生一个异常(你会发现更多的方法,相信我),仍然存在如何处理它的问题。如果你是一个 C++ WIN32 程序员,肯定熟悉SEH(结构异常处理)。你将从中找到安慰,C#中的命令几乎是相同的,而且它们也以相似的方式运作。

The following three sections introduce C#’s exception-handling statements:

以下三节介绍了C#的异常处理语句:

。用 try-catch 捕获异常
。用try-finally 清除异常
。用try-catch-finally 处理所有的异常

7.2.1 使用 try 和 catch捕获异常

你肯定会对一件事非常感兴趣——不要提示给用户那令人讨厌的异常消息,以便你的应用程序继续执行。要这样,你必须捕获(处理)该异常。
这样使用的语句是try 和 catch。try包含可能会产生异常的语句,而catch处理一个异常,如果有异常存在的话。清单7.3 用try 和 catch为OverflowException 实现异常处理。

清单7.3 捕获由Factorial Calculation引发的OverflowException 异常



1: using System;
2: 
3: class Factorial
4: {
5: public static void Main(string[] args)
6: {
7: long nFactorial = 1, nCurDig=1;
8: long nComputeTo = Int64.Parse(args[0]);
9: 
10: try
11: {
12: checked
13: {
14: for (;nCurDig <= nComputeTo; nCurDig++)
15: nFactorial *= nCurDig;
16: }
17: }
18: catch (OverflowException oe)
19: {
20: Console.WriteLine("Computing {0} caused an overflow exception", nComputeTo);
21: return;
22: }
23: 
24: Console.WriteLine("{0}! is {1}",nComputeTo, nFactorial);
25: }
26: }


为了说明清楚,我扩展了某些代码段,而且我也保证异常是由checked 语句产生的,甚至当你忘记了编译器设置时。
正如你所见,异常处理并不麻烦。你所有要做的是:在try语句中包含容易产生异常的代码,接着捕获异常,该异常在这个例子中是OverflowException类型。无论一个异常什么时候被引发,在catch段里的代码会注意进行适当的处理。
如果你不事先知道哪一种异常会被预期,而仍然想处于安全状态,简单地忽略异常的类型。



try
{

}
catch
{

}


但是,通过这个途径,你不能获得对异常对象的访问,而该对象含有重要的出错信息。一般化异常处理代码象这样:



try
{

}
catch(System.Exception e)
{

}


注意,你不能用ref或out 修饰符传递 e 对象给一个方法,也不能赋给它一个不同的值。

7.2.2 使用 try 和 finally 清除异常

如果你更关心清除而不是错误处理, try 和 finally 会获得你的喜欢。它不仅抑制了出错消息,而且所有包含在 finally 块中的代码在异常被引发后仍然会被执行。尽管程序不正常终止,但你还可以为用户获取一条消息,如清单 7.4 所示。

清单 7.4 在finally 语句中处理异常



1: using System;
2: 
3: class Factorial
4: {
5: public static void Main(string[] args)
6: {
7: long nFactorial = 1, nCurDig=1;
8: long nComputeTo = Int64.Parse(args[0]);
9: bool bAllFine = false;
10: 
11: try
12: {
13: checked
14: {
15: for (;nCurDig <= nComputeTo; nCurDig++)
16: nFactorial *= nCurDig;
17: }
18: bAllFine = true;
19: }
20: finally
21: {
22: if (!bAllFine)
23: Console.WriteLine("Computing {0} caused an overflow exception", nComputeTo);
24: else
25: Console.WriteLine("{0}! is {1}",nComputeTo, nFactorial);
26: }
27: }
28: }



通过检测该代码,你可能会猜到,即使没有引发异常处理,finally也会被执行。这是真的——在finally中的代码总是会被执行的,不管是否具有异常条件。为了举例说明如何在两种情况下提供一些有意义的信息给用户, 我引进了新变量bAllFine。bAllFine告诉finally 语段,它是否是因为一个异常或者仅是因为计算的顺利完成而被调用。

作为一个习惯了SEH程序员,你可能会想,是否有一个与__leave 语句等价的语句,该语句在C++中很管用。如果你还不了解,在C++中的__leave 语句是用来提前终止 try 语段中的执行代码,并立即跳转到finally 语段 。坏消息, C# 中没有__leave 语句。但是,在清单 7.5 中的代码演示了一个你可以实现的方案。

清单 7.5 从 try语句 跳转到finally 语句



1: using System;
2: 
3: class JumpTest
4: {
5: public static void Main()
6: {
7: try
8: {
9: Console.WriteLine("try");
10: goto __leave;
11: }
12: finally
13: {
14: Console.WriteLine("finally");
15: }
16: 
17: __leave:
18: Console.WriteLine("__leave");
19: }
20: }


当这个应用程序运行时,输出结果为



try
finally
__leave


一个 goto 语句不能退出 一个finally 语段。甚至把 goto 语句放在 try 语句 段中,还是会立即返回控制到 finally 语段。因此,goto 只是离开了 try 语段并跳转到finally 语段。直到 finally 中的代码完成运行后,才能到达__leave 标签。按这种方式,你可以模仿在SEH中使用的的__leave 语句。

顺便地,你可能怀疑goto 语句被忽略了,因为它是try 语句中的最后一条语句,并且控制自动地转移到了 finally 。为了证明不是这样,试把goto 语句放到Console.WriteLine 方法调用之前。尽管由于不可到达代码你得到了编译器的警告,但是你将看到goto语句实际上被执行了,且没有为 try 字符串产生的输出。

7.2.3 使用try-catch-finally处理所有异常

应用程序最有可能的途径是合并前面两种错误处理技术——捕获错误、清除并继续执行应用程序。所有你要做的是在出错处理代码中使用 try 、catch 和 finally语句。清单 7.6 显示了处理零除错误的途径。

清单 7.6 实现多个catch 语句



1: using System;
2: 
3: class CatchIT
4: {
5: public static void Main()
6: {
7: try
8: {
9: int nTheZero = 0;
10: int nResult = 10 / nTheZero;
11: }
12: catch(DivideByZeroException divEx)
13: {
14: Console.WriteLine("divide by zero occurred!");
15: }
16: catch(Exception Ex)
17: {
18: Console.WriteLine("some other exception");
19: }
20: finally
21: {
22: }
23: }
24: }


这个例子的技巧为,它包含了多个catch 语句。第一个捕获了更可能出现的DivideByZeroException异常,而第二个catch语句通过捕获普通异常处理了所有剩下来的异常。

你肯定总是首先捕获特定的异常,接着是普通的异常。如果你不按这个顺序捕获异常,会发生什么事呢?清单7.7中的代码有说明。

清单7.7 顺序不适当的 catch 语句



1: try
2: {
3: int nTheZero = 0;
4: int nResult = 10 / nTheZero;
5: }
6: catch(Exception Ex)
7: {
8: Console.WriteLine("exception " + Ex.ToString());
9: }
10: catch(DivideByZeroException divEx)
11: {
12: Console.WriteLine("never going to see that");
13: }



编译器将捕获到一个小错误,并类似这样报告该错误:
wrongcatch.cs(10,9): error CS0160: A previous catch clause already 
catches all exceptions of this or a super type (‘System.Exception’)

最后,我必须告发CLR异常与SEH相比时的一个缺点(或差别):没有 EXCEPTION_CONTINUE_EXECUTION标识符的等价物,它在SEH异常过滤器中很有用。基本上,EXCEPTION_CONTINUE_EXECUTION 允许你重新执行负责异常的代码片段。在重新执行之前,你有机会更改变量等。我个人特别喜欢的技术为,使用访问违例异常,按需要实施内存分配。

7.3 引发异常

当你必须捕获异常时,其他人首先必须首先能够引发异常。而且,不仅其他人能够引发,你也可以负责引发。其相当简单:


throw new ArgumentException("Argument can’t be 5");


你所需要的是throw 语句和一个适当的异常类。我已经从表7.1提供的清单中选出一个异常给这个例子。

表 7.1 Runtime提供的标准异常



异常类型 描述

Exception 所有异常对象的基类
SystemException 运行时产生的所有错误的基类
IndexOutOfRangeException 当一个数组的下标超出范围时运行时引发
NullReferenceException 当一个空对象被引用时运行时引发
InvalidOperationException 当对方法的调用对对象的当前状态无效时,由某些方法引发
ArgumentException 所有参数异常的基类
ArgumentNullException 在参数为空(不允许)的情况下,由方法引发
ArgumentOutOfRangeException 当参数不在一个给定范围之内时,由方法引发
InteropException 目标在或发生在CLR外面环境中的异常的基类
ComException 包含COM 类的HRESULT信息的异常
SEHException 封装win32 结构异常处理信息的异常


然而,在catch语句的内部,你已经有了随意处置的异常,就不必创建一个新异常。可能在表7.1 中的异常没有一个符合你特殊的要求——为什么不创建一个新的异常?在即将要学到小节中,都涉及到这两个话题。

7.3.1 重新引发异常

当处于一个catch 语句的内部时,你可能决定引发一个目前正在再度处理的异常,留下进一步的处理给一些外部的try-catch 语句。该方法的例子如 清单7.8所示。

清单 7.8 重新引发一个异常



1: try
2: {
3: checked
4: {
5: for (;nCurDig <= nComputeTo; nCurDig++)
6: nFactorial *= nCurDig;
7: }
8: }
9: catch (OverflowException oe)
10: {
11: Console.WriteLine("Computing {0} caused an overflow exception", nComputeTo);
12: throw;
13: }


注意,我不必规定所声明的异常变量。尽管它是可选的,但你也可以这样写:
throw oe;
现在有时还必须留意这个异常。

7.3.2 创建自己的异常类

尽管建议使用预定义的异常类,但对于实际场合,创建自己的异常类可能会方便。创建自己的异常类,允许你的异常类的使用者根据该异常类采取不同的手段。

在清单 7.9 中出现的异常类 MyImportantException遵循两个规则:第一,它用Exception结束类名。第二,它实现了所有三个被推荐的通用结构。你也应该遵守这些规则。

清单 7.9 实现自己的异常类 MyImportantException



1: using System;
2: 
3: public class MyImportantException:Exception
4: {
5: public MyImportantException()
6: :base() {}
7: 
8: public MyImportantException(string message)
9: :base(message) {}
10: 
11: public MyImportantException(string message, Exception inner)
12: :base(message,inner) {}
13: }
14: 
15: public class ExceptionTestApp
16: {
17: public static void TestThrow()
18: {
19: throw new MyImportantException("something bad has happened.");
20: }
21: 
22: public static void Main()
23: {
24: try
25: {
26: ExceptionTestApp.TestThrow();
27: }
28: catch (Exception e)
29: {
30: Console.WriteLine(e);
31: }
32: }
33: }



正如你所看到的,MyImportantException 异常类不能实现任何特殊的功能,但它完全基于System.Exception类。程序的剩余部分测试新的异常类,给System.Exception 类使用一个catch 语句。

如果没有特殊的实现而只是给MyImportantException定义了三个构造函数,创建它又有什么意义呢?它是一个重要的类型——你可以在catch语句中使用它,代替更为普通的异常类。可能引发你的新异常的客户代码可以按规定的catch代码发挥作用。

当使用自己的名字空间编写一个类库时,也要把异常放到该名字空间。尽管它并没有出现在这个例子中,你还是应该使用适当的属性,为扩展了的错误信息扩充你的异常类。

7.4 异常处理的“要”和“不要” 

作为最后的忠告之语,这里是对异常引发和处理所要做和不要做的清单:



。当引发异常时,要提供有意义的文本。
。要引发异常仅当条件是真正异常;也就是当一个正常的返回值不满足时。
。如果你的方法或属性被传递一个坏参数,要引发一个ArgumentException异常。
。当调用操作不适合对象的当前状态时,要引发一个 InvalidOperationException异常。
。要引发最适合的异常。
。要使用链接异常,它们允许你跟踪异常树。
。不要为正常或预期的错误使用异常。
。不要为流程的正常控制使用异常。
。不要在方法中引发 NullReferenceException或IndexOutOfRangeException异常。



7.5 小结

这一章由介绍溢出校验开始。你可以使用编译器开关(默认是关),使整个应用程序允许或禁止溢出校验。如果需要微调控制,你可以使用校验和非校验语句,它允许你使用或不使用溢出校验来执行一段代码,尽管没有给应用程序设置开关。

当发生溢出时,一个异常就被引发了。如何处理异常取决于你。我提出了各种途径,包括你最有可能贯穿整个应用程序使用的:try、catch 和finally 语句。在伴随的多个例子中,你学到了它与WIN32结构异常处理(SEH)的差别。
异常处理是给类的用户; 然而,如果你负责创建新的类,就可以引发异常。有多种选择:引发早已捕获的异常,引发存在的框架异常,或者按规定的实际目标创建新的异常类。

最后,你需要阅读引发和处理异常的各种“要”和“不要”。

第六章 控制语句

有一种语句,你在每种编程语言控制流程语句中都可以找到。在这一章中,我介绍了C#的控制语句,它们分为两个主要部分:
。选择语句
。循环语句
如果你是C或C++程序员,很多信息会让你感到似曾相似;但是,你必须知道它们还存在着一些差别。

6.1 选择语句

当运用选择语句时,你定义了一个控制语句,它的值控制了哪个语句被执行。在C#中用到两个选择语句:
。if 语句
。switch 语句 

6.1.1 if 语句

最先且最常用到的语句是 if 语句。内含语句是否被执行取决于布尔表达式:



if (布尔表达式) 内含语句


当然,也可以有else 分枝,当布尔表达式的值为假时,该分支就被执行:



if (布尔表达式) 内含语句 else 内含语句


在执行某些语句之前就检查一个非零长字符串的例子: 



if (0 != strTest.Length)
{



这是一个布尔表达式。(!=表示不等于。) 但是,如果你来自C或者C++,可能会习惯于编写象这样的代码:



if (strTest.Length)
{



这在C#中不再工作,因为 if 语句仅允许布尔( bool) 数据类型的结果,而字符串的Length属性对象返回一个整形(integer)。编译器将出现以下错误信息:



error CS0029: Cannot implicitly convert type ’int’ to ’bool’ (不能隐式地转换类型 ’int’ 为 ’bool’。) 


上边是你必须改变的习惯,而下边将不会再在 if 语句中出现赋值错误:



if (nMyValue = 5) … 



正确的代码应为 



if (nMyValue == 5) … 



因为相等比较由==实行,就象在C和C++中一样。看以下有用的对比操作符(但并不是所有的数据类型都有效):
== ——如果两个值相同,返回真。
!= ——如果两个值不同,返回假。
<, <=, >, >= —— 如果满足了关系(小于、小于或等于、大于、大于或等于),返回真。

每个操作符是通过重载操作符被执行的,而且这种执行对数据类型有规定。如果你比较两个不同的类型,对于编译器,必须存在着一个隐式的转换,以便自动地创建必要的代码。但是,你可以执行一个显式的类型转换。

清单 6.1 中的代码演示了 if 语句的一些不同的使用场合,同时也演示了如何使用字符串数据类型。这个程序的主要思想是,确定传递给应用程序的第一个参数是否以大写字母、小写字母或者数字开始。 

清单 6.1 确定字符的形态 



1: using System;
2: 
3: class NestedIfApp
4: {
5: public static int Main(string[] args)
6: {
7: if (args.Length != 1) 
8: {
9: Console.WriteLine("Usage: one argument");
10: return 1; // error level
11: }
12: 
13: char chLetter = args[0][0];
14: 
15: if (chLetter >= ’A')
16: if (chLetter <= ’Z')
17: {
18: Console.WriteLine("{0} is uppercase",chLetter);
19: return 0;
20: }
21: 
22: chLetter = Char.FromString(args[0]);
23: if (chLetter >= ’a' && chLetter <= ’z')
24: Console.WriteLine("{0} is lowercase",chLetter);
25: 
26: if (Char.IsDigit((chLetter = args[0][0])))
27: Console.WriteLine("{0} is a digit",chLetter);
28: 
29: return 0;
30: }
31: } 



始于第7行的第一个 if 语段检测参数数组是否只有一个字符串。如果不满足条件,程序就在屏幕上显示用法信息,并终止运行。

可以采取多种方法从一个字符串中提取出单个字符——既可象第13行那样利用字符索引,也可以使用Char类的静态 FromString 方法,它返回字符串的第一个字符。

第16~20行的 if 语句块使用一个嵌套 的if 语句块检查大写字母。用逻辑“与”操作符(&&)可以胜任小写字母的检测,而最后通过使用Char类的静态函数IsDigit,就可以完成对数字的检测。

除了“&&”操作符之外,还有另一个条件逻辑操作符,它就是代表“或”的“&brvbar;&brvbar;”。两个逻辑操作符都 是“短路”式的。对于“&&”操作符,意味着如果条件“与”表达式的第一个结果返回一个假值,余下的条件“与”表达式就不会再被求值了。相对应,“&brvbar;&brvbar;”操作符当第一个真条件满足时,它就“短路”了。

我想让大家理解的是,要减少计算时间,你应该把最有可能使求值“短路”的表达式放在前面。同样你应该清楚,计算 if 语句中的某些值会存在着替在的危险。 



if (1 == 1 &brvbar;&brvbar; (5 == (strLength=str.Length)))
{
Console.WriteLine(strLength);



当然,这是一个极其夸张的例子,但它说明了这样的观点:第一条语句求值为真,那么第二条语句就不会被执行,它使变量strLength维持原值。给大家一个忠告:决不要在具有条件逻辑操作符的 if 语句中赋值。 

6.1.2 switch 语句

和 if 语句相比,switch语句有一个控制表达式,而且内含语句按它们所关联的控制表达式的常量运行。 



switch (控制表达式)
{
case 常量表达式:
内含语句
default:
内含语句



控制表达式所允许的数据类型为: sbyte, byte, short, ushort, uint, long, ulong, char, string, 或者枚举类型。只要使其它不同数据类型能隐式转换成上述的任何类型,用它作为控制表达式也很不错。

switch 语句接以下顺序执行:
1、控制表达式求值
2、如果 case 标签后的常量表达式符合控制语句所求出的值,内含语句被执行。
3、如果没有常量表达式符合控制语句,在default 标签内的内含语句被执行。
4、如果没有一个符合case 标签,且没有default 标签,控制转向switch 语段的结束端。

在继续更详细地探讨switch语句之前,请看清单 6.2 ,它演示用 switch语句来显示一个月的天数(忽略跨年度)
清单 6.2 使用switch语句显示一个月的天数 



1: using System;
2: 
3: class FallThrough
4: {
5: public static void Main(string[] args)
6: {
7: if (args.Length != 1) return;
8: 
9: int nMonth = Int32.Parse(args[0]);
10: if (nMonth < 1 &brvbar;&brvbar; nMonth > 12) return;
11: int nDays = 0;
12: 
13: switch (nMonth)
14: {
15: case 2: nDays = 28; break;
16: case 4:
17: case 6:
18: case 9:
19: case 11: nDays = 30; break;
20: default: nDays = 31;
21: }
22: Console.WriteLine("{0} days in this month",nDays);
23: }
24: } 



switch 语段包含于第13~21行。对于C程序员,这看起来非常相似,因为它不使用break语句。因此,存在着一个更具生命力的重要差别。你必须加上一个break语句(或一个不同的跳转语句),因为编译器会提醒,不允许直达下一部分。

何谓直达?在C(和C++)中,忽略break并且按以下编写代码是完全合法的:



nVar = 1
switch (nVar)
{
case 1:
DoSomething();
case 2:
DoMore();



在这个例子中,在执行了第一个case语句的代码后,将直接执行到其它case标签的代码,直到一个break语句退出switch语段为止。尽管有时这是一个强大的功能,但它更经常地产生难于发现的缺陷。

可如果你想执行其它case标签的代码,那怎么办? 有一种办法,它显示于清单6.3中。 

清单 6.3 在swtich语句中使用 goto 标签 和 goto default 



1: using System;
2: 
3: class SwitchApp
4: {
5: public static void Main()
6: {
7: Random objRandom = new Random();
8: double dRndNumber = objRandom.NextDouble();
9: int nRndNumber = (int)(dRndNumber * 10.0);
10: 
11: switch (nRndNumber)
12: {
13: case 1:
14: //什么也不做
15: break;
16: case 2:
17: goto case 3;
18: case 3:
19: Console.WriteLine("Handler for 2 and 3");
20: break;
21: case 4:
22: goto default;
23: // everything beyond a goto will be warned as
24: // unreachable code
25: default:
26: Console.WriteLine("Random number {0}", nRndNumber);
27: }
28: }
29: } 


在这个例子中,通过Random类产生用于控制表达式的值(第7~9行)。switch语段包含两个对switch语句有效的跳转语句。

goto case  标签:跳转到所说明的标签

goto default: 跳转到 default 标签

有了这两个跳转语句,你可以创建同C一样的功能,但是,直达不再是自动的。你必须明确地请求它。
不再使用直达功能的更深的含义为:你可任意排列标签,如把default标签放在其它所有标签的前面。为了说明它,我创建了一个例子,故意不结束循环: 



switch (nSomething)
{
default:
case 5:
goto default;



我已经保留了其中一个swich 语句功能的讨论直至结束——事实上你可以使用字符串作为常量表达式。这对于VB程序员,可能听起来不象是什么大的新闻,但来自C或C++的程序员将会喜欢这个新功能。

现在,一个 switch 语句可以如以下所示检查字符串常量了。 



string strTest = "Chris";
switch (strTest)
{
case "Chris":
Console.WriteLine("Hello Chris!");
break;
}



6.2 循环语句

当你想重复执行某些语句或语段时,依据当前不同的任务,C#提供4个不同的循环语句选择给你使用:
。for 语句 
。foreach 语句 
。while 语句 
。do 语句 

6.2.1 for 语句

当你预先知道一个内含语句应要执行多少次时,for 语句特别有用。当条件为真时,常规语法允许重复地执行内含语句(和循环表达式):



for (初始化;条件;循环) 内含语句


请注意,初始化、条件和循环都是可选的。如果忽略了条件,你就可以产生一个死循环,要用到跳转语句(break 或goto)才能退出。 



for (;;)
{
break; // 由于某些原因




另外一个重点是,你可以同时加入多条由逗号隔开的语句到for循环的所有三个参数。例如,你可以初始化两个变量、拥有三个条件语句,并重复4个变量。
作为C或C++程序员,你必须了解仅有的一个变化:条件语句必须为布尔表达式,就象 if 语句一样。

清单6.4 包含使用 for 语句的一个例子。它显示了如何计算一个阶乘,比使用递归函数调用还要快。 

清单 6.4 在for 循环里计算一个阶乘 



1: using System;
2: 
3: class Factorial
4: {
5: public static void Main(string[] args)
6: {
7: long nFactorial = 1;
8: long nComputeTo = Int64.Parse(args[0]);
9: 
10: long nCurDig = 1;
11: for (nCurDig=1;nCurDig <= nComputeTo; nCurDig++)
12: nFactorial *= nCurDig;
13: 
14: Console.WriteLine("{0}! is {1}",nComputeTo, nFactorial);
15: }
16: } 


尽管该例子过于拖沓,但它作为如何使用for 语句的一个开端。
首先,我本应在初始化内部声明变量nCurDig:
for (long nCurDig=1;nCurDig <= nComputeTo; nCurDig++) nFactorial *= nCurDig;

另一种忽略初始化的选择如下行,因为第10行在for 语句的外部初始化了变量。(记住C#需要初始化变量):
for (;nCurDig <= nComputeTo; nCurDig++) nFactorial *= nCurDig;

另一种改变是把++操作符移到内含语句中:
for ( ;nCurDig <= nComputeTo; ) nFactorial *= nCurDig++;

如果我也想摆脱条件语句,全部要做的是增加一条if 语句,用break 语句中止循环: 



for (;;)
{
if (nCurDig > nComputeTo) break;
nFactorial *= nCurDig++;



除了用于退出for语句的break语句外,你还可以用continue 跳过当前循环,并继续下一次循环。



for (;nCurDig <= nComputeTo;)
{
if (5 == nCurDig) continue; // 这行跳过了余下的代码
nFactorial *= nCurDig++; 





6.2.2 foreach 语句

已经在Visual Basic 语言中存在了很久的一个功能是,通过使用For Each 语句收集枚举。C#通过foreach 语句,也有一个用来收集枚举的命令:



foreach(表达式中的类型标识符) 内含语句


循环变量由类型和标识符声明,且表达式与收集相对应。循环变量代表循环正在为之运行的收集元素。 

你应该知道不能赋一个新值给循环变量,也不能把它当作ref 或out 参数。这样引用在内含语句中被执行的代码。 

你如何说出某些类支持foreach 语句? 简而言之,类必须支持具有 GetEnumerator()名字的方法,而且由其所返回的结构、类或者接口必须具有public 方法MoveNext() 和public 属性Current。如果你想知道更多,请阅读语言参考手册,它有很多关于这个话题的详细内容。 

对于清单 6.5 中的例子,我恰好偶然选了一个类,实现了所有这些需要。我用它来列举被定义过的所有的环境变量。 

清单 6.5 读所有的环境变量 



1: using System;
2: using System.Collections;
3: 
4: class EnvironmentDumpApp
5: {
6: public static void Main()
7: {
8: IDictionary envvars = Environment.GetEnvironmentVariables();
9: Console.WriteLine("There are {0} environment variables declared", envvars.Keys.Count);
10: foreach (String strKey in envvars.Keys)
11: {
12: Console.WriteLine("{0} = {1}",strKey, envvars[strKey].ToString());
13: }
14: }
15: }


对GetEnvironmentVariables的调用返回一个IDictionary类型接口,它是由.NET框架中的许多类实现了的字典接口。通过 IDictionary 接口,可以访问两个收集:Keys 和 Values。在这个例子里,我在foreach语句中使用Keys,接着查找基于当前key值的值(第12行)。

当使用foreach时,只要注意一个问题:当确定循环变量的类型时,应该格外小心。选择错误的类型并没有受到编译器的检测,但它会在运行时受检测,且会引发一个异常。 

6.2.3 while 语句

当你想执行一个内含语句0次或更多次时,while语句正是你所盼望的: 



while (条件) 内含语句 


条件语句——它也是一个布尔表达式 ——控制内含语句被执行的次数。你可以使用 break 和continue语句来控制while语句中的执行语句,它的运行方式同在for语句中的完全相同。

为了举例while的用法,清单 6.6 说明如何使用一个 StreamReader类输出C#源文件到屏幕。 

清单 6.6 显示一个文件的内容 



1: using System;
2: using System.IO;
3: 
4: class WhileDemoApp 
5: {
6: public static void Main()
7: {
8: StreamReader sr = File.OpenText ("whilesample.cs");
9: String strLine = null;
10: 
11: while (null != (strLine = sr.ReadLine()))
12: {
13: Console.WriteLine(strLine);
14: }
15: 
16: sr.Close();
17: }
18: } 


代码打开文件 whilesample.cs, 接着当ReadLine 方法返回一个不等于null的值时,就在屏幕上显示所读取的值。注意,我在while条件语句中用到一个赋值。如果有更多的用&&和&brvbar;&brvbar;连接起来的条件语句,我不能保证它们是否会被执行,因为存在着“短路”的可能。 

6.2.4 do 语句

C#最后可利用的循环语句是do语句。它与while语句十分相似,仅当经过最初的循环之后,条件才被验证。 



do
{
内含语句
}
while (条件); 


do语句保证内含语句至少被执行过一次,而且只要条件求值等于真,它们继续被执行。通过使用break语句,你可以迫使运行退出 do 语块。如果你想跳过这一次循环,使用continue语句。

一个如何使用do语句的例子显示在清单 6.7中。它向用户请求一个或多个数字,并且当执行程序退出do循环后计算平均值。 

清单 6.7 在do 循环中计算平均值 



1: using System;
2: 
3: class ComputeAverageApp
4: {
5: public static void Main()
6: {
7: ComputeAverageApp theApp = new ComputeAverageApp();
8: theApp.Run();
9: }
10: 
11: public void Run()
12: {
13: double dValue = 0;
14: double dSum = 0;
15: int nNoOfValues = 0;
16: char chContinue = ’y';
17: string strInput;
18: 
19: do
20: {
21: Console.Write("Enter a value: ");
22: strInput = Console.ReadLine();
23: dValue = Double.Parse(strInput);
24: dSum += dValue;
25: nNoOfValues++;
26: Console.Write("Read another value?");
27: 
28: strInput = Console.ReadLine();
29: chContinue = Char.FromString(strInput);
30: }
31: while (‘y’ == chContinue);
32: 
33: Console.WriteLine("The average is {0}",dSum / nNoOfValues);
34: }
35: } 


在这个例子里,我在静态 Main函数中实例化 ComputeAverageApp类型的一个对象。它同样接着调用实例的Run方法,该方法包含了计算平均值所有必要的功能。

do 循环跨越第19~31行。条件是这样设定的:分别回答各个问题 “y”,以决定是否要增加另一个值。输入任何其它字符会引起程序退出 do语块,且平均值被计算。

正如你可以从提到的例子看出,do语句和while语句差别不太大——仅有的差别就是条件在什么时候被求值。 

6.3 小结

这章解释了如何使用C#中用到的各种选择和循环语句。 if 语句在应用程序中可能是最为常用的语句。当在布尔表达式中使用计算时,编译器会为你留意。但是,你一定要确保条件语句的短路不会阻止必要代码的运行。
switch 语句——尽管同样与C语言的相应部分相似——但也被改善了。直达不再被支持,而且你可以使用字符串标签,对于C程序员,这是一种新的用法。

在这一章的最后部分,我说明如何使用for、foreach、while和do语句。语句完成各种需要,包括执行固定次数的循环、列举收集元素和执行基于某些条件的任意次数的语句。

第五章 类

前一章讨论了数据类型和它们的用法。现在我们转移到C#中至关重要的结构——类。没有了类,就连简单的C#程序都不能编译。这一章假定你知道了一个类的基本组成部分:方法、属性、构造函数和析构函数。C#在其中增加了索引和事件。

在这一章中,你学到下列有关类的话题。
。使用构造函数和析构函数
。给类写方法
。给一个类增加属性存取标志
。实现索引
。创建事件并通过代表元为事件关联客户
。应用类、成员和存取修饰符。 

5.1 构造函数和析构函数

在你可以访问一个类的方法、属性或任何其它东西之前, 第一条执行的语句是包含有相应类的构造函数。甚至你自己不写一个构造函数,也会有一个缺省的构造函数提供给你。 



class TestClass
{
public TestClass(): base() {} // 由编译器提供



一个构造函数总是和它的类名相同,但是,它没有声明返回类型。总之,构造函数总是public的,你可以用它们来初始化变量。 



public TestClass()
{
// 在这给变量
// 初始化代码等等。



如果类仅包含静态成员(能以类型调用,而不是以实例调用的成员),你可以创建一个private的构造函数。



private TestClass() 
{
}


尽管存取修饰符在这一章的后面将要大篇幅地讨论,但是private意味着从类的外面不可能访问该构造函数。所以,它不能被调用,且没有对象可以自该类定义被实例化。

并不仅限于无参数构造函数——你可以传递初始参数来初始化成员。

public TestClass(string strName, int nAge) { … } 

作为一个C/C++程序员,你可能习惯于给初始化写一个附加的方法,因为在构造函数中没有返回值。当然,尽管在C#中也没有返回值,但你可以引发一个自制的异常,以从构造函数获得返回值。更多有关异常处理的知识在第七章 "异常处理"中有讨论。但是,当你保留引用给宝贵的资源,应该想到写一个方法来解决:一个可以被显式地调用来释放这些资源。问题是当你可以在析构函数(以类名的前面加"~"的方式命名)中做同样的事情时,为何还要写一个附加的方法.



public ~TestClass()
{
// 清除



你应该写一个附加方法的原因是垃圾收集器,它在变量超出范围后并不会立即被调用,而仅当间歇期间或内存条件满足时才被触发。当你锁住资源的时间长于你所计划的时间时,它就会发生。 

因此,提供一个显式的释放方式是一个好主意,它同样能从析构函数中调用。 



public void Release()
{
// 释放所有宝贵的资源


public ~TestClass()
{
Release();



调用析构函数中的释放方法并不是必要的——总之,垃圾收集会留意释放对象。但没有忘记清除是一种良好的习惯。 

5.2 方法

既然对象能正确地初始化和结束,所剩下来的就是往类中增加功能。在大多数情况下,功能的主要部分在方法中能得到实现。你早已见过静态方法的使用,但是,这些是类型(类)的部分,不是实例(对象)。

为了让你迅速入门,我把这些方法的烦琐问题安排为三节:
。方法参数
。改写方法
。方法屏蔽

5.2.1 方法参数

因方法要处理更改数值,你多多少少要传递值给方法,并从方法获得返回值。以下三个部分涉及到由传递值和为调用者获取返回结果所引起的问题。 

。输入参数
。引用参数
。输出参数 

5.2.1.1 输入参数

你早已在例子中见过的一个参数就是输入参数。你用一个输入参数通过值传递一个变量给一个方法——方法的变量被调用者传递进来的值的一个拷贝初始化。清单5.1 示范输入参数的使用。 

清单 5.1 通过值传递参数 



1: using System;
2: 
3: public class SquareSample
4: {
5: public int CalcSquare(int nSideLength)
6: {
7: return nSideLength*nSideLength;
8: }
9: }
10: 
11: class SquareApp
12: {
13: public static void Main()
14: {
15: SquareSample sq = new SquareSample();
16: Console.WriteLine(sq.CalcSquare(25).ToString());
17: }
18: } 


因为我传递值而不是引用给一个变量,所以当调用方法时(见第16行),可以使用一个常量表达式(25)。整型结果被传回给调用者作为返回值,它没有存到中间变量就被立即显示到屏幕上 。

输入参数按C/C++程序员早已习惯的工作方式工作。如果你来自VB,请注意没有能被编译器处理的隐式ByVal或ByRef——如果没有设定,参数总是用值传递。

这点似乎与我前面所陈述的有冲突:对于一些变量类型,用值传递实际上意味着用引用传递。 

迷惑吗? 一点背景知识也不需要:COM中的东西就是接口,每一个类可以拥有一个或多个接口。一个接口只不过是一组函数指针,它不包含数据。重复该数组会浪费很多内存资源;所以,仅开始地址被拷贝给方法,它作为调用者,仍然指向接口的相同指针。那就是为什么对象用值传递一个引用。 

5.2.1.2 引用参数

尽管可以利用输入参数和返回值建立很多方法,但你一想到要传递值并原地修改它(也就是在相同的内存位置),就没有那么好运了。这里用引用参数就很方便。


void myMethod(ref int nInOut)


因为你传递了一个变量给该方法(不仅仅是它的值),变量必须被初始化。否则,编译器会报警。 

清单 5.2 显示如何用一个引用参数建立一个方法。 

清单 5.2 通过引用传递参数 



1: // class SquareSample
2: using System;
3: 
4: public class SquareSample
5: {
6: public void CalcSquare(ref int nOne4All)
7: {
8: nOne4All *= nOne4All;
9: }
10: }
11: 
12: class SquareApp
13: {
14: public static void Main()
15: {
16: SquareSample sq = new SquareSample();
17: 
18: int nSquaredRef = 20; // 一定要初始化
19: sq.CalcSquare(ref nSquaredRef);
20: Console.WriteLine(nSquaredRef.ToString());
21: }
22: } 


正如所看到的,所有你要做的就是给定义和调用都加上ref限定符。因为变量通过引用传递,你可以用它来计算出结果并传回该结果。但是,在现实的应用程序中,我强烈建议要用两个变量,一个输入参数和一个引用参数。 

5.2.1.3 输出参数

传递参数的第三种选择就是把它设作一个输出参数。正如该名字所暗示,一个输出参数仅用于从方法传递回一个结果。它和引用参数的另一个区别在于:调用者不必先初始化变量才调用方法。这显示在清单5.3中。 

清单 5.3 定义一个输出参数 



1: using System;
2: 
3: public class SquareSample
4: {
5: public void CalcSquare(int nSideLength, out int nSquared)
6: {
7: nSquared = nSideLength * nSideLength;
8: }
9: }
10: 
11: class SquareApp
12: {
13: public static void Main()
14: {
15: SquareSample sq = new SquareSample();
16: 
17: int nSquared; // 不必初始化
18: sq.CalcSquare(15, out nSquared);
19: Console.WriteLine(nSquared.ToString());
20: }
21: } 



5.2.2 改写方法

面向对象设计的重要原则就是多态性。不要理会高深的理论,多态性意味着:当基类程序员已设计好用于改写的方法时,在派生类中,你就可以重定义(改写)基类的方法。基类程序员可以用virtual 关键字设计方法:


virtual void CanBOverridden()


当从基类派生时,所有你要做的就是在新方法中加入override关键字:


override void CanBOverridden()


当改写一个基类的方法时,你必须明白,不能改变方法的访问属性——在这章的后面,你会学到更多关于访问修饰符的知识。

除了改写基类方法的事实外,还有另一个甚至更重要的改写特性。当把派生类强制转换成基类类型并接着调用虚拟方法时,被调用的是派生类的方法而不是基类的方法。


((BaseClass)DerivedClassInstance).CanBOverridden();


为了演示虚拟方法的概念,清单 5.4 显示如何创建一个三角形基类,它拥有一个可以被改写的成员方法(ComputeArea)。 

清单 5.4 改写一个基类的方法 



1: using System;
2: 
3: class Triangle
4: {
5: public virtual double ComputeArea(int a, int b, int c)
6: {
7: // Heronian formula
8: double s = (a + b + c) / 2.0;
9: double dArea = Math.Sqrt(s*(s-a)*(s-b)*(s-c));
10: return dArea;
11: }
12: }
13: 
14: class RightAngledTriangle:Triangle
15: {
16: public override double ComputeArea(int a, int b, int c)
17: { 
18: double dArea = a*b/2.0;
19: return dArea;
20: }
21: }
22: 
23: class TriangleTestApp
24: {
25: public static void Main()
26: {
27: Triangle tri = new Triangle();
28: Console.WriteLine(tri.ComputeArea(2, 5, 6));
29: 
30: RightAngledTriangle rat = new RightAngledTriangle();
31: Console.WriteLine(rat.ComputeArea(3, 4, 5));
32: }
33: } 



基类Triangle定义了方法ComputeArea。它采用三个参数,返回一个double结果,且具有公共访问性。从Triangle类派生出的是RightAngledTriangle,它改写了ComputeArea 方法,并实现了自己的面积计算公式。两个类都被实例化,且在命名为TriangleTestApp的应用类的Main() 方法中得到验证。

我漏了解释第14行:
class RightAngledTriangle : Triangle
在类语句中冒号(:)表示RightAngledTriangle从类 Triangle派生。那就是你所必须要做的,以让C#知道你想把 Triangle当作RightAngledTriangle的基类。当仔细观察直角三角形的ComputeArea方法时,你会发现第3个参数并没有用于计算。但是,利用该参数就可以验证是否是“直角”。如清单5.5所示。 

清单 5.5 调用基类实现 



1: class RightAngledTriangle:Triangle
2: {
3: public override double ComputeArea(int a, int b, int c)
4: {
5: const double dEpsilon = 0.0001;
6: double dArea = 0;
7: if (Math.Abs((a*a + b*b - c*c)) > dEpsilon)
8: {
9: dArea = base.ComputeArea(a,b,c);
10: }
11: else
12: {
13: dArea = a*b/2.0;
14: }
15: 
16: return dArea;
17: }
18: } 



该检测简单地利用了毕达哥拉斯公式,对于直角三角形,检测结果必须为0。如果结果不为0,类就调用它基类的 ComputeArea来实现。


dArea = base.ComputeArea(a,b,c);


例子的要点为:通过显式地利用基类的资格检查,你就能轻而易举地调用基类实现改写方法。
当你需要实现其在基类中的功能,而不愿意在改写方法中重复它时,这就非常有帮助。 

5.2.3 方法屏蔽

重定义方法的一个不同手段就是要屏蔽基类的方法。当从别人提供的类派生类时,这个功能特别有价值。看清单 5.6,假设BaseClass由其他人所写,而你从它派生出 DerivedClass 。 

清单 5.6 Derived Class 实现一个没有包含于 Base Class中的方法 



1: using System;
2: 
3: class BaseClass
4: {
5: }
6: 
7: class DerivedClass:BaseClass
8: {
9: public void TestMethod()
10: {
11: Console.WriteLine("DerivedClass::TestMethod");
12: }
13: }
14: 
15: class TestApp
16: {
17: public static void Main()
18: {
19: DerivedClass test = new DerivedClass();
20: test.TestMethod();
21: }
22: } 



在这个例子中, DerivedClass 通过TestMethod()实现了一个额外的功能。但是,如果基类的开发者认为把TestMethod()放在基类中是个好主意,并使用相同的名字实现它时,会出现什么问题呢?(见清单5.7) 

清单 5.7 Base Class 实现和 Derived Class相同的方法 



1: class BaseClass
2: {
3: public void TestMethod()
4: {
5: Console.WriteLine("BaseClass::TestMethod");
6: }
7: }
8: 
9: class DerivedClass:BaseClass
10: {
11: public void TestMethod()
12: {
13: Console.WriteLine("DerivedClass::TestMethod");
14: }
15: } 


在优秀的编程语言中,你现在会遇到一个真正的大麻烦。但是,C#会给你提出警告:
hiding2.cs(13,14): warning CS0114: ’DerivedClass.TestMethod()’ hides inherited member 

‘BaseClass.TestMethod()’. To make the current method override that implementation, add 

the override keyword. Otherwise add the new keyword.
(hiding2.cs(13,14):警告 CS0114:’DerivedClass.TestMethod()’ 屏蔽了所继承的成员 

‘BaseClass.TestMethod()’。要想使当前方法改写原来的实现,加上 override关键字。否则加上新的关键字。具有了修饰符new,你就可以告诉编译器,不必重写派生类或改变使用到派生类的代码,你的方法就能屏蔽新加入的基类方法。清单5.8 显示如何在例子中运用new修饰符。 

清单 5.8 屏蔽基类方法 



1: class BaseClass
2: {
3: public void TestMethod()
4: {
5: Console.WriteLine("BaseClass::TestMethod");
6: }
7: }
8: 
9: class DerivedClass:BaseClass
10: {
11: new public void TestMethod()
12: {
13: Console.WriteLine("DerivedClass::TestMethod");
14: }
15: } 


使用了附加的new修饰符,编译器就知道你重定义了基类的方法,它应该屏蔽基类方法。但是,如果你按以下方式编写:



DerivedClass test = new DerivedClass();
((BaseClass)test).TestMethod();


基类方法的实现就被调用了。这种行为不同于改写方法,后者保证大部分派生方法获得调用。

5.3 类属性

有两种途径揭示类的命名属性——通过域成员或者通过属性。前者是作为具有公共访问性的成员变量而被实现的;后者并不直接回应存储位置,只是通过 存取标志(accessors)被访问。

当你想读出或写入属性的值时,存取标志限定了被实现的语句。用于读出属性的值的存取标志记为关键字get,而要修改属性的值的读写符标志记为set。在你对该理论一知半解以前,请看一下清单5.9中的例子,属性SquareFeet被标上了get和set的存取标志。

清单 5.9 实现属性存取标志 



1: using System;
2: 
3: public class House
4: {
5: private int m_nSqFeet;
6: 
7: public int SquareFeet
8: {
9: get { return m_nSqFeet; }
10: set { m_nSqFeet = value; }
11: }
12: }
13: 
14: class TestApp
15: {
16: public static void Main()
17: {
18: House myHouse = new House();
19: myHouse.SquareFeet = 250;
20: Console.WriteLine(myHouse.SquareFeet);
21: }
22: } 



House类有一个命名为SquareFeet的属性,它可以被读和写。实际的值存储在一个可以从类内部访问的变量中——如果你想当作一个域成员重写它,你所要做的就是忽略存取标志而把变量重新定义为:
public int SquareFeet;

对于一个如此简单的变量,这样不错。但是,如果你想要隐藏类内部存储结构的细节时,就应该采用存取标志。在这种情况下,set 存取标志给值参数中的属性传递新值。(可以改名,见第10行。)

除了能够隐藏实现细节外,你还可自由地限定各种操作:
get和set:允许对属性进行读写访问。
get only:只允许读属性的值。
set only:只允许写属性的值。

除此之外,你可以获得实现在set标志中有效代码的机会。例如,由于种种原因(或根本没有原因),你就能够拒绝一个新值。最好是没有人告诉你它是一个动态属性——当你第一次请求它后,它会保存下来,故要尽可能地推迟资源分配。 

5.4 索引

你想过象访问数组那样使用索引访问类吗 ?使用C#的索引功能,对它的期待便可了结。 

语法基本上象这样:
属性 修饰符 声明 { 声明内容} 

具体的例子为



public string this[int nIndex]
{
get { … }
set { … }




索引返回或按给出的index设置字符串。它没有属性,但使用了public修饰符。声明部分由类型string和this 组成用于表示类的索引。get和set的执行规则和属性的规则相同。(你不能取消其中一个。) 只存在一个差别,那就是:你几乎可以任意定义大括弧中的参数。限制为,必须至少规定一个参数,允许ref 和out 修饰符。

this关键字确保一个解释。索引没有用户定义的名字,this 表示默认接口的索引。如果类实现了多个接口,你可以增加更多个由InterfaceName.this说明的索引。 

为了演示一个索引的使用,我创建了一个小型的类,它能够解析一个主机名为IP地址——或一个IP地址列表(以http://www.microsoft.com为例  )。这个列表通过索引可以访问,你可以看一下清单 

5.10 的具体实现。 

清单 5.10 通过一个索引获取一个IP地址 



1: using System;
2: using System.Net;
3: 
4: class ResolveDNS
5: {
6: IPAddress[] m_arrIPs;
7: 
8: public void Resolve(string strHost)
9: {
10: IPHostEntry iphe = DNS.GetHostByName(strHost);
11: m_arrIPs = iphe.AddressList;
12: }
13: 
14: public IPAddress this[int nIndex]
15: {
16: get
17: {
18: return m_arrIPs[nIndex];
19: }
20: }
21: 
22: public int Count
23: {
24: get { return m_arrIPs.Length; }
25: }
26: }
27: 
28: class DNSResolverApp
29: {
30: public static void Main()
31: {
32: ResolveDNS myDNSResolver = new ResolveDNS();
33: myDNSResolver.Resolve("http://www.microsoft.com");
34: 
35: int nCount = myDNSResolver.Count;
36: Console.WriteLine("Found {0} IP’s for hostname", nCount);
37: for (int i=0; i < nCount; i++)
38: Console.WriteLine(myDNSResolver);
39: } 
40: } 



为了解析主机名,我用到了DNS类,它是System .Net 名字空间的一部分。但是,由于这个名字空间并不包含在核心库中,所以必须在编译命令行中引用该库:
csc /r:System.Net.dll /out:resolver.exe dnsresolve.cs

解析代码是向前解析的。在该 Resolve方法中,代码调用DNS类的静态方法GetHostByName,它返回一个IPHostEntry对象。结果,该对象包含有我要找的数组——AddressList数组。在退出Resolve 方法之前,在局部的对象实例成员m_arrIPs中,存储了一个AddressList array的拷贝(类型IPAddress 的对象存储在其中)。

具有现在生成的数组 ,通过使用在类ResolveDNS中求得的索引,应用程序代码就可以在第37至38行列举出IP地址。(在第6章 "控制语句",有更多有关语句的信息。) 因为没有办法更改IP地址,所以仅给索引使用了get存取标志。为了简单其见,我忽略了数组的边界溢出检查。

5.4 事件

当你写一个类时,有时有必要让类的客户知道一些已经发生的事件。如果你是一个具有多年编程经验的程序员,似乎有很多的解决办法,包括用于回调的函数指针和用于ActiveX控件的事件接收(event sinks)。现在你将要学到另外一种把客户代码关联到类通知的办法——使用事件。

事件既可以被声明为类域成员(成员变量),也可以被声明为属性。两者的共性为,事件的类型必定是代表元,而函数指针原形和C#的代表元具有相同的含义。

每一个事件都可以被0或更多的客户占用,且客户可以随时关联或取消事件。你可以以静态或者以实例方法定义代表元,而后者很受C++程序员的欢迎。

既然我已经提到了事件的所有功能及相应的代表元,请看清单5.11中的例子。它生动地体现了该理论。 

清单5.11 在类中实现事件处理



1: using System;
2: 
3: // 向前声明
4: public delegate void EventHandler(string strText);
5: 
6: class EventSource
7: {
8: public event EventHandler TextOut;
9: 
10: public void TriggerEvent()
11: {
12: if (null != TextOut) TextOut("Event triggered");
13: }
14: }
15: 
16: class TestApp
17: {
18: public static void Main()
19: {
20: EventSource evsrc = new EventSource();
21: 
22: evsrc.TextOut += new EventHandler(CatchEvent);
23: evsrc.TriggerEvent();
24: 
25: evsrc.TextOut -= new EventHandler(CatchEvent);
26: evsrc.TriggerEvent();
27: 
28: TestApp theApp = new TestApp();
29: evsrc.TextOut += new EventHandler(theApp.InstanceCatch);
30: evsrc.TriggerEvent();
31: }
32: 
33: public static void CatchEvent(string strText)
34: {
35: Console.WriteLine(strText);
36: }
37: 
38: public void InstanceCatch(string strText)
39: {
40: Console.WriteLine("Instance " + strText);
41: }
42: } 



第4行声明了代表元(事件方法原形),它用来给第8行中的EventSource类声明TextOut事件域成员。你可以观察到代表元作为一种新的类型声明,当声明事件时可以使用代表元。

该类仅有一个方法,它允许我们触发事件。请注意,你必须进行事件域成员不为null的检测,因为可能会出现没有客户对事件感兴趣这种情况。

TestApp类包含了Main 方法,也包含了另外两个方法,它们都具备事件所必需的信号。其中一个方法是静态的,而另一个是实例方法。

EventSource 被实例化,而静态方法CatchEvent被预关联上了 TextOut事件:
evsrc.TextOut += new EventHandler(CatchEvent);

从现在起,当事件被触发时,该方法被调用。如果你对事件不再感兴趣,简单地取消关联:
evsrc.TextOut -= new EventHandler(CatchEvent);

注意,你不能随意取消关联的处理函数——在类代码中仅创建了这些处理函数。为了证明事件处理函数也和实例方法一起工作,余下的代码建立了TestApp 的实例,并钩住事件处理方法。
事件在哪方面对你特别有用?你将经常在ASP+中或使用到WFC (Windows Foundation Classes)时,涉及到事件和代表元。 

5.5 应用修饰符

在这一章的学习过程中,你已经见过了象public、virtual等修饰符。欲以一种易于理解的方法概括它们,我把它们划分为三节: 

。类修饰符
。成员修饰符 
。存取修饰符 

5.5.1 类修饰符

到目前为止,我还没有涉及到类修饰符,而只涉及到了应用于类的存取修饰符。但是,有两个修饰符你可以用于类:

abstract——关于抽象类的重要一点就是它不能被实例化。只有不是抽象的派生类才能被实例化。派生类必须实现抽象基类的所有抽象成员。你不能给抽象类使用sealed 修饰符。

sealed——密封 类不能被继承。使用该修饰符防止意外的继承,在.NET框架中的类用到这个修饰符。
要见到两个修饰符的运用,看看清单5.12 ,它创建了一个基于一个抽象类的密封类(肯定是一个十分极端的例子)。 

清单 5.12 抽象类和密封类 



1: using System;
2: 
3: abstract class AbstractClass
4: {
5: abstract public void MyMethod();
6: }
7: 
8: sealed class DerivedClass:AbstractClass
9: {
10: public override void MyMethod()
11: {
12: Console.WriteLine("sealed class");
13: }
14: }
15: 
16: public class TestApp
17: {
18: public static void Main()
19: {
20: DerivedClass dc = new DerivedClass();
21: dc.MyMethod();
22: }
23: } 



5.5.2 成员修饰符

与有用的成员修饰符的数量相比,类修饰符的数量很少。我已经提到了一些,这本书即将出现的例子描述了其它的成员修饰符。

以下是有用的成员修饰符:

abstract——说明一个方法或存取标志不能含有一个实现。它们都是隐式虚拟,且在继承类中,你必须提供 override关键字。
const——这个修饰符应用于域成员或局部变量。在编译时常量表达式被求值,所以,它不能包含变量的引用。
event ——定义一个域成员或属性作为类型事件。用于捆绑客户代码到类的事件。
extern——告诉编译器方法实际上由外部实现。第10章 “和非受管代码互相操作” 将全面地涉及到外部代码。
override——用于改写任何基类中被定义为virtual的方法和存取标志。要改写的名字和基类的方法必须一致。
readonly——一个使用 readonly修饰符的域成员只能在它的声明或者在包含它的类的构造函数中被更改。 
static——被声明为static的成员属于类,而不属于类的实例。你可以用static 于域成员、方法、属性、操作符甚至构造函数。
virtual——说明方法或存取标志可以被继承类改写。 

5.5.3 存取修饰符

存取修饰符定义了某些代码对类成员(如方法和属性)的存取等级。你必须给每个成员加上所希望的存取修饰符,否则,默认的存取类型是隐含的。

你可以应用4个 存取修饰符之一:
public——任何地方都可以访问该成员,这是具有最少限制的存取修饰符。
protected——在类及所有的派生类中可以访问该成员,不允许外部访问。
private——仅仅在同一个类的内部才能访问该成员。甚至派生类都不能访问它。
internal——允许相同组件(应用程序或库)的所有代码访问。在.NET组件级别,你可以把它视为public,而在外部则为private。

为了演示存取修饰符的用法,我稍微修改了Triangle例子,使它包含了新增的域成员和一个新的派生类(见清单 5.13)。 

清单 5.13 在类中使用存取修饰符 



1: using System;
2: 
3: internal class Triangle
4: {
5: protected int m_a, m_b, m_c;
6: public Triangle(int a, int b, int c)
7: {
8: m_a = a;
9: m_b = b;
10: m_c = c;
11: }
12: 
13: public virtual double Area()
14: {
15: // Heronian formula
16: double s = (m_a + m_b + m_c) / 2.0;
17: double dArea = Math.Sqrt(s*(s-m_a)*(s-m_b)*(s-m_c));
18: return dArea;
19: }
20: }
21: 
22: internal class Prism:Triangle
23: {
24: private int m_h;
25: public Prism(int a, int b, int c, int h):base(a,b,c)
26: {
27: m_h = h;
28: }
29: 
30: public override double Area()
31: {
32: double dArea = base.Area() * 2.0;
33: dArea += m_a*m_h + m_b*m_h + m_c*m_h;
34: return dArea;
35: }
36: }
37: 
38: class PrismApp
39: {
40: public static void Main()
41: {
42: Prism prism = new Prism(2,5,6,1);
43: Console.WriteLine(prism.Area());
44: }
45: } 



Triangle 类和 Prism 类现在被标为 internal。这意味着它们只能在当前组件中被访问。 

请记住“.NET组件”这个术语指的是包装( packaging,),而不是你可能在COM+中用到的组件。 

Triangle 类有三个 protected成员,它们在构造函数中被初始化,并用于面积计算的方法中。由于这些成员是protected 成员,所以我可以在派生类Prism中访问它们,在那里执行不同的面积计算。 

Prism自己新增了一个成员m_h,它是私有的——甚至派生类也不能访问它。
花些时间为每个类成员甚至每个类计划一种保护层次,通常是个好主意。当需要引入修改时,全面的计划最终会帮助你,因为没有程序员会愿意使用“没有文档”的类功能。

5.6 小结

这章显示了类的各种要素,它是运行实例(对象)的模板。在一个对象的生命期,首先被执行的代码是个构造函数。构造函数用来初始化变量,这些变量后来在方法中用于计算结果。

方法允许你传递值、引用给变量,或者只传送一个输出值。方法可以被改写以实现新的功能,或者你可以屏蔽基类成员,如果它实现了一个具有和派生类成员相同名字的方法。

命名属性可以被当作域成员(成员变量)或属性存取标志实现。后者是get和set存取标志,忽略一个或另外一个,你可以创建仅写或仅读属性。存取标志非常适合于确认赋给属性的值。

C#类的另外一个功能是索引,它使象数组语法一样访问类中值成为可能。还有,如果当类中的某些事情发生时,你想客户得到通知,要让它们与事件关联。

当垃圾收集器调用析构函数时,对象的生命就结束了。由于你不能准确地预测这种情况什么时候会发生,所以应该创建一个方法以释放这些宝贵的资源,当你停止使用它们时。