刘基诚 译 C# 精髓 Beijing ● Cambridge ● Farnham ● Koln" ● Paris ● Sebastopol ● Taipei ● Tokyo O'Reilly & Associates, Inc. 授权中国电力出版社出版 著Ben Albahari, Peter Drayton & Brad Merrill书名/C#精髓 书号/ ISBN 7-5083-0732-1 责任编辑 / 刘江 封面设计 / Ellie Volckhausen,张健 出版发行 / 中国电力出版社( www.infopower.com.cn) 地址/ 北京三里河路 6 号(邮政编码 100044) 经销/ 全国新华书店 印刷/ 北京市地矿印刷厂 开本/ 787 毫米× 1092 毫米 16 开本 15 印张 210 千字 版次/ 2001 年 8 月第一版 2001 年 8 月第一次印刷 印数/ 0001-5000 册 定价/ 29.00 元(册) 图书在版编目(CIP)数据 C# 精髓(美)阿尔巴哈里( Albahari, B.)等编著;刘基诚译 . - 北京:中国电力 出版社,2001 书名原文:C# Essentials ISBN 7-5083-0732-1 Ⅰ .C... Ⅱ. ①阿 ... ②刘 ... Ⅲ .C 语言-程序设计、Ⅳ .TP312 中国版本图书馆 CIP 数据核字(2001)第 060058 号 北京市版权局著作权合同登记 图字:01-2001-3292 号 ©2001 by O'Reilly & Associates, Inc. Simplified Chinese Edition, jointly published by O'Reilly & Associates, Inc. and China Electric Power Press, 2001. Authorized translation of the English edition, 2001 O'Reilly & Associates, Inc., the owner of all rights to publish and sell the same. All rights reserved including the rights of reproduction in whole or in part in any form. 英文原版由 O'Reilly & Associates, Inc. 出版 2001。 简体中文版由中国电力出版社出版 2001。英文原版的翻译得到 O'Reilly & Associates, Inc. 的授 权。此简体中文版的出版和销售得到出版权和销售权的所有者 —— O'Reilly & Associates, Inc.的 许可。 版权所有,未得书面许可,本书的任何部分和全部不得以任何形式重制。O'Reilly & Associates 公司介绍 为了满足读者对网络和软件技术知识的迫切需求,世界著名计算机图书出版机构 O'Reilly & Associates公司授权中国电力出版社,翻译出版一批该公司久负盛名的英 文经典技术专著。 O'Reilly & Associates 公司是世界上在 UNIX、X、Internet 和其他开放系统图 书领域具有领导地位的出版公司,同时是联机出版的先锋。 从最畅销的《The Whole Internet User's Guide & Catalog》(被纽约公共图书馆 评为二十世纪最重要的 50本书之一)到 GNN(最早的Internet 门户和商业网站),再 到 WebSite(第一个桌面 PC 的 Web 服务器软件),O'Reilly & Associates 一直处于 Internet 发展的最前沿。 许多书店的反馈表明,O'Reilly & Associates是最稳定的计算机图书出版商 —— 每一本书都一版再版。与大多数计算机图书出版商相比, O'Reilly & Associates公司 具有深厚的计算机专业背景,这使得 O'Reilly & Associates形成了一个非常不同于其 他出版商的出版方针。O'Reilly & Associates所有的编辑人员以前都是程序员,或者 是顶尖级的技术专家。O'Reilly & Associates 还有许多固定的作者群体 —— 他们本 身是相关领域的技术专家、咨询专家,而现在编写著作, O'Reilly & Associates 依靠 他们及时地推出图书。因为 O'Reilly & Associates紧密地与计算机业界联系着,所以 O'Reilly & Associates 知道市场上真正需要什么图书。译者序 微软发布C#(读如 C sharp)语言至今已经一年有余。按照微软发布的《C# Language Specification (0.26版)》开宗明义第一句话的说法:“C# 是一门从C 和 C++ 派生 而来的、简单的、面向对象的、类型安全的语言。 ” 这门新语言引起了开发人员的广泛关注,大家最关心的问题集中在:微软设计这门 语言的目的是什么?是否应该学习和应用这门语言? 这两个问题其实是息息相关的。在我看来, 学习C#的理由至少应该包括以下几条。 它是一门全新设计的语言 请注意,C# 是 2000 年 7 月份才公之于众的,考虑到微软的实力及其善于利用后发 优势取得竞争胜利的辉煌历史,C# 的设计水准和未来前景绝对不可小视。 事实上,C#的设计师正是久负盛名的Turbo Pascal和Delphi之父Anders Hejlsberg。 在设计过程中,Hejlsberg 借鉴了各种主流语言的得失,可以说是集各种优点于一 身(吸收了 Delphi和 VB等 RAD 语言的简单易用,C 和 C++ 的强大,以及 Java 的 类型安全等特性) ,同时又尽量避免了前人所犯的错误。正如 Hejlsberg 在访谈中 谈到的:“我们引入 C#,是为了给觉得 C++ 太复杂的C++ 程序员,和觉得 Java 缺 乏某些 C 和 C++ 特性的 Java 程序员提供另一种选择。 ” 同时作为新生事物,C# 完全从零开始,没有历史包袱,反映了当前软件开发从面 向对象到面向组件、从单独程序到 Internet 应用、从专有标准到开放标准( XML, SOAP)的最新趋势。 它是专门为 .NET 平台设计的 我们知道,.NET 平台是微软整个公司未来的希望所在,为此投入了绝大部分人力 和物力。而事实也证明, .NET 平台不愧是新世纪的创新产品。它的优秀甚至立即 在开源社区引起了强烈反响,作为回应, GNU启动了 DotGnu计划,GNOME 项目 的领导人Miguel de Icaza也为之倾倒,发起了旨在开发 Unix上 .NET实现的Mono 项目。.NET平台是包括微软各种操作系统(从WinCE到Windows ME再到Windows2000、Windows XP)、其上的 .NET 框架和企业服务器以及服务构件、最上层的 Visual Studio.NET 开发工具在内的庞大产品线,这种规模和实力也只有 Sun 的 JavaOne可以相提并论。.NET平台包含了微软这个软件巨人对软件未来的思考,融 入了软件服务化、组件化、 Internet 化、语言集成化、开发配置简易化、安全性可 靠性最大化等等最新理念。 而 C# 正是 .NET 平台的母语(native language)。虽然 .NET 也支持 Java 以外的各 种主要语言,但要开发 .NET 平台应用程序,C# 显然是第一选择。事实上, .NET 平台本身的许多重要应用(比如 ASP.NET)就是用 C# 编写的。 C# 除了本身的优秀之外,它的更多优点主要集中在与 .NET平台的天然集成性上。 因此,要真正理解 C#,必须学习 .NET 平台。 它是面向组件设计的 相信有过大型软件开发经验的读者都会领悟到组件的威力。面向组件是软件开发的 大势所趋。但是微软原有的组件技术( COM/COM+/DCOM)非常复杂,难于掌握, 以至许多专家(比如 Don Box)都要经历禅宗式的顿悟体验,就更不用说普通开发 者了。 而面向组件正是 C# 的主要设计目的之一。Anders Hejlsberg 对 C# 的宣传词就是: “C/C++家族中第一个面向组件的语言。 ” 因此,C#中加入了属性( property)、属 性信息( attribute)和装箱/ 拆箱( boxing/unboxing)等等组件开发所需要的概念。 尤其是属性和属性信息,成为 Anders Hejlsberg 的得意之作。同时, .NET 平台中 组件的部署也相当简单。 总之,用 C# 在 .NET平台上开发,大大降低了组件开发的门槛,消除了所谓“ DLL 噩梦”,普通的开发者也不用再理会什么 COM,GUID,HRESULT,AddRef, Release 了。 它是以 Internet 为中心的 在分布计算领域,微软原先的“线上协议” DCOM 以及 Sun 公司的解决方案 EJB 都是有状态的、紧密连接的,因此伸缩性很难令人满意。 C# 和 .NET 平台转向了 无状态的、松散连接的开放 Web 协议 —— XML 和 SOAP。这种选择是微软对自 身曾经忽视 Internet 的一种反思和修正,已经深深融入了整个平台的设计当中。To C#,or not to C#? 读者在做出自己的选择之前,必须了解清楚 C#的来龙去脉,同时考虑自身的具体 情况。 时下软件开发社区流行着这样的说法:软件开发的主流大约十年一变, 80 年代是 Unix 平台 /C/ 面向过程,90 年代是 Windows/C++/ 面向对象,到了 21 世纪的未来 十年应该转向 .NET/C#/ 面向组件了。也许我们都无法回避这一历史大趋势。 对于希望尽快了解C#的读者,我极力推荐这本由富有经验的开发高手和微软.NET 开发团队的成员合著的书。本书的篇幅与《 C# Language Specification (0.26版)》 相仿,却几乎涵盖了 C#和.NET 平台的所有核心内容,而且实例相当丰富,含金量 极高,充分体现了作者的功力和 O'Reilly公司言简意赅( in a nutshell)的风格。因 此本书刚一面世,就赢得了读者的好评, amazon.com 给予了四星评价,同时还成 为微软公司内部的热门书籍。书名翻译为“精髓”也可以说是实至名归。本书尤其 适合有面向对象编程经验、熟悉某种语言的读者。除了能够借以快速熟悉C#/.NET 平台之外,本书更可以作为日后开发时查索之用,极为方便。这也正是国外程序员 的电脑桌上和手提电脑包里总少不了 O'Reilly 公司图书的原因所在。 我翻译此书时,几乎搜罗了所有市面上已经出版的相关中文书籍,并且对照阅读了 网上的各种资料,摞起来都要超过一尺了,但是平心而论,要论概念阐述的全面精 确,还真的要首推本书。我在为确定许多新名词的译法遍寻参考时就发现,像属性 (property)、属性信息( attribute)、配件( assembly)、合约( contract)之类有难 度的地方,很多书根本就没有涉及或加以区分。 为了对得起本书的原作者,我在前前后后一个多月的翻译过程中尽了自己的最大努 力。虽然从小学就开始接触算法语言,从事软件开发前后也已经十多年了,但是翻 译这样一本高度浓缩、内容极新的技术图书,也是如履薄冰,感觉并不轻松。书中 已经有定论的术语译名均尽量采用,许多未有定论的,都经过了再三的讨论和斟 酌,读者可以参阅书后的“词汇表”部分。原书中存在的少量错误,包括 O'Reilly 公司已经公布的和翻译时发现的,均已在译文中改正。 本书对于初学者而言有一定的难度,希望我加入的一些译注,能够起到降低门槛的 作用。C#和.NET平台在没有最后定案之前肯定还会有较大的发展,所以希望大家 经常访问书中列出的网络资源,了解更多最新的背景知识。我要特别指出的是,学习 C# 必须尽量了解其植根的土壤 —— .NET平台,可喜的是中国电力出版社已经 引进了 O'Reilly 公司出版的本书姐妹篇《 .NET Framework Essentials》,希望能够 早日面世。 感谢中国电力出版社给我翻译和学习本书的机会。本书承蒙王敏之女士审校并查找 提供相关材料,受益良多,在此深表感谢。 我为本书专门设立了一个电子邮箱,读者可以及时反馈意见和批评: csharpbook@sohu.com。 刘基诚 2001/8/20 深夜 于北京西郊1 前言 本书将简明扼要地介绍 C#(读如 C Sharp)语言和 .NET 框架,使读者能够尽快 掌握这项最新的开发技术。C#和.NET的序幕是在 2000年7月佛罗里达州奥兰多 市举行的 Microsoft 专业开发人员大会( Professional Developers Conference, PDC)上揭开的。此后很快,.NET SDK(Software Development Kit)就在Internet 上发布了。 本书是以Microsoft .NET SDK beta版为基础的。C#语言和.NET框架(Framework) 将来还会继续发展。要跟上最新趋势,请经常访问“ C#在线资源”一节中列出的 网上资源,以及 O'Reilly 网站中为本书设置的网页(参见“建议与评论”)。 本书读者 虽然我们尽力使本书对所有要学习C#的人都有所裨益,但是本书的主要读者还是 已经熟悉 C++,Smalltalk,Java或 Delphi(译注1)等面向对象语言的开发人员。 C# 可以用于编写 Web 应用程序和服务,以及传统的独立程序或客户 / 服务器程 序。具备这些领域的经验当然有助于更快地掌握 C#语言和.NET 框架,但这不是 必需的。 译注 1: 准确地说是 Object Pascal。 前言2 关于本书 本书分为如下 5 章,6 个附录。 第一章“简介”,将引导读者走进 C# 语言和 .NET 框架。 第二章“C# 语言参考”,将详细介绍 C# 语言,本章可以当做语言参考使用。 第三章“.NET 框架编程”,讲述如何使用 C# 语言和 .NET 框架。 第四章“ BCL 综述”,概述了 .NET 中关键的库(按其功能进行组织) ,并介绍了 每个库最重要的名字空间。 第五章“核心 .NET 工具”,概述了.NET 框架中附带的工具,包括 C#编译器和导 入 COM 对象、导出 .NET 对象的工具。 6个附录提供了程序员感兴趣的其他信息,包括一个按字母顺序组织的 C#关键字 参考,正则表达式和字符串格式,配件( assembly)和名字空间( namespace)映 射的交叉引用。 本书假定你已经有.NET 框架SDK的 beta版。如要了解本书中讲述的语言特性和 类库的更多细节,我们推荐 Microsoft .NET 联机文档。 C# 在线资源 在正式发布前,C# 语言和 .NET 框架肯定还会发生变化。而且,由于 Microsoft 已经把 C# 和 CLI(Common Language Infrastructure,公用语言基础结构)提交 给 ECMA 进行标准化,这不可避免地还会产生一些变化。 要与最新的进展保持一致,应该定期访问 O'Reilly 网站中为本书设置的网页(参 见“建议与评论”)。前言 3 我们还推荐以下网站: http://msdn.microsoft.com/net/ Microsoft .NET Developer Center是.NET的官方网站,包括 .NET框架SDK (含有C#编译器)的最新版本,以及文档、技术文章、示范代码、到讨论组 的链接和第三方资源。 http://msdn.microsoft.com/net/thirdparty/default.asp C# 语言和 .NET 框架开发人员感兴趣的第三方资源的完整列表。 http://discuss.develop.com/dotnet.html DevelopMentor的DOTNET 讨论列表。可能是对 .NET语言和框架最好的自 由独立讨论站点。 参与者中经常有Microsoft的关键工程师。 http://www.devx.com/dotnet/resources/ .NET 在线资源的 DevX 列表。内容丰富而且全面。 还有两篇有意思的文章,读者可以看一看: http://windows.oreilly.com/news/hejlsberg_0800.html C#首席设计师Anders Hejlsberg的访谈, 由O'Reilly编辑John Osborn撰写。 http://www.genamics.com/visualj++/csharp_comparative.htm 该文比较了 C# 与 C++ 和 Java, 由本书作者之一 Ben Albahari 撰写。 排版约定 本书英文采用以下排版约定: 斜体(Italic) 表示目录或文件名等。 等宽字体(Constant width) 表示类型、名字空间、函数、关键字等应该原样录入的语言结构。以及代码 行、类、类成员和 XML 标签。 前言4 等宽斜体(Constant width italic) 表示语法上可以替代的参数名或应由用户提供的元素。 本书中将提供许多(但不是全部)语言结构的语法格式。这不是为了面面俱到, 更具体的细节可以查阅 .NET SDK 中的 Microsoft C# 语言参考。我们的目的是 使你能够快速地理解某个语言结构的语法。我们用 XML 频率运算符( ?,*,和 +)更精确地指定某个结构中元素出现的次数。 x 表示 x 照原样使用(等宽字体)。 x? 表示 x 可能不出现或仅出现一次。 x* 表示 x 可能不出现或出现多次,用逗号分隔。 x+ 表示 x 可能出现一次或多次,用逗号分隔。 [... ] 在未使用{},()和[]隐含分组时,表示代码元素的逻辑分组。 [ x|y ] 表示几个代码元素中仅有一个出现。 建议与评论 本书的内容都经过测试,尽管我们做了最大的努力,但错误和疏忽仍然是在所难 免的。如果你发现有什么错误,或者是对将来的版本有什么建议,请通过下面的 地址告诉我们: 美国: O'Reilly & Associates,Inc. 101 Morris Street Sebastopol, CA 95472前言 5 中国: 100080 北京市海淀区知春路 49 号希格玛公寓 B 座 809 室 奥莱理软件(北京)有限公司 询问技术问题或对本书的评论,请发电子邮件到: info@mail.oreilly.com.cn 最后,你可以在 WWW 上找到我们: http://www.oreilly.com http://www.oreilly.com.cn 致谢 本书如果没有其他人的支持,是不可能完成的。要感谢的人包括我们的朋友、家 人和 O'Reilly 公司努力工作的员工们。 我们三位都要感谢 Jeff Peil 对本书中有关线程和互操作章节的贡献。还要感谢 Scott Wiltamuth,Joe Nalewabu,Andrew McMullen 和 Michael Perry, 他们的技 术审读极大地提高了书稿的质量。 Ben Albahari 首先,我要感谢我的家庭(Sonia,Miri和Joseph Albahari)和朋友(Marcel Dinger 和 Lenny Geros),感谢他们当我“粘”在计算机上时对我的理解。还要感谢散落 在我桌上的那些 CD 的主唱乐队(不胜枚举,但写到这里时,要特别感谢 Fiona Apple,Dream Theater 和 Incubus),没有这些 CD,我不可能挺到早晨 5:00 不睡 觉,而且还去打扰同处一个时区的本书的其他合作者( John Osborn,Peter Drayton 和 Brad Merrill)。最后我要感谢所有对新技术有热情的人,你们是我写 作本书的原动力。我把本书献给已过世的父亲 Michael,是他的远见卓识,使我 还是一个孩子时,就接触到了编程。 前言6 Peter Drayton 首先,感谢我的妻子, Julie DuBois, 感谢她长期充满爱意的支持。无论数字世界 如何迷人,是她让我记得,还有更精彩的现实世界。我要感谢本书的合作者, Ben 和 Brad的慷慨,让我有机会参加本书的写作。也要感谢编辑 John Osborn 在我们 三个跑题的时候,将我们拉回到原来的轨道上来。感谢所有我的朋友和同事(特 别是 John Prout,Simon Fell,Simon Shortman 和 Chris Torkildson)在技术和 生活上支持我。最后,要感谢在南非的家人,尤其是我的父亲老 Peter Drayton, 已故的母亲 Irene Mary Rochford Drayton,是他们在人生道路上给我有力的指 导。 Brad Merrill 我要感谢儿子 Haeley,伴侣 Jodi,和朋友( Larry, Colleen 和 Evan)在我写书期 间给予的耐心和支持。还要感谢 Ben 和 Peter 的合作,以及使我们始终保持清醒 的编辑 John Osborn。i 目录 前言 ...................................................................................1 第一章 简介......................................................................7 C# 语言 ......................................................................................................................... 7 CLR ............................................................................................................................... 9 BCL .............................................................................................................................11 第一个 C# 程序 ......................................................................................................... 11 第二章 C# 语言参考...................................................... 13 标识符........................................................................................................................ 13 类型............................................................................................................................ 13 变量............................................................................................................................ 26 表达式与运算符 ....................................................................................................... 28 语句............................................................................................................................ 31 类型组织.................................................................................................................... 40 继承............................................................................................................................ 42 访问修饰字................................................................................................................ 48 类和结构.................................................................................................................... 50ii 目录 接口............................................................................................................................ 69 数组............................................................................................................................ 73 枚举............................................................................................................................ 75 委托( delegate)....................................................................................................... 77 事件( event)............................................................................................................ 80 try 语句和异常 ........................................................................................................... 83 属性信息.................................................................................................................... 86 不安全代码和指针 ................................................................................................... 89 预处理指令................................................................................................................ 92 XML 文档 ................................................................................................................... 93 第三章 .NET 框架编程................................................ 100 公用类型.................................................................................................................. 100 数学.......................................................................................................................... 106 字符串 ...................................................................................................................... 108 集合.......................................................................................................................... 111 正则表达式.............................................................................................................. 119 输入 / 输出 ............................................................................................................... 121 联网.......................................................................................................................... 125 线程.......................................................................................................................... 130 配件.......................................................................................................................... 135 反射.......................................................................................................................... 139 定制属性信息.......................................................................................................... 147 自动内存管理.......................................................................................................... 154 同本机 DLL 互操作................................................................................................ 158 与 COM 互操作....................................................................................................... 165 第四章 BCL 综述......................................................... 170 核心类型.................................................................................................................. 171 文本.......................................................................................................................... 171目录 iii 集合.......................................................................................................................... 172 流和输入输出.......................................................................................................... 172 联网.......................................................................................................................... 172 线程.......................................................................................................................... 173 安全.......................................................................................................................... 173 反射.......................................................................................................................... 174 序列化...................................................................................................................... 174 远程调用.................................................................................................................. 175 Web 服务 .................................................................................................................. 175 数据访问.................................................................................................................. 176 XML .......................................................................................................................... 176 图形.......................................................................................................................... 177 丰富的客户应用程序............................................................................................. 177 Web 应用程序.......................................................................................................... 177 全球化...................................................................................................................... 178 配置.......................................................................................................................... 178 高级组件服务.......................................................................................................... 179 配件.......................................................................................................................... 179 诊断与调试.............................................................................................................. 180 与未管制代码互操作............................................................................................. 180 组件和工具支持 ..................................................................................................... 180 运行时设施.............................................................................................................. 181 本地操作系统设施 ................................................................................................. 181 第五章 核心 .NET 工具............................................... 182 附录一 C# 关键字 ....................................................... 187 附录二 正则表达式...................................................... 194iv 目录 附录三 格式限定符...................................................... 199 附录四 数据列集 ......................................................... 206 附录五 使用配件......................................................... 208 附录六 名字空间与配件 ............................................. 212 词汇表 ......................................................................... 2197 第一章 简介 C# 是一种专门为 Microsoft 全新的 .NET 框架( Framework)而创建的编程语言。 .NET 框架由一个称为 CRL(Common Languge Runtime,公用语言运行时环境) 的运行时环境,和一套基类库组成,为各种语言和工具提供了丰富的开发平台。 C# 语言 编程语言的能力有很多方面。一些语言很强大但容易出错,而且很难驾驭,而另 一些相对简单但功能或性能上又有局限。C# 是一种全新的语言,旨在提供简单 性、表达力和性能的最佳结合。 C# 的许多特性都是借鉴了其他语言(尤其是 Java 和 C++)的优劣得失而设计出 来的。C#语言规范由Anders Hejlsberg和Scott Wiltamuth编写。Anders Hejlsberg 因为创造了Turbo Pascal编译器并领导了 Delphi的设计团队,早已经在编程领域 闻名遐迩。 C# 的关键特性包括以下几个方面: 面向组件 管理程序复杂性的绝佳方式是将程序分为几个相互操作的组件,其中的一些 可以用于多种环境中。C# 被设计成可以容易地创建组件,还提供了面向组第一章8 件的语言结构,如属性( property)、事件,和称为属性信息( attribute)的 声明性结构(译注 1)。 一站式编码 C# 中与声明有关的一切都仅限于声明本身,而不会分散在几个源码文件或 一个源码文件的几个地方。例如,类型在单独头文件或 IDL(接口定义语言) 文件中无需附加声明,属性的 get/set方法按逻辑分类,文档直接嵌入在声明 中,等等。而且,由于声明的顺序无关,类型不需要单独的存根( stub)声 明供其他类型使用。 版本协调(versioning) C#提供了显式的接口实现,隐藏继承成员和只读修饰字(modifier)等特性, 这有助于组件新版本与其他的旧组件协调工作。 类型安全和统一类型系统 C# 是类型安全的,这可以确保一个变量只能通过与它相关的类型访问。这 种封装有利于促进良好的程序设计方式,并且通过禁止(因为疏忽或恶意) 随意覆盖变量,消除了潜在的错误或安全的漏洞。 所有 C# 类型(包括基本类型)都由一个基类型派生而来。这样就提供了一 个统一类型系统( 译注 2),这也意味着,所有类型(结构、接口、委托、枚 举和数组)都有同样的基本功能,例如能够转化为一个字符串,能够序列化 或存储在一个集合中。 自动和手动内存管理 C# 依靠一个运行时环境自动进行内存管理。这样程序员就可以从对象处理 中解放出来,消除了诸如悬挂( dangling)指针、内存泄漏和循环引用等问 题。 译注 1: property和 attribute的翻译是本书的难点,也是读者需要特别注意的地方。这是因为 两词一般都通译为“属性” ,而在 C#中,两个概念完全不同。本书对此进行了详细解 释和区分,这是目前已出的中文书中所缺乏的。这两个概念的具体细节,请参见下文。 另外,有些 C# 书籍中,在讲述面向对象一般概念时,仍然会使用 attribute 来表示对 象的属性,请注意与 C# 中的 attribute 区分。 译注 2: 也就是 CTS。简介 9 但是,C#没有去掉指针这种类型,只是在大多数编程任务中没有必要使用。 对于对性能要求极高的和需要良好互操作性的场合还可以使用指针,但只能 允许用在 unsafe 代码块中,这需要很高的执行安全权限。 CLR 的使用 C# 相对于其他语言,尤其是 C++ 这样的传统编译语言,所具有的巨大优势 就在于它与 .NET CLR 的紧密配合,C# 的许多方面都与 CLR 相同,特别是 类型系统,内存管理模型,异常处理机制。 CLR .NET 框架最基本的特点就是,程序是在由 CLR 提供的执行管制环境中执行的。 CLR 大大地提高了运行时程序间的交互性、可移植性、安全性、开发的简便性和 跨语言的集成性,并且提供了丰富的基础类库。 这些优点的关键就在于.NET程序的编译方式。每种针对 .NET的语言都将源码编 译成元数据(metadata)和 MSIL 代码。元数据中有程序完整的说明,包括所有 类型,还有每个函数的实际实现。这些实现将保存为 MSIL,这是一种描述程序 指令的与机器无关的代码。CLR 使用这个“蓝图”在运行时生成 .NET 程序,而 且可以提供传统方法(将代码直接编译为汇编语言)远不能提供的服务。 CLR 的关键特性包括: 运行时交互性 程序不仅通过元数据进行各种形式的交互。还可以在运行时搜索新类型,然 后实例化并调用这些类型的方法。 可移植性 程序在支持CLR的操作系统/处理器组合上无需重新编译即可运行。这种平 台无关性的关键要素是运行时的即时编译器( JIT),由它将 MSIL 编译成平 台基础上的本地代码。第一章10 安全性 有关安全性的考虑贯彻了整个.NET框架设计。此中关键也在于 CLR能够正 确分析 MSIL 指令是否安全。 部署简单 配件是一种可以完全自我进行描述( self-describing)的包,包含一个程序的 所有元数据和MSIL。配件的部署非常简单:将配件复制到客户机上就行了。 版本协调(versioning) 配件可以正确地与以之为基础的新版配件协作,无需重新编译。此中关键是 可以通过元数据解析所有类型引用。 简化开发 CLR 提供了许多特性,大大简化了开发,包括无用资源回收,异常处理,调 试和描述(profiling,译注 3)等服务。 跨语言集成 CLR 的 CTS(Common Type System,公用类型系统)定义了可在元数据和 MSIL 中表达的类型,以及可在这些类型上执行的操作。 CTS 非常宽松,足 以支持各种不同语言,包括微软的语言如 C#,VB.NET和 VC.NET,以及其 他第三方语言如 COBOL、Eiffel,Haskell, Mercury, ML, Oberon,Perl, Python, Smalltalk 和 Scheme。 CLS(Common Language Specification,公用语言规范)定义了 CTS的子集, 提供了使 .NET 语言可以互相共享和扩展库的通用标准。例如, Eiffel 程序 员可以创建一个派生自 C# 类的类,并覆盖其虚方法。 与原有代码的互操作性 CLR 提供了与(用 COM 和C 写成的)庞大的已有软件库的互操作性。 .NET 类型可以作为 COM 类型暴露( expose),而COM 类型可以作为 .NET 类型 导入。而且, CLR 提供了 PInvoke(译注 4),一种使 C 函数、结构和回调能 够很容易地在 .NET 程序中使用的机制。 译注 3: profiling 一词意思较多,由于缺乏足够上下文,此处遵循编译原理教科书的译法。 译注 4: Platform Invoke Service(平台调用服务)的简称。简介 11 BCL .NET 框架提供了 BCL(基类库,Base Class Library),可用于所有语言,这些 库提供了从运行时核心功能如线程和运行时类型操作(反射) ,到高级功能,如数 据访问,丰富的客户端支持以及 Web 服务(其中代码甚至可以嵌入网页) ,非常 广泛。C# 几乎没有内置库,它使用 BCL 就行了。 第一个 C# 程序 下面是我们第一个 C# 程序(译注 5): namespace FirstProgram { using System; class Test { static void Main () { Console.WriteLine ("Hello, World! Welcome to C#!"); } } } C# 程序由组织成名字空间的类型(通常是类, 译注 6)构成。每个类型包含函数 成员(通常是方法) ,以及数据成员(通常是字段) 。在这个程序中,我们定义了 名为 Test 的类,它包含一个名为 Main 的方法,向 Console 窗口输出一句话 “Hello, World! Welcome to C#!”。Console 类封装了标准输入输出功能,提供 了 WriteLine 这样的方法。使用其他名字空间的类, 要用到 using 指令。因为 Console类在 System名字空间中,要写“ using System”;类似的,其他名字空 间的类型要使用我们的 Test 类,也要写“using FirstProgram”。 在 C# 中 , 不存在单独的函数; 函数总是与一个类型相关联,或者是某个类型的 实例。我们的这个程序很简单,只使用了静态成员,也就是说,函数只是与类型 译注 5: 这里遵循 C 语言之父 Kernighan 和 Ritchie 的惯例,代表新语言向世界问好。 译注 6:类型( type)是 C#中用来表示事物的术语。包括各种数据类型(整数、浮点) 、自定 义类型(如窗口、按钮)等等。第一章12 相关联,而不是类型的实例。而且,只使用了 void方法,也就是说,方法不返回 值。最后要注意的是,C# 将 Main 方法作为默认的执行入口点。 要把这个程序编译成可执行文件,将它粘贴到一个文本文件,存为 Test.cs,然后 在命令提示符下输入csc Test.cs。这样就把它编译成一个叫作Test.exe的可执 行文件了。13 第二章 C# 语言参考 本章将遍览C#语言的方方面面。如果有一定的类型化面向对象语言的经验,其中 的很多特性你都会很熟悉。 标识符 标识符( identifier)是程序员为类型、方法、变量所选择的名字。标识符必须是 一个英文单词,本质上由以字母或下划线打头的 Unicode 字符组成。标识符不能 与关键字冲突。有一个特例, @ 前缀可以用来避免这种冲突,但 @ 字符不认为是 标识符的一部分。比如,下面两个标识符等价: Ko n @Ko n C#标识符是区分大小写的,但为了与其他语言兼容,不应该仅用大小写区分公开 的(public)或是保护的(protected)标识符。 类型 C#程序是通过建立新类型( type),并使用已有类型来编写的,包括 C#自身定义 R R第二章14 的以及从其他库中导入的类型。每个类型都包含一套数据和函数成员,合起来就 形成了 C# 程序关键构件的模块单元。 类型实例 一般情况下,要使用类型,必须创建类型的实例。这些需要类型来实例化 (instantiate)的数据成员和函数成员,我们称之为实例成员( instance member)。 可用于类型本身的数据成员和函数成员称之为静态成员(static member)。 实例:创建和使用类型 下面的程序中,我们创建自己的类型Counter以及另一个使用Counter实例的类 型Test。 Counter类型的使用预定义类型int,Test类型使用在System名字 空间中定义的 Console 类的静态函数成员 Writeline。 // 从 System 名字空间导入类型,如 Console using System; class Counter { // 新类型通常有类或结构 // --- 数据成员 --- int value; // 类型 int 的字段 int scaleFactor; // 类型 int 的字段 // 构造器,用于初始化类型实例 public Counter(int scaleFactor) { this.scaleFactor = scaleFactor; } // 方法 public void Inc() { value+=scaleFactor; } // 属性 public int Count { get {return value; } } } class Test { // 执行从这里开始 static void Main() {C# 语言参考 15 // 创建 Counter 类型的实例 Counter c = new Counter(5); c.Inc(); c.Inc(); Console.WriteLine(c.Count); // 输出 "10"; // 创建另一个 Counter 类型的实例 Counter d = new Counter(7); d.Inc(); Console.WriteLine(d.Count); // 输出 "7"; } } 隐式和显式转换 每种类型都有自己的规则集,定义了与其他类型相互转换的方法,类型间的转换 可以是隐式的也可以是显式的。隐式( implicit)转换可以自动执行,而显式 (explicit)转换需要使用 C 类型转变换运算符“()”。 int x = 123456; // int 是 4字节整数 long y = x; // 隐式转换为 8 字节整数 short z =(short)x // 显式转换为 8字节整数 隐式转换的前提是,保证可以成功,不会丢失信息。与此相反,显式转换则在运 行时的环境决定转换是否成功,或信息可以在转换中丢失时,才是必需的。 大多数据规则由语言提供,比如上面的数字转换。有时还会需要自己定义隐式和 显式转换(参见“表达式与运算符”一节)。 类型的分类 所有类型,包括预定义和用户定义的,都可分为三类:值(value),引用(reference) 和指针(pointer)。 值类型 通常表示基本类型。简单类型,如基本数字类型( int,long,bool 等),都是结第二章16 构,也是值类型。你可以通过自定义结构扩展简单类型集合。而且, C#还可以定 义枚举(译注 1)。 引用类型 通常表示更复杂、功能更多的类型。最基本的 C#引用类型是类,但特殊的功能是 由数组,委托(delegate)和接口类型提供的。 指针类型 指针类型在 C# 中不太常见,仅用于在不安全语句块中进行显式内存操作(参见 “不安全代码和指针”一节)。 预定义类型 C# 有两类预定义类型: ● 值类型:整数,浮点数,decimal,char,bool ● 引用类型:object,string 所有这些类型值可在System名字空间找到,例如,以下两个语句语法上是相同 的: int i = 5; System.Int32 i = 5; 整数类型 参见下表。 译注 1:在C# 中引入枚举是经过了深思熟虑的,因为 Java 中没有这一类型。详情请参见 O'Reilly 编辑 John Osborn 对 C# 首席设计师 Anders Hejlsberg 的访谈:http:// windows.oreilly.com/news/hejlsberg_0800.html。C# 语言参考 17 C# 类型 系统类型 大小(字节)有无符号 sbyte System.Sbyte 1 有 short System.Int16 2 有 int System.Int32 4 有 long System.Int64 8 有 byte System.Byte 1 无 ushort System.UInt16 2 无 uint System.UInt32 4 无 ulong System.UInt64 8 无 sbyte,short,int和long 是有符号整数;byte,ushort,uint 和ulong是 无符号整数。 对于 n 位的无符号整数,其值的范围是 0~2n-1。而对于 n 位的有符号整数,其值 的范围是 -2n-1~2n-1-1。整数可以用十进制数或十六进制数表示: int x = 5; ulong y = 0x1234AF; // 前缀 0x 指的是十六进制 当一个整数对于不同的类型都有效时,默认类型按以下顺序选择: int uint long ulong 以下后缀可以加在值的后面以显示地指定其类型: U Uint 或ulong L long 或ulong UL ulong 整数转换。整数值之间的隐式转换,在转换目标类型包含被转类型所有可能值时, 是允许的,否则需要显式地进行转换。例如,你可以隐式地将 int转为 long,但 必须显式地将 int 转换为 short:第二章18 int x = 123456; long y = x; // 隐式,无信息损失 short z = (short)x; // 显式,将使 x 缩短 浮点类型 C# 类型 System 类型 大小(字节) float System.Single 4 double System.Double 8 float 类型可保存近似于 ±1.5×10-45~±3.4×1038 的数,含 7 位有效数字。 double 类型可保存近似于 ±5.0×10-324~±1.7×10308,含 15~16 位有效数。 浮点类型保存特殊值 +0,-0,+∞,-∞或“非数”(NaN,not a number)用于表 示一些数字运算和被零除的结果。float 和double 实现了 IEEE 754 格式化类型 标准,该标准为几乎所有处理器所支持,由 http://www.ieee.org 定义。 浮点数可以用十进制或幂的形式表示。float型要加的后缀“ f”或“ F”。double 型可以选用后缀“d”或“D”。 float x = 9.81f; double y = 7E-02; // 0.07 浮点数转换。从 float 到double 的隐式转换不会损失信息,是允许的,但反之 则不行。从 int,uint 或 long 到float,以及从 long 到double 的隐式转换都 是允许的,这样可以使代码更易读: int strength = 2; int offset = 3; float x = 9.53f * strenngtgth - offset; 如果本例中使用更大的值,可能会丧失精度。但值的范围不会减小,因为 float 和double最大最小可能值都超过了int,uint和long。所有其他整型和浮点 数型之间的转都必须是显式的:C# 语言参考 19 float x = 3.53f; int offset = (int)x; decimal 类型 C# 类型 System 类型 大小(字节) decimal System.Decimal 16 decimal 类型可以保存 ±1.0×10-28 到近似于 ±7.9×10-28 的值,有效数为 28~29。 decimal 类型保存 28 个数字以及小数点的位置。与浮点值不同,它的精度更高, 但范围更小。它经常用于财务计算中,在这种场合,它的高精度和没有舍入错误 存储十进制数的能力是极为重要的。例如,数 0.1 可用 decimal 类型准确表示, 但形式是一个浮点类型的循环二进制数。这里没有 +0,-0,+∞,-∞和非数概念。 decimal 数需要后缀“M”或“m”。 decimal x = 80603.454327m; // 存有准确值 decimal转换。从所有整数类型到decimal的隐式转换都有允许的,因为 decimal 类型可以表示所有整型值。Decimal与浮点数必须进行显示转换,因为浮点类型 值范围比 decimal 大,但精度却低。 char 类型 C# 类型 System 类型 大小(字节) char System.Char 2 char 类型表示 Unicode 字符。 char 的直接量可以包含单引号括起来的一个字符、Unicode 或转义字符: 'A' // 一个字符 '\u0041' // Unicode第二章20 '\x0041' // 无符号 short 十六进制数 '\n' // 转义字符 表 2-1 总结了 C# 认可的转义字符。 表 2-1 转义字符 字符 意义 值 \' 单引号 0x0027 \" 双引号 0x0022 \\ 反斜线 0x005C \0 空字符 0x0000 \a 警铃 0x0007 \b 退格 0x0008 \f 换页 0x000C \n 换行符 0x000A \r 回车 0x000D \t 水平制表 0x0009 \v 垂直制表 0x000B char 转换。从 char隐式转换为大多数数字类型都没有问题。这主要要看数字类 型是否能装下无符号 short 类型。如果不行,就需要显式转换。 bool 类型 C# 类型 System 类型 大小(字节) bool System.Boolean 1/ 2 bool 类型是逻辑值,可以赋值为 true 或 false。 虽然逻辑值只需一位( 0 或 1),但实际中都是占据一个字节,因为这是大多数处 理器结构所能寻址的最小单位。数组中的每个元素需要两字节。C# 语言参考 21 bool 转换。逻辑类型和数字类型之间不能转换。 object 类型 C# 类型 System 类型 大小(字节) object System.Object 0/ 8(内存开销) object类型是值类型和引用类型的最终基类。值类型没有存储开销( overhead)。 而引用类型是存储在堆中的,本身就需要有内存开销。在 .NET运行时环境中 ,引 用类型实例的内存开销是 8 个字节,用来存储对象的类型和“同步化锁状态或是 否已被无用资源回收器固定”等临时信息。注意,每个引用类型实例的引用要使 用 4 个字节。 更多关于 System.Object 类型的内容,请参见第三章中“常见类型”一节。 string 类型 C# 类型 System 类型 大小(字节) string System.String 20(最小) C# 的 string代表不可变的 Unicode 字符,是 System.String 类的别名 (参见第 三章中“ String”一节 )。 虽然 string 是一个类,但它在编程中的使用是如此普遍,以至于 C# 编译器和 .NET 运行时都给予它特权。 与其他类不同,新实例可以用一个 string 直接量创建: string a = "Heat"; 字符串还可以用不变( verbatim)字符串直接量创建。不变字符串直接量以 @ 开 头 , 表示该字符串必须保持不变,即使它跨越了许多行或者包含转义符(即 "\")。 下例中,a1 和 a2,以及 b1 和 b2 分别代表同一个字符串:第二章22 string a1 = "\\\\server\\fileshare\\helloworld.cs"; string a2 = @"\\server\fileshare\helloworld.cs"; Console.WriteLine(a1==a2); // 输出"True" string b1 = "First Line\r\nSecond Line"; string b2 = @"First Line Second Line"; Console.WriteLine(b1==b2); // 输出"True" 类型和内存 值和引用类型的基本区别在于它们的存储方式。 值类型的内存 一个值类型实例的内存保存着一个原始值,如一个数或字符。值类型还可以存在 堆栈中或内联。堆栈( stack)是一块内存区,每次进入一个方法时增加(因为方 法局部变量需要存储空间),并在方法退出时减小(因为局部变量不再需要了) 。 所谓内联( inline)指的是,值类型是作为对象的一部分声明的,比如它是一个数 组的成员或一个类的字段。 引用类型的内存 一个引用类型实例的内存保存着栈中一个对象的地址。引用类型可以为空,竟没 有引用任何对象。在程序执行时,引用被赋值为堆中已有或新建的对象。堆中的 一个对象会留在内存中,直至运行时的无用资源回收器确定,不需再引用时,对 象会被丢弃,并释放内存。 值类型和引用类型的比较 创建值类型或引用类型实例,可以用 new关键字调用类型的构造器,值类型构造 器只是初始化的一个对象。而引用类型构造器则在堆中创建一个新对象,然后初 始化这个对象。 // 引用类型声明C# 语言参考 23 class PointR { public int x, y; } // 值类型声明 struct PointV { public int x, y; } class Test { static void Main() { PointR a; // 局部引用类型 ,使用堆栈 // 中的 4 个字节保存地址 PointV b; // 局部值类型 , 使用堆栈 // 中的 8 个字节保存 x 和 y a = new PointR(); // 将堆中分配的 PointR 新实例的地址赋值给引用 // 堆中的对象使用 8 个字节保存 x 和 y // 另 8 个字节用于核心对对象的需求 // 比如存储对象的类型和同步化状态 b = new PointV(); // 调用值类型的默认构造器 // PointR 和 PointV 的默认构造器 // 将每个字段设置为默认值 // x 和 y均为 0 a.x = 7; b.x = 7; } } // 在方法的结尾,局部变量 a和 b 超出了作用域 // 但 PointR 的新实例仍然在内存中 // 直到无用资源回收器认为它不会再被引用了 给一个引用类型赋值将复制到一个对象的引用,而给一个值类型赋值将复制一个 对象的值。 ... PointR c = a; PointV d = b; c.x = 9; d.x = 9; Console.WriteLine(a.x); // 输出 9 Console.WriteLine(b.x); // 输出 7 } }第二章24 本例中,堆中的对象可以通过许多变量引用,而堆栈的内联的对象只能通过其声 明的变量访问。 统一类型系统 C# 提供了一个统一类型系统( unified type system),而 object 类是引用的值 类型共同的最终基类型。这意味着,所有类型,除了偶尔使用的指针类型,都有 同样的基本特性集合。 简单类型是值类型 大多数语言中,简单类型( int,float等)和用户定义类型( Rectangle,Button 等)需要严格区分。 C#中,简单类型实际是 System名字空间中结构的别名。例 如,int类型是 System.Int32结构的别名,long类型是 System.Int64结 构的别名,等等。这意味着简单类型含有与任何用户定义的类型所拥有的相同的 特性。例如,可以对 int 调用方法: int i = 3; string s = i.ToString(); 这等效于: // 这是 System.Int32 的解释性版本 namespace System { struct Int32 { ... public string ToString() { return ...; } } } // 这是合法的,但我们推荐使用 int 别名 System.Int32 i = 5; string s = i.ToString();C# 语言参考 25 值类型扩展了简单类型集合 创建一个含有 1000 个 int 值的数组是非常高效的。下面一行在一个连续内存块 中分配 1000 个 int 值: int[] iarr = new int [1000]; 类似的,创建一个值类型 PointV 的数组也很高效: struct PointV { public int x, y } PointV[] pvarr = new PointV[1000]; 如果使用一个引用类型 PointR,要在实例化数组后实例化 1000 个点。 class PointR { public int x, y; } PointR[] prarr = new PointR[1000]; for( int i=0; i 方法调用: f(x) 索引: a[x] 后缀自增: x++ 后缀自减: x-- 构造器调用: new 数组堆栈分配: stackalloc 取类型: typeof 取结构大小: sizeof 算术检测开启: checked 算术检测关闭: unchecked 一元 正号: + 负号: - 非: ! 补位运算: ~ 前缀自增: ++x 前缀自减: --x 类型转换: (T)x 地址取值: * 取地址: & 乘除运算 乘: * 除: / 取余: % 加减运算 加: + 减: -第二章30 表 2-2 运算符优先级(续) 类别 运算符 移位 左移位: << 右移位: >> 关系运算 小于: < 大于: > 小于等于: <= 大于等于: >= 类型相等 / 兼容: is 相等 相等: == 不相等: != 逻辑位 位与: & 位异或: ^ 位或: | 逻辑 与 : && 或: || 三元条件: ?: 例如 int x = a > b ? 2 : 7; 等价于: int x; if (a > b) x = 2; else x = 7; 赋值 赋值 / 改变: = *= /= %= += -= <<= >>= &= ^= |= 算术溢出检测运算符 检测 / 不检测运算符: checked(表达式) unchecked(表达式) 检测 / 不检测语句:C# 语言参考 31 checked 语句或语句块 unchecked 语句或语句块 checked运算符通知运行时生成一个OverflowException异常,如果一个整 型表达式超出了类型的算术限制。该运算符还影响带有整数类型之间的 ++,--, -(一元),+,-,*,/ 和显示转换运算符()的表达式。下面是一个例子: int a = 1000000; int b = 1000000; // 检测表达式 int c = checked(a*b); // 检测语句块中的所有表达式 checked { ... c = a * b; ... } checked运算符只适用于运行时表达式,因为常量表达式是在编译中检测的(虽 然可以将 checked[+/-]命令行编译开关关闭)。unchecked 运算符禁用编译时 的算术检测,而且很好使用,但可以使表达式编译: const int signedBit = unchecked((int)0x80000000); 语句 C#程序的执行是由一系列按文本顺序执行的语句( statement)指定的。所有基于 过程的语言(如 C#)中的语句都是接功能执行的。C# 中两种最基本的语句是声 明语句和表达式语句。C# 也提供流程控制语句用于选择、循环和跳转。此外 C# 还提供特殊用途的语句,如锁定内存或处理异常。 多个语句可以相互组合,零条或更多语句可以用大括号“ {}”括起来,形成一个 语句块。可以使用单语句的地方都能使用语句块。第二章32 表达式语句 语法为: [ 变量 =]?表达式; 表达式语句用于计算一个表达式的值,并将结果赋给一个变量,也可能产生其他 效果(包括调用,new,++ 或 --)。表达式语句以分号结尾。例如: x = 5 + 6; // 赋予结果 x++; // 其他效果 y = Math.Min(x, 20); // 其他效果和赋予结果 Math.Min (x, y); // 抛弃结果,但其他效果仍然保存 x == y; // 错误,其他效果和赋予结果都没有了 声明语句 变量声明语法: 类型 [变量 [= 表达式]?]+ ; 常量声明语法: const 类型 [变量 = 常量表达式]+ ; 变量声明语法用于一个新变量。可以通过在声明将表达式的结果赋给一个变量 (这是可选的),初始化一个变量。 局部或常量变量的作用域可达到当前块的结尾,不能在当前或任何嵌套块中声明 另一个名字相同的局部变量。例如: bool a = true; while(a) { int x = 5; if (x==5) { int y = 7; int x = 2; // 错误,x已经定义了C# 语言参考 33 } Console.WriteLine(y); // 错误,y超出了作用域 } 常量声明与变量声明相似,只不过变量的值不能在声明后改变: const double speedOfLight = 2.99792458E08; speedOfLight+=10; // 错误 空语句 ; 空语句什么事情也不做,在不需要执行任何操作但又需要一条语句时,可以用空 语句占位,例如: while(!thread.IsAlive); 选择语句 C# 有许多方式根据条件来控制程序执行流程。本节讲述最简单的两种结构, if -else 语句和 switch语句。而且,C# 还提供了各种运算符和循环语句,可以 根据一个逻辑表达式有条件地执行。此外,C#还提供了根据条件控制执行流程的 面向对象方式,即虚方法调用和委托调用。 if-else 语句 if(逻辑表达式) 语句或语句块 [ else 语句或语句块]? if-else 语句根据逻辑表达式是否为 true来执行代码。与 C 不同,C# 只允许 使用逻辑表达式。例如:第二章34 int Compare(int a, int b) { if (a>b) return 1; else if (a5) break; // 跳出循环 } continue 语句 continue; continue 语句放弃循环中剩下的语句,并开始下一次循环: int x = 0; int y = 0; while (y<100) { x++; if ((x%7)==0) continue; // 继续下一次循环 y++; } goto 语句 goto 语句标号; goto case 常量; goto 语句将执行转到语句块中另一条带有“标号”的语句。标号语句只是方法 中的一个占位符: int x = 4; start:C# 语言参考 39 x++; if (x==5) goto start; goto case语句将执行转到下一个switch块的另一个case语句(参见“ switch 语句”一节)。 return 语句 return 表达式?; return语句用于退出一个方法,如果该方法不为空,必须返回一个方法return 类型的表达式: int CalcX(int a) { int x = a * 100; return x; // 将值返回调用方法 } throw 语句 throw 异常表达式?; throw 语句抛出一个 Exception,表明发生了异常情况(参见“ try 语句和异 常”一节)。 if (w==null) throw new Exception("w can't be null"); lock 语句 lock(表达式) 语句或语句块 lock 语句实际上是调用 Monitor类的 Enter 和 Exit 方法的一种语法简写方 式(参见第三章中的“线程”一节)。第二章40 类型组织 C#程序实际上是一组类型。这些类型在文件中定义,由名字空间组织,编译为模 块(module),然后组织进一个配件(assembly)。 通常,这些组织单位会交叉:一个配件可以含有许多名字空间,而一个名字空间 可以扩展到几个配件;一个模块可以是许多配件的部分,而一个配件可以包含许 多模块。一个源码文件可以包含许多名字空间,而一个名字空间可以跨越许多源 码文件。详情请参见第三章的“配件”一节。 文件 文件组织对于 C#编译器似乎并非重要:整个工程都可以放进一个 .cs 文件,而且 仍然可以编译成功(预处理语句是唯一的例外)。但是通常说来,在一个文件中有 一个类型,同时有一个与类名匹配的文件名和一个与类的名字空间名匹配的目录 名,这样比较好。 名字空间 名字空间声明语法为: namespace 名字 +(注 1){ using 语句 * [名字空间声明 | 类型声明]*(注 2) } 名字空间可以用于将相关类型归入一个层次结构中。通常名字空间中第一个名字 是组织的名字,其后跟着的名字以较好的间隔将类型分组。例如: 注 1: 用点分界。 注 2: 无分界符。C# 语言参考 41 namespace MyCompany.MyProduct.Drawing { class Point {int x, y, z} delegate void PointInvoker(Point p); } 嵌套名字空间 还可以不用点分界,而使用嵌套。语义上,下例与上例相同: namespace MyCompany { namespace MyProduct { namespace Drawing { class Point {int x, y, z} delegate void PointInvoker(Point p); } } } 使用类型的全限定名 类型的完整名字应包含其名字空间的名字。为在其他名字空间使用 Point类,可 以用全限定名(fully qualified name)引用它: namespace TestProject { class Test { static void Main() { MyCompany.MyProduct.Drawing.Point x; } } } using 关键字 using 关键是一种方便的方法,可以避免使用其他名字空间中的全名。语义上, 下例与上例相同: namespace TestProject { using MyCompany.MyProduct.Drawing; class Test {第二章42 static void Main() { Point x; } } } 类型和名字空间别名 类型名在一个名字空间中必须唯一。为避免不使用全限定名时的命名冲突,C#允 许为类型或名字空间指定别名。例如: using sys = System; // 名字空间别名 using txt = System.String; // 类型别名 class Test { static void Main() { txt s = "Hello, World!"; sys.Console.WriteLine(s); // 输出“ Hello, World!” sys.Console.WriteLine(s.GetType()); // System.String } } 全局名字空间 所有名字空间和类型隐式声明的最外层称为全局名字空间(global namespace)。 当一个类型没有在一个名字空间中声明时,它不能不加限定地在其他名字空间中 使用,因为它是全局名字空间的一个成员。 但是将类型按分类归入名字空间中,永远是好的做法。 继承 C#类可以从另一个类继承,以扩展或定制。类只能从一个类中继承,但可以被许 多类继承,这样就形成了类层次。任何类层次的根都是 object类,所有对象都 从它隐式地继承而来。从类继承,需要在类声明中指定要继承的父类,使用 C++ 的冒号符号:C# 语言参考 43 class Location { // 隐式从 object 继承 string name; // 初始化 Location 的构造器 public Location(string name) { this.name = name; } public string Name {get {return name;}} public void Display() { Console.WriteLine(Name); } } class URL : Location { // 从 Location 继承 public void Navigate() { Console.WriteLine("Navigating to "+Name); } // URL 构造器 , 它需要调用 Location 的构造器 public URL(string name) : base(name) {} } URL 拥有 Location 的所有成员,以及一个新成员,Navigate: class Test { static void Main() { URL u = new URL("http://microsoft.com"); u.Display(); u.Navigate(); } } 注意: 特殊类( specialized class)和通用类(general class),又称派生类( derived class)和基类(base class),或者子类( subclass)和超类(super class)。 类转换 类 D 可以隐含的向上转换(upcast)为它从中派生而来的类 B,而类 B 可以显示 地向下转换(downcast)为由它派生的类 D。例如:第二章44 URL u = new URL(); Location l = u; // 向上转换 u = (URL)l; // 向下转换 如果向下转换失败,将抛出 InvalidCastException 异常。 as 运算符 as 运算符用于向下转换,如果转换失败,结果值为 null。 u = l as URL; is 运算符 is运算符可以测试一个对象是否来自或派生自一个特殊类(或实现了一个接口) 。 通常用于在向下转换前进行测试: if (l is URL) ((URL)l).Navigate(); 多态性 多态性( polymorphism)是一种在许多类型上执行同一操作的能力,只要每个类 型都共享相同的特征子集。C# 定制类型可以通过继承类和实现接口显现多态性 (参见“接口”一节)。 下例中,show方法可以在 URL和LocalFile类型上执行 Display 操作,因为两 个类型都继承了 Location 的特征。 class LocalFile : Location { public void Execute() { Console.WriteLine("Executing "+Name); } // LocalFile 的构造器,它需要调用 URL 的构造器 public LocalFile(string name) : base(name) {} } class Test {C# 语言参考 45 static void Main() { URL u = new URL(); LocalFile l = new LocalFile(); Show(u); Show(l); } public static void Show(Location loc) { Console.Write("Location is: "); loc.Display(); } } 虚函数成员 多态性的一个关键点是,每个类型可以以自己的方式实现共享的特征。对于基类 而言,实现这种灵活性的一种方式,是将函数成员声明为 virtual(虚的)。派 生类可以为基类中标有 virtual 的函数成员提供自己的实现(参见“接口”一 节): class Location { public virtual void Display() { Console.WriteLine(Name); } ... } class URL : Location { // 删除开头的 http:// public override void Display() { Console.WriteLine(Name.Substring(6)); } ... } URL 现在有一个显示自己的定制方式。上一节中 Test 类的 Show 方法现在可以 调用 Display的新实现。已覆盖方法的签名( signature,译注 2)和虚方法必须 相同,但与 Java 和 C++ 不同,还需要 override 关键字。 译注 2: 签名指的是方法、属性、字段和局部变量的定义中所含的类型列表。对于方法而言, 签名指其名字、类型、返回类型等。第二章46 抽象类和成员 一个类可以被声明为 abstract(抽象的)。抽象类可以拥有 abstract 成员, 即没有隐含的虚实现的函数成员。前面的例子中,我们为 URL 类型指定了一个 Navigate 方法,为 LocalFile 类型指定了一个 Execute 方法。也可以用一 个带有称为 Launch 的 abstract 方法的 abstract 类声明 Location: abstract class Location { public abstract void Launch(); } class URL : Location { public override void Launch() { Console.WriteLine("Run Internet Explorer..."); } } class LocalFile : Location { public override void Launch() { Console.WriteLine("Run Win32 Program..."); } } 派生类必须覆盖所有继承的抽象成员,或者自我声明为 abstract。abstract 类 不能实例化。例如,如果 LocalFile不覆盖Launch,LocalFile 自己必须被声 明为 abstract,可能允许 Shortcut 和PhysicalFile 由它继承。 密封类(sealed class) 一个类可以通过在类声明中指定 sealed 修饰字,防止其他类从它继承: sealed class Math { ... } 要密封一个类最常见的情况,是在类只包含静态成员的时候,如基类库的 Math 类。密封类的另一种作用是使编译器将所有此类上进行的虚方法调用转成更快的 非虚方法调用。C# 语言参考 47 隐藏继承成员 除了用于调用构造器外,关键字 new还可以隐藏基类的数据成员、函数成员以及 类型成员。用 new覆盖一个虚拟方法,可以隐藏而不是覆盖此方法的基类实现: class B { public virtual void Foo() {} } class D : B { public override void Foo() {} } class N : D { public new void Foo() {} // 隐藏 D 的 Foo } N n = new N(); n.Foo(); // 调用 N 的 Foo ((D)n).Foo(); // 调用 D 的 Foo ((B)n).Foo(); // 调用 D 的 Foo 与基类签名相同的方法声明应该显式说明是覆盖还是隐藏继承成员。 版本协调虚函数成员 C# 中,如果方法覆盖了一个 virtual 方法,方法在被编译时将带有一个为 true 的标志。此标志对于版本协调至关重要。假定要写一个派生自 .NET 框架基类的 类,并将应用程序部署在一个客户机器上。客户后来要升级 .NET 框架,则 .NET 基类现在包含一个 virtual 方法,碰巧与派生其中一个方法的签名相同: public class Base { // 由库作者编写 public virtual void Foo() {...} // 更新中增加 } public class Derived : Base { // 由用户编写 public void Foo() {...} } 大多数面向对象的语言(如 Java)中,方法不能带这种标志编译,因此签名相同 的派生类的方法将覆盖基类的 virtual方法。这意味着,将对类型 D 的 Foo方 法进行一个 virtual调用,即使 D 的 Foo不可能按照类型 B 作者的指定意图实第二章48 现。这很容易使程序崩溃。 C#中,可以保证程序按原始意图工作。当有机会以最 新框架重编译时,可以给 Foo增加new修饰字,也可以重新给 Foo取其他的名字。 访问修饰字 为了提倡封装,一个类型或类型成员可以通过在声明中增加以下五种访问修饰字 之一,选择自己相对其他类型或其他配件而言的隐藏性。 public(公开) 这种类型或类型成员是完全可访问的。对于枚举类型(参见“枚举”一节) 和接口成员可以隐式访问。 internal(内部) 指配件 A中的类型或类型成员只能在A的内部访问。对于非嵌套类型而言是 默认的,因此可以省略。 private(私有) 类型 T 中的类型成员只能在 T 内部访问,对于类和结构成员而言是默认的, 可以省略。 protected(保护) 指类 C 中的类型成员只能在 C 或 C 派生的类中访问。 protected internal(内部保护) 指类 C 和配件 A 中的类型成员只能从 C、C 派生的类或 A 中访问,注意 C# 中 没有 protected 且 internal 的概念,即不存在类 C 和配件 A 中的类型成 员,只能从 A 中的类 C 或 C 派生的类访问的情形。 注意类型成员可以是嵌套类型。下面是使用访问修饰字的例子: // Assembly1.dll using System; public class A { private int x=5; public void Foo() {Console.WriteLine (x);} protected static void Goo() {}C# 语言参考 49 protected internal class NestedType {} } internal class B { private void Hoo () { A a1 = new A (); // 正确 Console.WriteLine(a1.x); // 错误,A.x 是 private A.NestedType n; // 正确,A.NestedType 是 internal A.Goo(); // 错误,A 的 Goo 是 protected } } // Assembly2.exe (引用 Assembly1.dll) using System; class C : A { // C 默认为 internal static void Main() { // Main 默认为 private A a1 = new A(); // 正确 a1.Foo(); // 正确 C.Goo(); // 正确,继承 A 的 protected static 成员 new A.NestedType(); // 正确,A.NestedType 是 protected new B(); // 错误,Assembly 1 的 B 是 internal Console.WriteLine(x); // 错误,A 的 x是 private } } 访问修饰字的限制 一个类型或类型成员自我声明的访问性不能比在声明中使用的类型更大。例如, 一个类不能是 public的,如果它从 internal的类派生而来;一个方法,如果 其参数对于配件是 internal 的,则不能为 protected 的。这项限制的原理是, 可访问实际上等效于可用。 而且,访问修饰字不能用于继承修饰字相冲突的时候。例如,一个 virtual(或 abstract)成员不可声明为private,因为它不可能被覆盖。类似地,一个密封 类不能定义新的保护成员,因为没有类可对其访问。 最后,为维持一个基类的合约( contract,译注3),带 override修饰字的函数成 员必须与它覆盖的 virtual 成员的可访问性相同。 译注 3: 合约指的是类所提供的与其客户相同的行为和状态。第二章50 类和结构 类的声明语法: 属性信息? 访问修饰字? new?[ abstract | sealed ]? class 类名[ :基类 | :接口 + | :基类,接口 +]? { 类成员} 结构的声明语法: 属性信息? 访问修饰字? new? struct 结构名[: 接口 +]? { 结构成员} 类或结构将数据、函数和嵌套类合并成一个新的类型,这是 C#程序的关键构件。 类或结构的主体由三种成员组成:数据、函数和类型。 数据成员 包括字段、常量、事件。最常见的数据成员是字段。事件是一种特例,因为 它们把类或结构中的数据和操作合二为一了(参见“事件”一节)。 函数成员 包括方法、属性、索引器( indexer)、运算符、构造器和析构器。注意,所 有函数成员要么是特殊化( specialized)的方法类型,要么是用一个或多个 特殊化的方法类型实现的。 类型成员 包括嵌套类型。类型可以嵌套以控制可访问性(参见“访问修饰字”一节) 。 下面是一个例子:C# 语言参考 51 class ExampleClass { int x; // 数据成员 void Foo() {} // 函数成员 struct MyNestedType {} // 类型成员 } 类和结构的区别 类和结构的区别如下: ● 类是一种引用类型;结构是一种值类型。因此,结构通常表示简单类型,可 以使用值的语法(如,赋值将复制值而不是引用)。 ● 类完全支持继承(参见前面“继承”一节) 。结构由 Object 继承,是隐含 密封的类和结构都可以实现接口。 ● 类可以有析构器,而结构没有。 ● 类可以定制无参数构造器并初始化实例字段;而结构不能。结构的默认无参 数构造器用默认值(实际是 0)初始化每个字段。如果结构声明构造器,所 有字段必须在构造器调用中赋值。 实例和静态成员 数据成员和函数成员可以是实例(默认)或静态成员。实例成员与一个类型的实 例相关联,而静态成员与类型本身相关联。而且,对类型以外的静态成员调用, 需要指定类型名。下例中,实例方法 PrintName。打印一个Panda实例的名字, 而静态方法 PrintSpeciesName打印程序( AppDomain)中所有Panda共享的名 字。 class Panda { string name; static string speciesName = "Ailuropoda melanoleuca"; // 初始化 Panda public Panda(string name) { this.name = name; }第二章52 public void PrintName() { Console.WriteLine(name); } public static void PrintSpeciesName() { Console.WriteLine(speciesName); } } class Test { static void Main() { Panda.PrintSpeciesName(); // 调用静态方法 Panda p = new Panda("Petey"); p.PrintName(); // 调用实例方法 } } 字段 属性信息? 访问修饰字? new? static? readonly? 类型[字段名[= 表达式]?]+ ; 字段用于保存一个类或结构的数据。字段也可以指成员变量: class MyClass { int x; float y = 1, z = 2; static readonly int MaxSize = 10; ... } 顾名思义,readonly(只读)修饰字保证字段不能在赋值和修改。这种字段称为 只读字段。只读字段的赋值总是在运行时计算。如要编译,非只读字段必须在其 声明或类型的构造器中赋值(参见“实例构造器”一节) 。非只读字段未赋值时会 产生一个警告。C# 语言参考 53 常量 常量的语法是: 属性信息? 访问修饰字? new? const 类型[ 常量名 = 常量表达式 ]+; 其中类型必须为以下预定义类型:sbyte,byte,short,ushort,int, uint, long,ulong,float,double,decimal,bool,char,string 或 enum。 constant(常量)是在编译时计算的字段,隐含为静态的。因此常量不能在方法 或构造器中计算,只能是几种预定义(内置)类型(参见前面的语法定义)之一。 public const double PI = 3.14159265358979323846; 常量的优点是,因为在编译时计算,编译器可以进行附加的优化。例如: public static double Circumference(double radius) { return 2 * Math.PI * radius; } 实际计算时等价于: public static double Circumference(double radius) { return 6.2831853071795862 * radius; } 用常量进行版本协调 readonly 字段不能由编译器进行优化,但它是有更好的版本升级性。例如,假 设常量 PI有错误,而微软发布了对包含 Math类的库的补丁,同时 Math类已经 部署在每台客户机器上了。如果一台客户机器上已部署了 circumference 方法 (计算周长),只有在重新附带Math类的最新版本编译程序之后,错误才能纠正, 如果使用 readonly 字段,此错误在下次客户程序执行时,会自动纠正。通常,第二章54 这种情形发生在字段值不是因错误而改变,而只是因为升级,如常量MaxThreads (最大线程数)的值由 500 变为 1000。 属性 属性信息? 访问修饰字? [override | new? [virtual | abstract | static]?]? unsafe? 类型 属性名 { [ 属性信息? get 语句块 | // 只读 属性信息? set 语句块 | // 只写 属性信息? get 语句块 // 读写 属性信息? set 语句块 | ] } 抽象访问器并不指定实现,因此它们用分号代替了语句块。参考“访问修饰 字的限制”一节。 属性(property)可以看成是一个面向对象字段。它通过允许类或结构控制对其 数据访问,并隐藏数据的内部表示,提高了封装性。例如: public class Well { decimal dollars; // 私有字段 public int Cents { get { return(int)(dollars * 100); } set { // 在 set 中值是一个隐式变量 if (value>=0) // 典型的验证代码 dollars = (decimal)value/100; } } } class Test { static void Main() { Well w = new Well(); w.Cents = 25; // set int x = w.Cents; // get w.Cents += 10; // get 和 setC# 语言参考 55 } } 访问器 get 返回属性类型的值。访问器 set 拥有属于属性类型的隐含参数值。 注意: 许多语言松散地用 get 或 set 方法惯例实现属性,事实上 C# 的属性会编译成 get_xxx 或 set_xxx 方法,这是它们在 MSIL 中的表示。例如: public int get_Cents {...} public void set_Cents (int value) {...} 简单属性访问器由 JIT(just-in-time compiler,即时编译器)内联,这意味着属 性访问和字段访问没有性能差异。内联是一种优化措施,用方法主体代替方法调 用。 索引器(indexer) 属性信息? 访问修饰字? [override | new?[ virtual | abstract ]? ]? unsafe? 类型 this [ 属性信息? [类型 参数]+ ] { 属性信息? get 语句块 | // 只读 属性信息? set 语句块 | // 只写 属性信息? get 语句块 // 读写 属性信息? set 语句块 | } 抽象访问器并不指定实现,因此它们用分号代替了语句块。参考“访问修饰 字的限制”一节。 索引器提供了一种自然的方式,在封装了集合的类和结构中索引元素,语法是使 用数组类型的一对方法括号[]。例如: public class ScoreList { int[] scores = new int [5]; // 索引器第二章56 public int this[int index] { get { return scores[index]; } set { if(value >= 0 && value <= 10) scores[index] = value; } } // 属性(只读) public int Average { get { int sum = 0; foreach(int score in scores) sum += score; return sum / scores.Length; } } } class IndexerTest { static void Main() { ScoreList sl = new ScoreList(); sl[0] = 9; sl[1] = 8; sl[2] = 7; sl[3] = sl[4] = sl[1]; System.Console.WriteLine(sl.Average); } } 类型可以声明多个索引器,接受不同的参数。 注意: 索引器将编译成 get_Item (...)或 set_Item (...)方法,在 MSIL 中的表 示为: public Story get_Item (int index) {...} public void set_Item (int index, Story value) {...} 方法(method) 方法的声明语法为:C# 语言参考 57 属性信息? 访问修饰字? [ override | new? [ virtual | abstract | static extern? ]? ]? unsafe? [ void | type ] 方法名 (参数列表) 语句块 参数列表的语法为: [ 属性信息?[ref | out]? 类型 参数 ]* [ params 属性信息? 类型[] 参数 ]? 注意: abstract 和extern 方法不含方法主体。可参考前面“访问修饰字的 限制”一 节。 所有C#代码都是以方法或方法的特殊形式(构造器、析构器和运算符都是方法的 特殊类型,属性和索引器在内部以 get/set 方法实现)。 签名 方法的签名以方法的参数列表中每个参数的类型和修饰字为特征。参数修饰字 ref 和out 允许参数按引用传递,而不是按值传递。 按值传递参数 默认值,C#的参数是按值传递的,这也是最常见的情况。这意味着值的拷贝是在 传递给方法时创建的: static void Foo(int p) {++p;} static void Main() { int x = 8; Foo(x); // 生成值类型 x 的拷贝 Console.WriteLine(x); // x 仍然是 8 }第二章58 赋给 p 一个新值并没有改变 x 的内容,因为属性和 x 保存在不同的内存位置。 ref 修饰字 为了按引用传递,C#提供了参数修饰字ref。使用这个修饰字可以使属性和 X指 向同一内存位置: static void Foo(ref int p) {++p;} static void Test() { int x = 8; Foo(ref x); // 将 x 的引用传给 Foo Console.WriteLine(x); // x 现在是 9 } 现在,赋给 p 一个新值改变了 X 的内容。这通常是要按引用传递参数的原因,虽 然偶然传递大的结构时也很有效。请注意在方法调用中和方法声明中是必需的, 这样可以非常清晰,因为参数修饰字改变了方法的签名(参见“签名”一节)。 out 修饰字 C# 规定变量在使用前必须赋值,因此提供了 out 修饰字,作为 ref 修饰字的自 然补充。ref修饰字要求变量在传递给方法之前必须赋值,同样, out修饰字要求 变量在从方法返回时必须赋值: using System; class Test { static void Split(string name, out string firstNames, out string lastName) { int i = name.LastIndexOf(' '); firstNames = name.Substring(0, i); lastName = name.Substring(i+1); } static void Main() { string a, b; Split("Nuno Bettencourt", out a, out b); Console.WriteLine("FirstName:{0}, LastName:{1}", a, b); } }C# 语言参考 59 params 修饰字 params修饰字可以使用在方法的最后一个参数上,这样方法就可以接受任意数 目的某种类型的参数。例如: using System; class Test { static int Add(params int[] iarr) { int sum = 0; foreach(int i in iarr) sum += i; return sum; } static void Main() { int i = Add(1, 2, 3, 4); Console.WriteLine(i); // 10 } } 重载方法 类型可以重载方法(即有许多同名方法) ,只要签名不同即可。例如,下列方法可 以在同一类型中共存: void Foo(int x); viod Foo(double x); void Foo(int x, float y); void Foo(float x, int y); void Foo(ref int x); void Foo(out int x); 但是,以下几对方法不能在同一个类型中共存,因为 return类型和params修 饰字不能作为方法的签名限定。 void Foo(int x); float Foo(int x); // 编译错 void Goo (int[] x); void Goo (params int[] x); // 编译错第二章60 运算符 可重载运算符: + - ! ~ ++ -- + - *(仅用于二进制)/ % &(仅用于二进制) | ^ << >> ++ != > < >= <=== 兼做可重载运算符的直接量: true false C#可以重载运算符,以处理定制类或结构等运算子。 运算符在重载之前是一个带 关键字 operator的静态方法(而不是方法名) ,参数代表运算子,返回代表表达 式结果的类型。 实现值相等 最常见的重载运算符是 == 和!=,用于实现值相等,与引用相等相对。如果两个 对象值相同,它们就是相等的,即使它们的内存位置不同。 在下面的例子中,将重载虚拟的 Equals 方法,将其功能定向为“ ==”运算符。 这样一个类以后可以作为它的一个基类对待(参见“多态性”一节) ,也可以测试 值相等。它还提供与其他不能重载运算符的 .NET 语言的兼容性。 class Note int value; public Note(int semitonesFromA) { value = semitonesFromA; } public static bool operator ==(Note x, Note y) { return x.value == y.value; }C# 语言参考 61 public static bool operator !=(Note x, Note y) { return x.value != y.value; } public override bool Equals(object o) { if(!(o is Note)) return false; return this ==(Note)o; } } Note a = new Note(4); Note b = new Note(4); Object c = a; Object d = b; // 按引用比较 a和 b Console.WriteLine((object)a ==(object)b; // false // 按值比较 a和 b Console.WriteLine(a == b); // true // 按引用比较 c和 d Console.WriteLine(c == d); // false // 按值比较 c和 d Console.WriteLine(c.Equals(d)); // true 成对的逻辑运算符 C# 编译器规定必须定义成对的逻辑运算符。包括“ ==”和“!=”;“<”和“ >”; “<=”和“>=”。 定制隐式和显式转换 我们在前面“类型”一节中解释过,隐式转换的前提,是必须保证成功而且转换 中不丢失信息。相反,显示转换必须在运行时环境中决定转换是否成功,信息是 否丢失。下例中,我们定义了 Note(音符)类型和 double(代表音符频率,单 位为 Hz)之间的转换: ... // 转换为 Hz第二章62 public static implicit operator double(Note x) { return 440*Math.Pow(2,(double)x.value/12); } // 从 Hz 转换而来(精确到半音) public static explicit operator Note(double x) { return new Note((int)(0.5+12*(Math.Log(x/440)/Math.Log(2)))); } ... Note n =(Note)554.37; // 显示转换 double x = n; // 隐式转换 三状态逻辑运算符 关键字 true 和 false 可以用作运算符,定义三状态逻辑定义。这些类型可以无 缝地与逻辑结构[即 if,do,while,for 和条件( ?:)语句]合作。 System. Data.SQLTypes.SQLBoolean 结构提供了此功能: public struct SQLBoolean ... { ... public static bool operator true(SQLBoolean x) { return x.value == 1; } public static bool operator false(SQLBoolean x) { return x.value == -1; } public static SQLBoolean operator !(SQLBoolean x) { return new SQLBoolean(- x.value); } public bool IsNull { get { return value == 0;} } ... } class Test { void Foo(SQLBoolean a) { if (a) Console.WriteLine("True"); else if (! a) Console.WriteLine("False"); elseC# 语言参考 63 Console.WriteLine("Null"); } } 间接可重载运算符 运算符 &&和||可以自动用 & 和 | 计算,因此无需重载。运算符 []可以用索引器 定制(参见“索引器”一节) 。赋值运算符 =不能重载,但其他所有赋值运算符可 以自动用其相应的二进制运算符计算(如,+= 可从 + 计算)。 实例构造器 属性信息? 访问修饰字? unsafe? 类名 (参数列表) [ :[base | this] (参数列表) ]? 语句块 构造器的实例,可用来在类或结构实例化时指定执行代码。类构造器首先在堆 (heap)中创建一个类的新实例,然后进行初始化,而结构构造器仅进行初始化。 与普通方法不同,构造器的名字与声明它的类和结构相同,但没有返回类型: class MyClass { public MyClass() { // 初始化代码 } } 类或结构可以重载多个构造器,还可以在用 this关键字调用方法主体时,调用 其中的一个: class MyClass { public int x; public MyClass() : this(5) {} public MyClass(int v) { x = v;第二章64 } } MyClass m1 = new MyClass(); MyClass m2 = new MyClass(10); Console.WriteLine(m1.x) // 5 Console.Writeline(m2.x) // 10; 如果类不定义任何构造器,就创建一个隐式的无参数构造器。结构不能这样,因 为总要隐式定义一个构造器,用默认值(实际上是 0)初始化每个字段。 调用基类构造器 类构造器必须首先调用一个基类。当基类有无参数构造器时,隐式调用该构造器。 当基类仅提供需要参数的构造器时,派生类构造器必须显式调用一个基类构造器, 使用 base 关键字。构造器还可以调用重载构造器(由它调用基类构造器): class B { public int x ; public B(int a) { x = a; } public B(int a, int b) { x = a * b; } // 注意B 的构造器需要参数 } class D : B { public D() : this(7) {} // 调用一个重载构造器 public D(int a) : base(a) {} // 调用一个基类构造器 } 字段初始化的顺序 初始化的另一种方式是在声明时给字段赋初值: class MyClass { int x = 5; } 字段赋值应在构造器执行之前进行,而且按其显示的顺序初始化。在对于继承链C# 语言参考 65 中的每个类而言,其每个字段的赋值应在构造器执行之前进行,按派生的先后顺 序。 构造器访问修饰字 类或结构可以为构造器选择任何访问修饰字。将构造器指定为私有的,以防止类 被构造,有时会很有用。这对完全由静态成员组成的工具类(如 System.Math) 是非常合适的。 静态构造器 属性信息? static 类名 () 语句块 静态构造器允许初始化代码在类或结构的第一个实例创建之前,或类或结构的静 态成员被访问之前执行。类或结构可以仅定义一个静态构造器,而且必须是无参 数的,名字与类或结构相同。 class Test { static Test() { Console.WriteLine("Test Initialized"); } } 基类构造器的顺序 与实例构造器一致,静态构造器也遵循继承链,因此也应按派生的先后顺序调用。 静态字段初始化顺序 与实例字段一致,每个静态字段赋值在静态构造器调用之前进行,而且字段按其 显示的顺序初始化: class Test {第二章66 public static int x = 5; public static void Foo() {} static Test() { Console.WriteLine("Test Initialized"); } } 访问 Test.x 或 Test.Foo,把它赋给 x,输出“Test Initialized”。 静态构造器调用的不确定问题 静态构造器不能显示调用,在首次使用前,运行时环境可以调用它们。程序无法 知道静态构造器的调用时间。下例中, “Test Initialized”可能在“Test2 Initialized”之后输出。 class Test2 { public static void Foo() {} static Test() { Console.WriteLine("Test2 Initialized"); } } Test.Foo(); Test2.Foo(); 自引用 C# 提供了用于访问类自身或其派生类成员的关键字:this 和 base。 this this关键字表示一个变量,是某个类或结构实例的引用,只能从类或结构的非静 态函数成员中访问。this还可由构造器用来调用一个重载构造器(参见“实例构 造器”一节) ,或用来声明或访问索引器(参见“索引器”一节) 。this 变量的普 通用法是将字段名或参数名区分开: class Dude { string name; public Test(string name) {C# 语言参考 67 this.name = name; } public void Introduce(Dude a) { if (a!=this) Console.WriteLine("Hello, I'm "+name); } } base base与this类似,只不过它访问的是一个覆盖或隐藏的基类函数成员。base还 可以调用基类构造器(参见“实例构造器”一节) ,或者访问基类索引器(使用 base 代替this)。调用 base也可以访问定义该成员的上级派生类。在上例基础 上,可以创建下例: class Hermit : Dude { public void new Introduce(Dude a) { base.TalkTo(a); Console.WriteLine("Nice Talking To You"); } } 注意: C# 中没有 C++ 作用域解析运算符“ ::”这样访问基类实例成员的方式。 析构器和终结器 属性信息? ~ 类名 () 语句块 C# 类可以声明析构器。声明一个 C# 析构器就是声明 Finalize 方法(称为终结 器)的语法简写,将被编译器扩展为以下方法声明: protected override void Finalize() { ... base.Finalize();第二章68 } 虽然 C# 终结器 / 析构器看起来与 C++ 析构器语法上很相似,但它们实际上区别 很大,记住这一事实非常重要:C# 终结器 / 析构器与 C++ 析构器的行为迥异。 因此我们建议,不要声明C#析构器或者依赖编译器生成终结器,在需要时直接声 明一个 Finalize 方法好了。 终结器是只针对类的方法,用于帮助清除非内存资源,通常由无用资源回收器在 清除无用内存前调用。 关于无用资源回收器和终结器的更多细节,参见第三章中的“自动内存管理”。 内嵌类型 内嵌类型( nested type)是指在另一个类型作用域内声明的类型。内嵌一个类型 有三点好处: ● 可以访问其外围类型的所有成员,不用成员的访问修饰字。 ● 可以用类型成员访问修饰字与其他类型隔离。 ● 从其外围类型之外访问内嵌类型,需要指定类型名(与静态成员相同)。 下面是一个内嵌类型的例子: using System; class A { int x = 3; // 私有成员 protected internal class Nested {// 选择任何访问级别 public void Foo () { A a = new A (); Console.WriteLine (a.x); // 可以访问 A 的私有成员 } } } class B { static void Main () {C# 语言参考 69 A.Nested n = new A.Nested (); // Nested 的作用域在 A 内 n.Foo (); } } // 在一个类型声明中使用 "new" 的例子 class C : A { new public class Nested {} // 隐藏已继承类型成员 } 注意: C# 中的内嵌类近似等效于Java中的静态内部类。但 C#中没有 Java非静态内部 类的对等物,其中内部类有一个外围类实例的引用。 接口 属性信息? 访问修饰字? new? interface 接口名 [ : 基接口 + ]? { 接口成员 } 接口(interface)与类有些相似,但有以下主要区别: ● 接口只是指定成员,但并不实现。这与纯抽象类相似,抽象类只包含抽象成 员。 ● 类或结构可以实现多个接口,而类只能从一个类继承。 ● 结构可以实现一个接口,但结构不能从类继承。 在“类与结构”一节中, 我们将多态性定义为在许多类型上执行相同操作的能力, 只要这些类型有相同的功能子集。接口的目的正是为了定义这个功能子集 (译注 4)。 译注 4:在《 C# Language Specification》中,接口的定义是“定义合约的语法结构” 。所谓 合约( contract)就是类或结构保证支持的方法、属性、事件等等,也就是本书此处 所说的“功能子集” 。第二章70 接口包括一或多个方法、属性、索引器和事件。这些成员隐含时总是公开的和抽 象的(所以也是虚拟的和非静态的)。 定义一个接口 接口声明与类相同,但它不提供其成员的实现,因为所有成员都是隐含抽象的。 这些成员通过实现接口的类或结构来实现。 下例中,接口定义了一个方法: public interface IDelete { void Delete(); } 实现接口 实现接口的类或结构可以说成是“履行了接口的合约” 。下例中,支持删除的 GUI 控件,如 TextBox 或TreeView,或你自己的定制 GUI 控件,可以实现 IDelete 接口: public class TextBox : IDelete { public void Delete() {...} } public class TreeView : IDelete { public void Delete() {...} } 如果类从基类继承而来,每个要实现的接口的名字必须在基类名后出现: public class TextBox : Control, IDelete {...} public class TreeView : Control, IDelete {...} 使用接口 当需要多个类共享公共基类没有的功能时,接口非常有用。而且,接口在确保这 些类为其接口成员提供自己的实现时,是一个好办法,因为接口成员是隐含抽象 的。C# 语言参考 71 下面的例子假设一个表单包含许多 GUI控件,包括一些 TextBox和TreeView控 件,其中当前聚焦控件用 ActiveControl 属性访问。当用户点击菜单项或工具 栏按钮上的Delete时,看一看 ActiveControl是否实现了IDelete,如是,将 它转换为 IDelete 调用其 Delete 方法。 class MyForm { ... void DeleteClick() { if (ActiveControl is IDelete) { IDelete d = (IDelete)ActiveControl; d.Delete(); } } } 扩展接口 接口还可以扩展其他接口。例如: ISuperDelete : IDelete { bool CanDelete {get;} event EventHandler CanDeleteChanged; } 在实现ISuperDelete接口时,ActiveControl控件实现了CanDelete属性,表 示它要进行删除且不是只读的。同时控件还实现了CanDeleteChanged事件以便 在 CanDelete 属性改变时激活一个事件。此框架允许程序在 ActiveControl不 能删除时,重现(ghost)其 Delete 菜单项和工具栏按钮。 显式的接口实现 如果接口成员和类或结构的已有成员发生名字冲突,C#允许显式地实现一个接口 成员以解决此问题。下例中,我们来解决在两个接口都定义了 Delete 方法时发 生的名字冲突: public interface IDesignTimeControl { ...第二章72 object Delete(); } public class TextBox : IDelete, IDesignTimeControl { ... void IDelete.Delete() {...} object IDesignTimeControl.Delete() {...} // 注意:显式实现一个就能解决问题 } 与隐式接口实现不同,显示接口实现不能用 abstract,virtual,override 或 new等修饰字声明。而且,它们隐含就是 pulic的,而隐式实现必须使用 public 修饰字。但是,为访问方法,类或结构必须先转换成合适的接口: TextBox tb = new TextBox(); IDesignTimeControl idtc = (IDesignTimeControl)tb; IDelete id = (IDelete)tb; idtc.Delete(); id.Delete(); 重实现接口 如果基类用virtual(或abstract)修饰字实现了接口成员,派生类就可以覆盖 它。如果没有实现,派生类必须重实现此接口以覆盖成员: public class RichTextBox : TextBox, IDelete { // TextBox 的 Idelete.Delete 不是 virtual 的 // (因为显式接口实现不能是 virtual 的) public void Delete() {} } 此例中的实现可以将 RichTextBox 对象用作一个 IDelete 对象,并调用 Rich- TextBox 版本的 Delete。 接口转换 类或结构 T 可以隐式转换为 T 所实现的接口 I。类似的,接口 X 可以隐式地转换 为 X 所继承的接口 Y。接口可以隐式地转换为其他接口或非密封类。但是,接口 I 到密封类或结构 T 的显式转换,只有在 T 可以实现 I 时才是允许的。例如:C# 语言参考 73 interface IDelete {...} interface IDesigntimeControl {...} class TextBox : IDelete, IDesignTimeControl {...} sealed class Timer : IDesignTimeControl {...} TextBox tb1 = new TextBox (); IDelete d = tb1; // 隐式转换 IDesignTimeControl dtc = (IDesignTimeControl)d; TextBox tb2 = (TextBox)dtc; Timer t = (Timer)d; // 非法,Timer 不能实现 IDelete 标准装箱转换在结构和接口之间转换时进行。 数组 类型 [*]+ 数组名 = new 类型 [ 维数 + ][*]*; 注:[*]指集合[][,][,,] ... 数组用来将一组特定类型的元素存在相邻的内存块中。数组类型从System.Array 派生而来,在 C# 中用方括号对([])声明。例如 char[] vowels = new char[] {'a','e','i','o','u'}; Console.WriteLine(vowels [1]); // 输出 "e" 前面的函数调用将输出“ e”,因为数组索引从 0 开始。为了支持其他语言, .NET 可以以任意索引初始值创建数组,但 BCL库总是使用从 0开始的索引。一是数组 创建,其长度就无法再改变。但 System.Connection 类可以提供动态大小的数 组及其他数据结构,如关联数组(键 / 值)(参见第三章“集合”一节)。 多维数组 多维数组可分为两种:规则数组(rectangular array)和不规则数组(jagged array)。 规则数组表示一个 n 维块,不规则数组是数组的数组: // 规则数组 int [,,] matrixR = new int [3, 4, 5]; // 创建 1个立方体 // 不规则数组第二章74 int [][][] matrixJ = new int [3][][]; for (int i = 0; i < 3; i++) { matrixJ[i] = new int [4][]; for (int j = 0; j < 4; j++) matrixJ[i][j] = new int [5]; } // 赋值一个元素 matrixR [1,1,1] = matrixJ [1][1][1] = 7; 局部和字段数组声明 为方便起见,局部和字段声明在数组类型中赋值时可以省略,因为类型可在声明 中指定: int [,] array={{1,2},{3,4}}; 数组的长度和阶数 数组有一定的长度。对于多维数组方法,数组的 GetLength方法返回给定维的元 素个数,从 0(最远端)到阶数 -1(最近端): // 一维 for(int i = 0; i < vowels.Length; i++); // 多维 for(int i = 0; i < matrixR.GetLength(2); i++); 边界检测(Bounds Checking) 所有数组的索引操作都由CLR进行边界检测,对于非法索引将抛出 IndexOut- OfRangeException异常。与 Java中一样,边界检测可以避免程序错误和调试时 遇到困难,同时使代码可以在安全限制中执行。 注意: 通常边界检测对性能的影响很小,JIT可以进行优化,比如在进入循环前就决定 每个数组索引是否安全,以避免每次迭代都要检测。而且 C#提供了不安全代码 (unsafe code),可以明确地绕过边界检测(参见“不安全代码和指针”一节) 。C# 语言参考 75 数组转换 引用类型的数组可以转换为其他数组,使用其元素的类型所适用的方法(这称为 数组的协变性,array covariance)。所有数组都实现 System.Array,后者为任 何数组类型都提供了一般性的 get 和set 元素。 枚举 属性信息? 访问修饰字? new? enum 枚举名 [ : 整数类型 ]? { [属性信息? 枚举成员名 [ = 值 ]? ]* } 枚举是一组已命名的数字常数,比如: public enum Direction {North, East, West, South} 与 C 中不同,枚举成员时必须带上枚举的类型名。这消除了名字冲突,使代码更 清晰: Direction walls = Direction.East; 默认时,枚举是已赋值的整数常数 0,1,2 等。你可以选择是指定另一个数字类 型作为枚举的基础,还是显式地为每个枚举成员指定值: [Flags] public enum Direction : byte { North=1, East=2, West=4, South=8 } Direction walls = Direction.North | Direction.West; if((walls & Direction.North) != 0) System.Console.WriteLine("Can't go north!"); [Flags]属性信息是可选的。它用来告诉运行时环境,枚举中的值可以按位结合, 可以在调试程序中或将文本输出到控制台时相应地解码。例如:第二章76 Console.WriteLine(walls.Format()); // 显示 "North|West" Console.WriteLine(walls); // 调用 walls.ToString,显示“ 5” System.Enum类型也提供了许多有用的静态方法用于枚举,可以确定枚举的底 层类型,检测是否支持特定值,从一个字符串常数初始化枚举,获取一个合法值 列表,以及其他公用的操作如转换,等等。例如: using System; public enum Toggle : byte { Off=0, On=1 } class TestEnum { static void Main() { Type t = Enum.GetUnderlyingType(typeof(Toggle)); Console.WriteLine(t); // 输出 "Byte" bool bDimmed = Enum.IsDefined(typeof(Toggle), "Dimmed"); Console.WriteLine(bDimmed); // 输出 "False" Toggle tog =(Toggle)Enum.FromString(typeof(Toggle), "On"); Console.WriteLine(tog); // 输出 "1" Console.WriteLine(tog.Format()); // 输出 "On" object[] oa = Enum.GetValues(typeof(Toggle)); foreach(Toggle tog in oa) // 输出 "On=1, Off=0" Console.WriteLine("{0}={1}", tog.Format(), tog); } } 枚举运算符 与枚举相关的运算符有: == != < > <= & >= + - ^ | -= += = ++ ~ sizeof --C# 语言参考 77 枚举转换 枚举可以显式地转换成其他枚举。枚举和数字类型可以显式地互相转换。特例是 数字直接量 0,可以隐式地转换为枚举。 委托(delegate) 属性信息? 访问修饰字? new? delegate [ void | 类型 ] 委托名 (参数列表); 委托是一种用来定义方法签名的类型,因此委托实例可以拥有和调用一个方法, 或与其签名匹配的一组方法。委托声明由名字和方法签名组成。委托方法的签名 包括其返回类型,并允许在其参数列表中使用参数修饰字,扩展描述普通方法签 名特征的元素列表。目标方法的实际名与委托是无关的。如下例: delegate bool Filter(string S); 这个委托语句将创建一些委托实例,它们可以拥有和调用方法,这些方法返回逻 辑值,并有一个字符串参数。在下例中,创建了一个拥有 FirstHalfOfAlphabet 方法的 Filter。然后将其传给 Display 方法,由后者调用 Filter: class Test { static void Main() { Filter f = new Filter(FirstHalfOfAlphabet); Display(new String [] {"Ant","Lion","Yak"}, f); } static bool FirstHalfOfAlphabet(string s) { return "N".CompareTo(s) > 0; } static void Display(string[] names, Filter f) { int count = 0; foreach(string s in names) if(f(s)) // 调用委托第二章78 Console.WriteLine("Item {0} is {1}", count++, s); } } 组播委托(multicast delegate) 如果委托的返回类型为 void,它就是一个组播委托,可以拥有和调用多个方法。 下例中,我们声明一个简单委托MethodInvoker,它可以拥有并按顺序调用Foo 和 Goo方法。“+=”方法通过将右边的委托运算数与左边的相加,创建一个新委 托。 delegate void MethodInvoker(); class Test { static void Main() { new Test(); // prints "Foo","Goo" } Test() { MethodInvoker m = null; m += new MethodInvoker(Foo); m += new MethodInvoker(Goo); m(); } void Foo() { Console.WriteLine("Foo"); } void Goo() { Console.WriteLine("Goo"); } } 委托还可以用“-=”运算符从另一个委托中删除。例如: Test { MethodInvoker m = null; m += new MethodInvoker(Foo); m -= new MethodInvoker(Foo); // m 现在为 null } 委托按其相加的顺序调用。注意,对委托的“ +=”和“ -=”操作都是线程安全的。C# 语言参考 79 注意: 为了与 .NET 运行时环境协同工作,C#将对委托的“ +=”和“ -=”操作编译成 System.Delegate 类的静态Combine和Remove方法。返回类型为 void的委托 是 System.MulticastDelegate 的别名。而返回类型不为 void 的委托是(单 播)System.Delegate 的别名,因为它不能从多个方法返回值。 委托与函数指针的区别 委托在行为上与 C 的函数指针(或 Delphi的闭包)很像。但委托可以拥有多个方 法,以及与每个非静态方法相关联的实例。而且,同其他所有用于不安全块之外 的C#结构一样,它是类型安全和访问安全的,也就是说,可以防止你指向没有访 问权限的错误的方法类型或错误的方法。 委托与接口的比较 用委托能解决的问题也能用接口解决。下面是一个用 IFilter接口解决 Filter问 题的例子: interface IFilter { bool Filter(string s); } class Test { class FirstHalfOfAlphabetFilter : IFilter { public bool Filter(string s) { return ("N".CompareTo(s) > 0); } } static void Main() { FirstHalfOfAlphabetFilter f = new FirstHalfOfAlphabetFilter(); Display(new string [] {"Ant", "Lion", "Yak"}, f); } static void Display(string[] names, IFilter f) { int count = 0; foreach (string s in names) if (f.Filter(s)) Console.WriteLine("Item {0} is {1}", count++, s); } }第二章80 当然用委托处理更漂亮一些,但委托最好用于事件处理上。 事件(event) 事件处理本质上是一个对象通知其他对象发生了某事件的一个过程。这个过程通 常由组播委托所封装,后者内置有这种能力。 事件定义委托 .NET 框架定义了许多事件处理委托,但也可以自己编写。例如: delegate void MoveEventHandler(object source, MoveEventArgs e); 按习惯,事件委托的第一个参数表示事件来源,第二个参数由System.EventArgs 派生而来,并保存了有关事件的数据。 用 EventArgs 存储事件信息 可以从 EventArgs 派生,以包括与特定事件相关的信息: public class MoveEventArgs : EventArgs { public int newPosition; public bool cancel; public MoveEventArgs(int newPosition) { this.newPosition = newPosition; } } 声明和激发一个事件 类或结构可以通过对委托字段使用事件修饰符,声明一个事件。下例中, Slider 类有一个 Position 属性,可以在任何时候其位置改变时激发一个 Move 事件: class Slider {C# 语言参考 81 int position; public event MoveEventHandler Move; public int Position { get { return position; } set { if (position != value) { // 如果位置改变 if (Move != null) { // 如果调用列表不为空 MoveEventArgs args = new MoveEventArgs(value); Move(this, args); // 激发事件 if (args.cancel) return; } position = value; } } } } event 关键字通过确保只有“+=”和“-=”运算符能在委托上执行,来进行封 装。这意味着其他类可以自我注册为接收到了事件通知,但只有 Slider类可以 调用委托(激发事件),或清空委托的调用列表。 用事件处理器处理事件 可以通过在事件上增加事件处理器来处理事件。事件处理器是一种委托,它包裹 了在激发事件时想调用的方法。 在下例中,我们想让 Form 处理 Slider 的 Position 的发生的变化。可以通过 创建包裹了事件处理方法 Slider,Move 的 MoveEventHandler委托实现。此委 托将被加入 Move 事件的事件处理器列表(开始是空的) 。改变 Slider 对象的 位置激发 Move 事件,它将调用 Slider.Move 方法: class Form { static void Main() { Slider slider = new Slider(); // 注册 Move 事件 slider.Move += new MoveEventHandler(slider_Move); slider.Position = 20;第二章82 slider.Position = 60; } static void slider_Move(object source, MoveEventArgs e) { if(e.newPosition < 50) Console.WriteLine("OK"); else { e.cancel = true; Console.WriteLine("Can't go that high!"); } } } 通常可以增加 Slider 类以生成 Move(移动)事件,无论何时其 Position(位 置)被鼠标移动、按键或其他用户动作所改变。 注意: 为了与.NET运行时环境协同工作,事件字段将addOn_xxx和removeOn_xxx方 法增加到带存根( stub)代码(用于访问实际上是类或结构私有的委托字段)的 类或结构。 事件属性 事件属性的语法如下: 属性信息? 访问修饰字? new? static? event 委托类型 事件属性名 属性信息? get 语句块 属性信息? set 语句块 } 虽然用事件修饰字改变委托字段很方便,但也可能不起作用。例如,一个有 100 个事件的类可以存储100个委托字段,即使这些事件中通常只有4个会被赋值。相 反你可以将这些委托存在散列表这样的集合中,使用属性而不是字段来暴露事件: public event MoveEventHandler Move {C# 语言参考 83 get { return (MoveEventHandler)myEventStorer["Move"]; } set { myEventStore ["Move"] = value; } } 注意: Beta2 版的事件属性使用 add/remove 成员。 try 语句和异常 try 语句块 [catch(异常类型值 ?)? 语句块]+ | finally 语句块 | [catch(异常类型值 ?)? 语句块]+ finally 语句块 try 语句 try语句的用途是在异常的环境下简化程序执行的处理。try语句可以完成两件 事。一是在 try 代码块的执行被 catch 代码块捕获时,抛出异常。二是保证没 有首先执行finally代码块时程序执行不离开try代码块。try代码块后面必须 跟有一个或多个 catch 代码块,或一个 finaly 代码块,或者两者都有。 异常(exception) C#异常是指包含表示异常程序状态发生信息的对象。当异常状态发生时(如一个 方法收到了非法值),将抛出异常对象,调用堆栈(call stack)将保持打开直至 异常被异常处理代码块所捕获。如下例所示: public class File {第二章84 ... public static StreamWriter CreateText(string s) { ... if (!Valid(s)) throw new IOException("Couldn't create...", ...); ... } } class Test { ... void Foo(object x) { StreamWriter sw = null; try { sw = File.CreateText("foo.txt"); sw.Write(x.ToString()); } catch(IOException ex) { Console.WriteLine(ex); } finally { if(sw != null) sw.Close(); } } } catch catch子句用于指定要捕获的异常类型(包括派生类型) 。异常必须是 System. Exception 类型,或由其派生的类型。 忽略异常变量 仅指定异常类型而不指定变量名,可以允许异常在无需使用异常实例的时候被捕 获,只要知道基类型就行了。上面的例子可以这样重写: catch(IOException) { // 不指定变量 Console.WriteLine("Couldn't create the foo!"); }C# 语言参考 85 捕获 System.Exception 为了捕获所异常,catch子句必须忽略 catch表达式。这只是为了捕获 System. Exception 而采用的一种语法缩写,因为 System.Exception 是所有异常的基 类。 catch { Console.WriteLine("Couldn't create the foo!"); } 指定多个 catch 子句 当声明多个catch子句时,只有第一个异常类型与已抛出的异常匹配的子句会执 行 catch代码块。如果 B 是 D 的基类,则常类型 B 先于异常类型 D 是非法的,因 为 B 不能达到。 try {...} catch (NullReferenceException) {...} catch (IOException) {...} catch {...} finally finally 块总是在控制离开 try 代码块时执行。finally 块在下列时候执行: ● 在 try 代码块完成和立即执行; ● 在 try代码块因跳转语句(如 return,goto)预先退出之后和在跳转语句 的目标之前立即执行; ● 在 catch 代码块执行后立即执行。 finally 块也是一种决定程序的方式,可以确保某代码总能执行。 在我们的范例主程序中,如果创建 foo.txt 时出了问题,会抛出 IOException,需 要执行Catch代码块,接下来是一个finally块。如果x是null,调用x.ToString()第二章86 抛出NullReferenceException。Catch子句不捕捉这种类型的异常,但finally 块依然会执行。这确保了你使用的文件能在退出 Foo 之前关闭。 System.Exception 类的关键属性 可能最常用的 System.Exception 的属性有: StackTrace 这是一个字符串表示从异常起点到 catch 代码块调用的所有方法。 Message 这是一个错误描述字符串。 InnerException 有时候捕获异常,然后抛出一个新的更具体的异常,非常有用。例如,可以 捕获一个 IOException,然后抛出一个 ProblemFooingException,它包 含有关错误的更具体的信息。这种情况下, ProblemFooinException 应该 包含 IOException,作为其构造器的 InnerException 参数,赋值给 InnerException 属性。这种层叠级的异常结构对调试尤其有用。 注意: C# 中所有异常都是运行时异常;没有与 Java 中编译时异常等效的东西。 属性信息 [[目标:]? 属性信息名 ( 位置性参数 + | [已命名参数 = 表达式]+ | 位置性参数 +,[已命名参数 = 表达式]+)?] 属性信息(attribute)是一种用附加信息修饰语言元素(配件,模块,类型,成 员,返回值和参数)的语言结构。在任何语言中,都需要指定类型、方法、参数C# 语言参考 87 和其他程序元素相关的信息。例如,类型可以指定一组派生接口,参数可以用ref 这样的修饰字指定自己的值如何传递。这种方法的局限是,只能用语言本身提供 的预定义结构。 有了属性信息,程序员可以为代码元素增加许多类型的信息。例如, .NET框架中 的序列化方式可以使用应用于类型和字段的各种序列化属性信息来定义。这比要 求语言有特殊的序列化语法更灵活。 Attribute 类 属性信息是由(直接或间接)从抽象类 System.Attribute 继承的类所定义的。 在指定一个元素的属性信息时,属性信息名是类型的名字。按习惯派生的类型名 以“Attribute”结尾,但这种后缀不是必需的。 本例中我们指定,Foo 类可用 Serializable 属性信息序列化: [Serializable] public class Foo {...} Serializeble属性信息实际上是在System名字空间中声明一个类型,如下所示: class SerializableAttribute : Attribute {...} 还可以用全限定类型名指定 Serializable 属性信息如下所示: [System.SerializableAttribute] public class Foo {...} 这两个例子在语法上是完全等效的。 注意: C# 语言和 BCL 包含许多预定义属性信息。关于 BCL 中其他属性信息以及如何 创建自定义属性信息的信息,参见第三章中“定制属性信息”一节。第二章88 属性信息的参数 属性信息可以带参数,从而指定仅仅属性信息所能表示以外的代码元素的附加信 息。 下例中,用 Obsolete属性信息指定类Foo是过时的。这个属性信息可以带参数, 指定编译器是否应将此类的用法当成错误: [Obsolete("Use Bar class instead", IsError=true)] public class Foo {...} 属性信息的参数分为两类:位置性( positional)参数和已命名( named)参数。 上例中,Use Bar class instead 是位置性参数,而 IsError=true 是已命名 参数。位置性参数对应的是传递给属性信息类型的公共构造器的参数。已命名参 数则对应于属性信息类型的公共读写或只写实例属性和字段的集合。 当指定元素的属性信息时,位置性参数是必需的,已命名参数则是可逆的。 因为在指定属性信息时使用的参数,是在编译时计算的,它们通常都仅限于常量 表达式。 属性信息的目标 属性信息的目标隐含是紧跟着它的代码元素。但有时也需要明确地指定属性信息 所应用的目标。在 C# 编译器 beta 1 版中,合法的目标是配件和模块。未来版本 中,显式的目标列表中将增添参数、返回值等等。 下面使用 CLSCompliant 属性信息指定整个配件的 CLS 兼容级别: [assembly:CLSCompliant(true)] 指定多个属性信息 可以为一个代码元素指定多个属性信息。每个属性信息列于一对方括号中(之间 用逗号隔开),或者分别列于几对方括号中,或两者的结合。C# 语言参考 89 因此,以下三个例子在语法上是等效的: [Serializable, Obsolete, CLSCompliant(false)] public class Bar {...} [Serializable] [Obsolete] [CLSCompliant(false)] public class Bar {...} [Serializable, Obsolete] [CLSCompliant(false)] public class Bar {...} 不安全代码和指针 C#支持在标记为不安全的代码块中,通过指针直接操作内存。指针类型主要用于 与 C 语言 API的互操作,但也可以用于访问受管制的堆以外的内存,或用于性能 极为重要的场合。 指针类型 对于C#程序中每个值类型或指针类型V而言,有一个对应的 C#指针类型V*。指 针实例存有值的地址。此值可以认为属于类型 V,但指针类型可以(不安全地)转 换为其他任何指针类型。表 2-3 总结了 C# 语言所支持的主要指针运算符。 表 2-3 主要的指针运算符 运算符含义 & 取址(address-of)运算符返回指向值的地址的指针 * 析值(dereference)运算符返回指针地址处的值 -> 指针-成员( pointer-to-member)运算符是一个语法缩写,x->y等价于 (*x).y第二章90 不安全代码 方法,语句块或单个语句可以用 unsafe关键字标记,以执行 C++ 风格的内存指 针运算。下例使用指针处理一个受管制的对象: unsafe void RedFilter(int[,] bitmap) { const int length = bitmap.Length; fixed (int* b = bitmap) { int* p = b; for(int i = 0; i < length; i++) *p++ &= 0xFF; } } 不安全代码通常运行得比对应的安全实现代码要快,后者此时必须要有带数组索 引和边界检测的嵌套循环。不安全 C# 方法比调用外部 C 函数也要快,因为没有 与离开管制执行环境相关的系统开销。 fixed 语句 fixed ([值 类型 | void ]* 名字 = [&]? 表达式) 语句块 fixed语句必须用于固定受管制对象,比如上一个指针实例中的 bitmap。在程 序执行期间,许多对象从堆中分配和释放。为了避免不必要地浪费或内存出现碎 片,无用资源回收器会移动对象。指向某对象是无效的,如果在引用时其地址已 经变化,因此要使用fixed语句告诉无用资源回收器固定对象而不要移动它。这 会影响运行时效率,因此 fixed代码块要尽量简短,而且在 fixed 块中应避免 堆分配。 C#仅从值类型而从不直接从引用类型返回指针。数组和字符串是例外情况, 但只 是语法上的例外,因为它们实际上返回的是指向其第一个元素必须为值类型的指 针,而不是对象本身。 引用类型中声明的值类型必须将引用类型固定,如下所示:C# 语言参考 91 class Test { int x; static void Main() { Test test = new Test(); unsafe { fixed(int* p = &test.x) { // 固定 Test *p = 9; } System.Console.WriteLine(test.x); } } } “->”运算符 除了 & 和 * 运算符之外,C# 提供了 C++ 风格的“->”运算符,可用于结构: struct Test { int x; unsafe static void Main() { Test test = new Test(); Test* p = &test; p->x = 9; System.Console.WriteLine(test.x); } } stackalloc 关键字 可以显式地用 stackalloc 关键字分配堆栈中的内存块。因为是在堆栈中分配, 其生命期就是使用它的方法的执行时间,同其他局部变量一样。内存块应使用 “[]”索引,但只是一个值类型,没有附加的描述信息或数组所提供的边界检测: int* a = stackalloc int [10]; for (int i = 0; i < 10; ++i) Console.WriteLine(a[i]); // 输出原始内存第二章92 不受管制代码的指针 指针还可用于访问受管制堆之外的数据,如在与 C 的 DLL 或 COM交互时,或处 理主内存之外的数据(如显存或嵌入设备的存储介质)的时候。 预处理指令 预处理指令( preprocessor directive)为编译器提供了有关代码区域的附加信息。 最常用的预处理指令是条件指令,提供了编译中应包含或排除的代码区域的信息。 例如: #define DEBUG class MyClass { int x; void Foo() { # if DEBUG Console.WriteLine("Testing: x = {0}", x); # endif ... } 在这个类中,Foo 中的语句是有条件编译的,是否编译取决于由用户选择的 DEBUG符号的存在。如果删除 DEBUG,语句就不编译。预处理符号可以像上面这 样在源代码中定义。它们将以 /define:symbol 命令行选项的形式传给编译器。 所有预处理符号默认为基,因此上面的 #define 语句等效于: #define DEBUG true #error和#warning符号用于防止条件指令的偶然误用,方法是让编译器在给定 的编译符号集是不应出现的时,产生警告或错误。 预处理指令 C# 语言支持的预处理指令如表 2-4 所示。C# 语言参考 93 表 2-4 预处理指令 预处理指令 动作 #define symbol 定义 symbol #undef symbol 解除定义 symbol #if symbol [operator 要测试的 symbol;operator 指跟在 #else,#elif, symbol2]... #endif后的: ==, !=, &&, || #else 执行 #endif 后的代码 #elif symbol #else 分支与 #if 测试的结合 [operator symbol2] #endif 条件指令的结束 #warning text text 指在编译器输出中的警告文字 #error text text 指在编译器输出中的错误信息 #line number [file] number 用于指定源代码中的行号; file 是计算机输出的文件名 #region name 标记概述区的开始 #end region 概述区的结束 XML 文档 C# 提供了三种不同风格的源代码文档:单行注释,多行注释和文档注释。 C/C++ 风格的注释 单行和多行注释使用 C++ 语法:“/”和“/*⋯ */”: int x = 3; // 这是一条注释 MyMethod(); /* 这是一条 跨了两行的注释 */ 这种风格注释的不利之处在于,没有记录类型的预定标准。因此,不容易解析而第二章94 自动生成文档。C#对此做了改进,可以在源代码中嵌入文档注释,提供了一种编 译时抽取和验证文档的自动化机制。 文档注释 文档注释与C#单行注释类似,但以“ ///”开头,可以应用于任何用户定义的类 型或成员。这些注释包括内嵌 XML 标签和描述文本。其中标签可以用来标记描 述文本,从而更好地定义类型或成员的语法,也可以更好地引入互相参考。 于是这些注释可以在编译时提取成一个单独的文档输出文件。编译器验证注释是 否内在一致,将互相参考扩展为全限定类型 ID,并输出良构( well-formed)的 XML文件。进一步处理就靠程序员自己了,虽然通常下一步是通过XSLT由XML 生成 HTML 文档。 下例是一个简单类型的文档: // Filename: DocTest.cs using System; class MyClass { /// /// The Foo method is called from /// Main /// /// Secret stuff /// Description for s static void Foo(string s) { Console.WriteLine(s); } static void Main() { Foo("42"); } } XML 文档文件 当上面的源文件以命令行选项“ /doc:”经过编译器之后,生成下面 的 XML 文件: C# 语言参考 95 DocTest The Foo method is called from Main Secret stuff Description for s 标签是自动生成的,构成了 XML文件的骨架。 标签表示此XML文件所在的配件。文档注释后的每个成员 都通过一个 标签包含在 XML文件中,并带有标识成员的名字属性信 息。注意 标签中的cref#属性信息也被扩展了,指向全限定类型和成员。 文档注释中嵌入的预定义XML文档标签也包含在这个XML文件中。标签已经过 验证,确保记录了所有参数、名字的正确性以及相互参考可以解析。最后,任何 附加的用户定义标签会被原样不动地传递过来。 预定义 XML 标签 本节列出了可用来标记描述文本的预定义 XML 标签集: 描述 描述 这个标签描述类型或成员。通常 包含简短的概述, 包含完整的描述。 描述 此标签描述了一个方法的参数。name属性信息是必需的,而且必须指的是第二章96 方法的一个参数。如果此标签用于一个方法的任何参数,则所有参数都需要 记录下来。而且要注意,必须用双引号把 name 括起来。 描述 此标签描述了一个方法的返回值。 [cref="type"]描述 此标签描述了一个方法可以抛出的异常。如存在,可选的 cref属性信息应 指向异常的类型。必须用双引号把类型名字括起来。 描述 此标签描述一个类型或成员所需的权限。如存在,可选的 cref属性信息应 指向代表成员所需权限集的类型,虽然编译器并不对此进行验证。必须用双 引号把类型名字括起来。 描述 代码 代码 这些标签提供了解释类型或成员用法的描述和范例源代码。通常 标签提供描述,并包含 标签,虽然它们也可以独立使用。如 果需要包含一行的代码片断,使用 ;如果包含多行代码片断,使用 文本 文本 这两个标签用于在文档中标识指向其他类型或成员的相互参考。通常 标签用于描述中的一行内,可分成独立的“See Also(亦可参考)” 段。这两个标签在工具生成文档的相互参考、索引和超文本链接形式时,非 常有用。成员名必须用双引号括起来。C# 语言参考 97 描述 此标签用于描述类的属性。 此标签标识了参数的用法。参数名 name 必须用双引号括起来。 name 描述 name 描述 文本 这些标签为文档生成程序提供了格式化文档的方式线索。 用户定义标签 C#编译器所认可的预定义XML标签没有什么太特别的,而且你还可以自己定义 标签。编译器唯一特殊处理的是 标签(它验证参数名并确认方法的所 有参数都已记录)和 cref属性信息(它验证属性信息指向真实存在的类型或成 员,并将其扩展为全限定类型或成员 ID)。cref属性信息还可以用在自定义标签 中,验证和扩展与预定义标签 等相同。 类型或成员的交叉参考 类型名和类型或成员交叉参考将被转换成唯一定义类型或成员的 ID。这些 ID由 一个前缀(定义 ID所代表的意义)和类型或成员的签名组成。表 2-5列出了类型 和成员前缀。第二章98 表 2-5 XML 类型 ID 的前缀 前缀 意义 N 名字空间 T 类型(类,结构,枚举,接口,委托) F 字段 P 属性(包括索引器) M 方法(包括特殊方法) E 事件 !错误 签名的生成规则有很好的文档说明,虽然相当复杂。 下面是一个生成类型和 ID 的例子 // 名字空间没有独立签名 namespace NS { // T:NS.MyClass class MyClass { // F:NS.MyClass.aField string aField; // P:NS.MyClass.aProperty short aProperty {get {...} set {...}} // T:NS.MyClass.NestedType class NestedType {...}; // M:NS.MyClass.X() void X() {...} // M:NS.MyClass.Y(System.Int32,System.Double@,System.Decimal@) void Y(int p1, ref double p2, out decimal p3) {...} // M:NS.MyClass.Z(System.Char[],System.Single[0:,0:]) void Z(char[] p1, float[,] p2) {...} // M:NS.MyClass.op_Addition(NS.MyClass,NS.MyClass) public static MyClass operator+(MyClass c1, MyClass c2) {...} // M:NS.MyClass.op_Implicit(NS.MyClass)~System.Int32 public static implicit operator int(MyClass c) {...} // M:NS.MyClass.#ctor MyClass() {...} // M:NS.MyClass.Finalize ~MyClass() {...}C# 语言参考 99 // M:NS.MyClass.#cctor static MyClass() {...} } }100 第三章 .NET 框架编程 大多数现代编程语言都包含某种形式的运行时库,提供公用服务,访问底层操作 系统和硬件。有很多例子,从简单的功能库,如 C 和 C++ 使用的 ANSI C运行时 库,到 Java 运行时环境提供的丰富的面向对象的类库。 与 Java 程序依赖 Java 类库和虚拟机的方式相同,C# 程序也依赖于 .NET 框架中 的服务,如基类库(BCL)和公用语言运行时环境(CLR)。 关于 BCL 的高级概述,请参见第四章。 本章处理创建C#程序时需要执行的最常见的任务。主题大致分为两类:使用BCL 中的功能,和如何与 BCL 的元素相互作用。 公用类型 在BCL 中有一些类型无处不在,因为它们是 BCL和CLR 运作和提供整个BCL库 所用的公用功能的基础。 本节讲述其中最常用的一些以及它们的用法。本节提到的类型都在 System名字 空间中。 .NET 框架编程 101 Object 类 System.Object 类是类层次的根,也是其他所有类的基类。 C# 的 Object类型 是 System.Object的别名。System.Object提供了一些所有对象都具有的方法 其签名如下 System.Object 类定义的片断所示: public class Object { public Object() {...} public virtual bool Equals(object o) {...} public virtual int GetHashCode(){...} public Type GetType(){...} public virtual string ToString() {...} protected virtual void Finalize() {...} protected object MemberwiseClone() {...} } Object(Object O) Object 基类的构造器。 Equals() 此方法计算两个对象是否等效。 默认实现按引用比较对象,因此类可以以按值比较覆盖此方法。 C# 中,还可以覆盖“= =”和“!=”运算符。更多信息参见第二章“类与 结构”一节中的“实现值相关”部分。 GetHashCode() 此方法允许对象提供自己的用于集合中的散列函数。 此函数返回值应传递两个测试:(1)代表同一值的两个对象应该返回同样的 散列码;(2)返回值应在运行时生成随机分布。 GetHashCode的默认实现实际上不能满足以上标准,因为它只能根据对象引 用返回一个值。因此,应该在除自己的类型中覆盖此方法。 有关散列码如何被预定义集合类所用的更多信息,参见本章后面的“集合” 一节。第三章102 GetType() 此方法用于访问代表对象类型的 Type对象。不能由用户的类型实现。有关 Type 类型和反射的总体信息,参见后面的“反射”一节。 ToString() 此方法提供一个代表对象的字符串,通常用于调试。 此方法的默认实现只返回类型的名字,在用户自己的类型中应被覆盖,以返 回代表对象的有意义的字符串。预定义类型如 int和 string,都覆盖此方 法以返回值,如下所示: using System; class Beeblebrox {} class Test { static void Main() { string s = "Zaphod"; Beeblebrox b = new Beeblebrox(); Console.WriteLine(s); // 输出 "Zaphod" Console.WriteLine(b); // 输出 "Beeblebrox" } } Finalize() Finalize 方法清除非内存资源,通常由无用资源回收器在回收对象内存之 前调用。Finalize 方法可以在任何引用类型上被覆盖,但很少用到。有关 终结器和无用资源回收器的讨论参见后面的“自动内存管理”一节。 MemberwiseClone() 此方法创建对象的浅( shallow)复制,不能由用户的类型实现。有关如何控 制类型的浅 / 深(deep)复制的语法,参见下面“ICloneable 接口”一节。 创建与 BCL 友好的类型 要创建能与BCL良好协作的新类型,应该适当地覆盖以上的一些方法。其中有些 覆盖与 C# 可覆盖的运算符类似。 下面是一个新的值类型的例子,该类型与 BCL 协调: .NET 框架编程 103 public class Point3D { public int x, y, z; public Point3D(int x, int y, int z) { this.x=x; this.y=y; this.z=z; // 初始化数据 } public override bool Equals(object o) { if (!(o is Point3D)) // 检测类型的相等性 return false; return (this==(Point3D)o); // 通过运算符 == 实现 } public static bool operator !=(Point3D lhs, Point3D rhs) { return (!(lhs==rhs)); // 通过运算符 == 实现 } public static bool operator ==(Point3D lhs, Point3D rhs) { return ((rhs.x==lhs.x) && (rhs.y==lhs.y) && (rhs.z==lhs.z)); } public override int GetHashCode(){ return x^y^z; } public override string ToString() { return String.Format("[{0},{1},{2}]", x, y, z); } } 此类覆盖了 Equals,运算符“ ==”和“ !=”,以提供基于值的相等语法,然后创 建了遵循上述规则的一个散列码,同时覆盖了 ToString 以方便调试。 这个类可以这样使用: using System; using System.Collections; public class Point3D {...} class TestPoint3D { static void Main() { // 使用 ToString,输出 "p1=[1,1,1] p2=[2,2,2] p3=[2,2,2]" Point3D p1 = new Point3D(1,1,1); Point3D p2 = new Point3D(2,2,2); Point3D p3 = new Point3D(2,2,2); Console.WriteLine("p1={0} p2={1} p3={2}, p1, p2, p3); // 测试相等性,说明 Equals,== 和!= 的用法 int i = 100; Console.WriteLine(p1.Equals(i)); // 输出"False"第三章104 Console.WriteLine(p1==p2); // 输出 "False" Console.WriteLine(p2==p3); // 输出 "True" // 使用一个 Hashtable 存储点(使用 GetHashCode) Hashtable ht = new Hashtable(); ht["p1"] = p1; ht["p2"] = p2; ht["p3"] = p3; // 输出 "p2=[2,2,2] p3=[2,2,2] p1=[1,1,1]" foreach (DictionaryEntry de in ht) Console.Write("{0}={1}", de.Key, de.Value); } } ICloneable 接口 public interface ICloneable { object Clone(); } ICloneable 用于克隆类或结构的实例。包含一个称为 Clone 的方法,返回实例 的副本。实现此接口时,Clone 方法可以只返回 this.MemberwiseClone(),进 行浅复制(且按复制字段),也可以进行定制的深复制,这样可以分别复制类或结 构中的字段。以下代码是最简单的实现: public class Foo : ICloneable { public object Clone() { return this.MemberwiseClone(); } } IComparable 接口 interface IComparable { int CompareTo(object o); } IComparable 是由拥有可排序实例的类型(参见后面“集合”一节)实现的。包 含一个方法,名为 CompareTo,它的返回值情况如下: .NET 框架编程 105 ● 如果实例 <0 返回- ● 如果实例 >0 返回 + ● 如果实例 ==0 返回 0 此接口由所有数值类型,string,DateTime来实现。也可以由提供比较语法的定 制类或结构实现。例如: using System; using System.Collections; class MyType : IComparable { public int x; public MyType(int x) { this.x = x; } public int CompareTo(object o) { return x -((MyType)o).x; } } class Test { static void Main() { ArrayList a = new ArrayList(); a.Add(new MyType(42)); a.Add(new MyType(17)); a.Sort(); foreach(MyType t in a) Console.WriteLine(((MyType)t).x); } } IFormattable 接口 public interface IFormattable { string Format(string format, IServiceObjectProvider sop); } IFormattable由这样的类型实现,它们拥有格式化选项,可以将其值转化为字符 串。例如,一个 decimal 值可以转化为 string 表示的货币值,或一个用逗号做 小数点的 string 值。格式化选项由“格式化字符串”指定(参见“格式化字符第三章106 串”一节) 。如果有IServiceObjectProvider 接口,则它将指定用于转换的具 体文化背景。 IFormattable接口通常在调用String类的一个Format方法时使用(参见后面 “字符串”一节)。 所有的公用类型(int,string,DateTime 等)都实现了这个接口。如果需要 String 类完全支持自定义类型,应该在格式化时实现这个接口。 数学 C# 和 BCL 提供了丰富的功能,使面向数学的编程更加容易、高效。 本节讲述几个适用于数学编程的最常见的类型,以及如何创建新的数学类型。本 节提到的类型均在 System 名字空间中。 数学的语言支持 C#有许多有用的数学功能,甚至可以创建定制数学类型。 运算符重载使定制数学 类型(如复数和向量)可以很自然地使用。二维数组提供了表达矩阵的快速简易 的方式。还可以用结构来高效地创建低开销的对象。下面是一些例子: struct Vector { float direction; float magnitude; public Vector(float direction, float magnitude) { this.direction = direction; this.magnitude = magnitude; } public static Vector operator *(Vector v, float scale) { return new Vector(v.direction, v.magnitude * scale); } public static Vector operator /(Vector v, float scale) { return new Vector(v.direction, v.magnitude * scale); } ... .NET 框架编程 107 } class Test { static void Main() { Vector [,] matrix = {{new Vector(1f,2f), new Vector(6f,2f)}, {new Vector(7f,3f), new Vector(4f,9f)}}; for (int i=0; i collection.Count) throw new InvalidOperationException(); return ++currentIndex < collection.Count; } public void Reset () { currentIndex = -1; } } } 集合可以以下列两种方式进行枚举: MyCollection mcoll = new MyCollection(); ... // 使用 foreach: 用你的类型名替换XXX foreach (XXX item in mcoll) { Console.WriteLine(item); ... } // 使用 IEnumerator: 用你的类型名替换 XXX IEnumerator ie = myc.GetEnumerator(); while (myc.MoveNext()) { XXX item = (XXX)myc.Current; Console.WriteLine(item); ... } ICollection 接口 public interface ICollection : IEnumerable { void CopyTo(Array array, int index); int Count {get;} bool IsReadOnly {get;} bool IsSynchronized {get;} object SyncRoot {get;} } ICollection 是所有集合(包括数组)都要实现的接口,它提供了以下方法: .NET 框架编程 117 CopyTo(Arry array, int index) 此方法将所有元素拷贝到数组中,从指定索引处开始。 Count 此属性返回集合元素的数目。 IsReadOnly 此属性返回一个值,表示集合是否能够修改。 IsSynchronized() 此方法可以确定集合是否是线程安全的。BCL中提供的集合本身不是线程安 全的,但都含有 Synchronized 方法,可以返回集合的一个线程安全的 Wrapper。 SyncRoot() 此属性返回一个对象(通常是集合本身) ,可以被锁定,以为集合提供基本 的线程安全支持。 IComparer 接口 public interface IComparer { int Compare(object x, object y); } IComparer是一个标准接口,为数组中排序比较两个对象。通常无需实现流接口, 因为 Comparer类型已提供了一个使用 IComparable 接口的默认实现,Array 类型使用的就是这种方式。 IList 接口 public interface IList : ICollection, IEnumerable { object this [int index] {get; set} int Add(object o); void Clear(); bool Contains(object value); int IndexOf(object value); void Insert(int index, object value);第三章118 void Remove(object value); void RemoveAt(int index); } IList 是可以索引数组集合(如 Arraylist)的接口。 IDictionary 接口 public interface IDictionary : ICollection, IEnumerable { object this [object key] {get; set}; ICollection Keys {get;} ICollection Values {get;} void Clear(); bool Contains(object value); IDictionaryEnumerator GetEnumerator(); void Remove(object key); } IDictionary 是用于键值为基础的(如 Hashtable 和 SortedList)的接口。 IDictionaryEnumerator 接口 public interface IDictionaryEnumerator : IEnumerator { DictionaryEntry Entry {get;} object Key {get;} object Value {get;} } IDictionaryEnumerator 是枚举字典内容的一个标准化接口。 IHashCodeProvider 接口 public interface IHashCodeProvider { int GetHashCode(object o); } IHashCodeProvider 是 Hashtable 集合用来散列存储对像的标准接口。 .NET 框架编程 119 正则表达式 BCL引入了对正则表达式匹配和替换能力的支持。这些表达式以 Per15 regexp为 基础,包括松散定量符( lazyquantifier)(??,*?,+?,{n,m}?),正负前瞻 (lookahead),以及各种计算。 本节提到的类型都存在 System.Text.RegularExpression 名字空间。 Regex 类 Regex类是BCL对正则表达式支持的核心。它可用作对象的实例和静态类型。表 示一个正则表达式的可变,编译过的实例,可通过匹配过程度应用于一个字符串。 从内部看,正则表达式可存为内部正则表达式字节码,在匹配时解释,或存为已 编译的 MSIL 操作码,由 CLR 在运行时即时(JIT)编译。这样可以在较差的正 则表达式启动时间和内存消耗,与运行时较好的原始匹配性能之间做权衡。 更多的正则表达式选项,所支持的字符转义,替代模式,字符集,定位断言,定 量器,分组结构,反向引用和选择,参见附录二“正则表达式”。 Match 和 MatchCollection 类 Match 类用于表示将正则表达式应用于字符串,寻找第一个成功的匹配结果。 MatchCollection类包含了一组 Match的实例的集合,表示将反复正则表达式 应用于字符串,直至出现第一个成功匹配的结果。 Group 类 代表一个分组表达式的结果。由这个类,可以接触到带 Captures属性的子表达 式匹配。第三章120 Capture 和 CaptureCollection 类 CaptureCollection 类包括一组Capture 实例的集合,每个实例都是代表一个 子表达式匹配的结果。 使用正则表达式 将以上类结合起来,你可以创建如下的例子: /* * 此例说明如何用多个 Capture 对多个组进行分组 * 可以这样编译链接此例: * csc /r:System.Text.RegularExpressions.dll test.cs */ using System; using System.Text.RegularExpressions; class Test { static void Main() { string text = "abracadabra1abracadabra2abracadabra3"; string pat = @" ( # start the first group abra # match the literal 'abra' ( # start the second (inner) group cad # match the literal 'cad' )? # end the second (optional) group ) # end the first group + # match one or more occurences "; Console.WriteLine("Original text = ["+text+"]"); Regex r = new Regex(pat, "x"); // 使用 'x' 修饰字忽略注释 int[] gnums = r.GetGroupNumbers(); // 获取组号列表 Match m = r.Match(text); // 获取第一个匹配 while (m.Success) { for (int i = 1; i < gnums.Length; i++) // 从组1 开始 { Group g = m.Group(gnums[i]); // 获取这一匹配的组 Console.WriteLine("Group"+gnums[i]+"=["+g.ToString()+"]"); CaptureCollection cc = g.Captures; // 获取这一组的 Capture .NET 框架编程 121 for (int j = 0; j < cc.Count; j++) { Capture c = cc[j]; Console.WriteLine("Capture" + j + "=["+c.ToString() + "] Index=" + c.Index + " Length=" + c.Length); } } m = m.NextMatch(); // 获取下一匹配 } } } 上例的输出如下: Original text = [abracadabra1abracadabra2abracadabra3] Group1=[abra] Capture0=[abracad] Index=0 Length=7 Capture1=[abra] Index=7 Length=4 Group2=[cad] Capture0=[cad] Index=4 Length=3 Group1=[abra] Capture0=[abracad] Index=12 Length=7 Capture1=[abra] Index=19 Length=4 Group2=[cad] Capture0=[cad] Index=16 Length=3 Group1=[abra] Capture0=[abracad] Index=24 Length=7 Capture1=[abra] Index=31 Length=4 Group2=[cad] Capture0=[cad] Index=28 Length=3 输入 / 输出 BCL 提供了一个基于流的 I/O 框架,可以处理各种流和后备存储类型。这种对流 的支持在 BCL的其他部分也到处可见,如非 I/O领域如加密,HTTP支持及其他。 本节讲述的核心的流类型,并提供相应的实例。 本节中所提到类型均在 System.I/O 名字空间中。第三章122 流和后备存储 流(stream)表示进出后备存储的数据流。而后备存储( backing store)则表示 流的起始端点。虽然后备存储经常是文件和网络连接,实际上它可以代表任何一 个能够读写原始数据的介质。 使用流从磁盘文件中读写是一个简单的例子,但是,流和后备存储并不限于磁盘 和网络 I/O。更复杂的例子是使用 BCL 中的加密支持,将经过内存的字节流加密 或解密。 抽象流类 Stream是一个抽象类,定义了按字节读写原始无类型数据流的操作。流一旦打 开,就会保持并可读写,直至流被刷新( flush)或关闭。关闭流首先要进行刷新, 然后关闭。 对于支持顺序访问的流,Stream 的方法包括 CanRead,CanWrite 和 CanSeek。 如果支持随机访问,还有 SetPosition 方法可用于移动到流的某个线性位置。 Stream类提供同步和异步读写操作。默认情况下,异步方法调用流的同步对应 的同步方法,通过将同步法包裹在一个委托类型中,并启动一个新线程。类似的, 在默认情况下,同步方法调用流对应的异步方法,并等待,直至线程完成其操作。 Stream派生的类必须重载同步或异步方法,但如需要也可以同时重载两类方法。 流派生的具体类 BCL包含抽象基类Stream的许多不同的具体实现。每个实现代表不同的存储媒 介,允许一个原始字节流从 / 到后备存储读写。 这些实现的例子包括FileStream类(从一到文件中读写)和 NetworkStream类 (通过网络发送和接收字节)。 另外,流可以作为另一个流的前端,按需要执行底层流上的其他处理。例子包括 流的加密 / 解密和流的缓冲。 .NET 框架编程 123 下面是一个例子,在磁盘上创建一个文本文件,并用抽象File类型向文件写数据: using System.IO; class Test { static void Main() { Stream s = new FileStream("foo.txt", Filemode.Create); s.WriteByte("67"); s.WriteByte("35"); s.Close(); } } 封装原始流 Stream类定义了以字节形式读写原始无类型数据的操作。但通常你要处理的是 字符流而非字节流。为了解决这一问题, BCL 提供了抽象基类 TextReader 和 TextWriter 定义了读写字符流的操作,以及一组具体实现。 抽象的 TextReader/TextWriter 类 TextReader 和 TextWriter 是抽象基类,定义了读写字符流的操作。这两个类 最基本的操作是从 / 到流读写的一个字符的方法。 TextReader类提供了默认的方法实现,用于读入一个字符数组或代表一行字符 的一个字符串。TextWriter类提供了默认的方法实现,方法实现用于写一个字 符数组或将普通类型(可根据格式选项选择)转化为一个字符序例。 BCL 包括抽象基类 TextReader和TextWriter的许多不同的具体实现。其中最 突出的是 StreamReader与StreamWriter,StringReader与StringWriter。 StringReader 与 StringWriter 类 StringReader 与 StringWriter 是从 TextReader 和TextWriter 分别派生而 来的具体类,用于操作 Stream(作为构造器参数传递)。第三章124 这些类可用来将 Stream(可以有后备存储,但只能处理原始数据)与 TextReader/TextWriter(可处理字符数据,但没有后备存储)结合起来。 而且,StreamReader 和StreamWriter 可以在字符和原始字节之前进行特定的 转换。包括将 Unicode 字符转换为 ANSI 字符,转换为高位在前( big-endian)或 低位在前(little-endian)存储格式。 下面的例子使用包裹了 FileStream 类的 StreamWriter 写入一个文件: using System.Text; using System.IO; class Test { static void Main() { Stream fs = new FileStream ("foo.txt", FileMode.Create); StreamWriter sw = new StreamWriter(fs, Encoding.ASCII); sw.Write("Hello!"); sw.Close(); } } StringReader 与 StringWriter 类 StringReader 与StringWriter 是分别从 TextReader 和TextWriter 派生而来 的具体类,用于操作字符串(作为构造器参数传递)。 StringReader类被认为是最简单的只读后备存储,因为它只执行字符串读操作。 StringWriter类则是最简单的只写后备存储,因为它只执行在StringBuilder 上执行写操作。 下面的例子使用(包裹了底层 StringBuilder后备存储的)StringWriter向字 符串写入: using System; using System.IO; using System.Text; class Test { static void Main() { .NET 框架编程 125 StringBuilder sb = new StringBuilder(); StringWriter sw = new StringWriter(sb); WriteHello(sw); Console.WriteLine(sb); } static void WriteHello(TextWriter tw) { tw.Write("Hello, String I/O!"); } } 目录和文件 File和Directory类封装了与文件I/O有关的操作,如目录和文件的复制、移动、 删除、重命名和枚举。 实际上对文件内容的操作要通过FileStream。File类有返回FileStream的类, 虽然你可以直接实例化 FileStream。 本例中读入并输出命令行所指定的文本文件内容: using System; using System.IO; class Test { static void Main(string[] args) { Stream s = File.OpenRead(args[0]); StreamReader sr = new StreamReader(s); Console.WriteLine(sr.ReadLine()); sr.Close(); } } 联网 BCL 中包含了许多能简化网络资源访问的类型。这些类型提供了不同层次的抽 象,允许程序忽略通常访问网络资源所需的细节,同时保持高度的控制。 本节讲述 BCL中提供的核心联网支持 , 并给出了使用预定义类的许多例子。本节第三章126 提到的类型都在System.Net.RegularExpressions和System.Net.Sockets名 字空间中。 网络编程模型 可以使用实现了通用请求 / 响应结构的一组类型来执行高层访问,其中该结构可 以扩展以支持新的协议。BCL中对此结构的实现还包括HTTP扩展以简化与Web 服务器的交互。 如果程序需要进行底层网络访问,有支持 TCP 和 UDP的类型。最后,在需要直 接传输层访问时,也有类型提供原始套接字访问。 通用请求 / 响应结构 请求 / 响应结构是以 URI(Uniform Resource Indicator,统一资源指示符)和流 I/O 为基础的 , 遵循厂( factory)设计模式( design pattern), 并很好地利用了抽 象类和接口。 厂类型 WebRequestFactory 用于解析 URI 并创建合适的协议处理器完成请求。 协议处理器共享公共的抽象基类( WebRequest),该基类暴露配置请求的属性和 获取响应的方法。 响应也可以用类型表示,并共享公共的抽象基类( WebRequest),该基类暴露一 个 NetworkStream,提供简单的基于流的 I/O,很容易与 BCL 的其他部分集成。 本例是流行的 Unix snarf 工具的一个简单实现。它演示了如何使用 WebRequest 和 WebResponse 类获取一个 URI 的内容,并将其输出到控制台 : // Snarf.cs - 与 /r:System.Net.dll 一起编译 // 运行 Snarf.exe 获取网页 using System; using System.IO; using System.Net; .NET 框架编程 127 using System.Text; class Snarf { static void Main(string[] args) { // 用 WebRequest 获取 URL 处的数据 WebRequest req = WebRequestFactory.Create(args[0]); WebResponse resp = req.GetResponse(); // 读入数据,进行 ASCII->Unicode 编码 Stream s = resp.GetResponseStream(); StreamReader sr = new StreamReader(s, Encoding.ASCII); string doc = sr.ReadToEnd(); Console.WriteLine(doc); // 将结果输出到控制台 } } HTTP 支持 请求 / 响应结构内在地通过使用子类型化支持协议扩展。 因为 WebRequestFactory 基于URI创建和返回正确的协议处理器,访问协议相 关的部件就像将WebRequest对象转换成正确的协议处理器并访问扩展功能一样 容易。 BCL 包含用于 HTTP 协议的特定支持,包括轻松访问和控制交互式 Web 会话的 元素,如 HTTP 首部 ,用户代理字符串,代理支持,用户凭证,验证, keep-alive (保持激活), 管道化及其他。 本例演示了 HTTP 特定的请求 / 响应类以控制用户代理字符串以请求和获取服务 器类型: // ProbeSvr.cs - 与 /r:System.Net.dll 一起编译 // 运行 ProbeSvr.exe < 服务器名 > 获取服务器类型 using System; using System.Net; class ProbeSvr { static void Main(string[] args) {第三章128 // 获取 WebRequest 的实例,转换为 HttpWebRequest WebRequest req = WebRequestFactory.Create(args[0]); HttpWebRequest httpReq = (HttpWebRequest)req; // 访问 HTTP 部件,如 User-Agent httpReq.UserAgent = "CSPRProbe/1.0"; // 获取响应并将其输出到控制台 WebResponse resp = req.GetResponse(); HttpWebResponse httpResp = (HttpWebResponse)resp; Console.WriteLine(httpResp.Server); } } 增加新的协议处理器 增加处理器以支持新的协议很简单:实现新的一组基于WebRequest和WebResponse 的派生类,实现 WebRequest派生类型上的IWebRequestCreate接口,并在运行 时用 WebRequestFactory 将它注册为新的协议处理器。一旦完成,任何使用请 求 / 响应结构的代码可以访问使用新的 URI 格式的联网资源(以及底层协议)。 使用 TCP,UDP 和 Socket System.Net.Sockets 名字空间包含有协议层 TCP 和 UDP支持的类型。这些类 型建立在底层 Socket 类型的基础上,Socket 类型本身可以直接访问传输层。 有两个类提供 TCP 支持:TCPListener 和TCPClient。TCPListener 监听到来 的连接,创建 Socket实例响应连接请求。TCPClient与远程主机连接,将底层 套接字的细节隐藏在一个 Stream 派生的类型中,可以在网络上进行流式 I/O。 有一个类 UDPClient 提供了 UDP 支持。UDPClient 既是客户端,又是监听器 (listener),还支持组播,可以将单个数据报以字节数组的形式进行发送和接收。 TCP和UDP类都有助于访问底层网络套接字(用 Socket类表示)。 Socket类 是本机Windows套接字之上薄薄的包裹层( thin wrapper),而且是受管制代码所 能访问的最底层的联网资源。 .NET 框架编程 129 下例简单实现了 IETF 在 RFC 865 中定义的 QUOTD(Quote of the Day)协议。 它说明了如何使用TCP监听器接收到来的请求,以及如何使用底层 Socket类型 完成请求: // QOTDListener.cs - 与 /r:System.Net.dll 一起编译 // 运行 QOTDListener.exe 响应到来的 QOTD 请求 using System; using System.Net; using System.Net.Sockets; using System.Text; class QOTDListener { static string[] quotes = {@"Sufficiently advanced magic is indistinguishable from technology -- Terry Pratchett", @"Sufficiently advanced technology is indistinguishable from magic -- Arthur C Clarke" }; static void Main() { // 在端口 17 启动 TCP 监听器 TCPListener l = new TCPListener(17); l.Start(); Console.WriteLine("Waiting for clients to connect"); Console.WriteLine("Press Ctrl+C to quit..."); int numServed = 1; while (true) { // 等待到来的套接字连接请求 Socket s = l.Accept(); // 将 quotes 编码为字节用于发送 Char[] carr = quotes[numServed%2].ToCharArray(); Byte[] barr = Encoding.ASCII.GetBytes(carr); // 将数据返回给客户端,清楚套接字,如此反复 s.Send(barr, barr.Length, 0); s.Shutdown(SocketShutdown.SdBoth); s.Close(); Console.WriteLine("{0} quotes served...", numServed++); } } }第三章130 测试此例时,运行监听器,并试图用一个 telnet客户端与本地主机的端口17连接。 (在 Windows 2000 中 , 可以通过在命令行输入:“telnet localhost 17”实 现。) 注意while循环结尾处Socket.Shutdown和Socket.Close的用法。这样就马 上刷新和关闭了套接字,而不是等待以后无用资源回收器来终结和回收不可到达 的 Socket 对象。 使用 DNS BCL 中的联网类型还支持正常和反向 DNS(Domain Name System,域名系统)。 下面是一个使用这些类型的例子: // DNSLookup.cs - 同 /r:System.Net.dll 一起编译 // 运行 DNSLookup.exe 以确定 IP 地址 using System; using System.Net; class DNSLookup { static void Main(string[] args) { IPHostEntry he = DNS.GetHostByName(args[0]); IPAddress[] addrs = he.AddressList; foreach (IPAddress addr in addrs) Console.WriteLine(addr); } } 线程 C#程序总是由一个或多个线程( thread)运行的,线程可以高效地并行执行程序。 下面是一个多线程程序的例子: using System; using System.Threading; class ThreadTest { static void Main() { Thread t = new Thread(new ThreadStart(Go)); .NET 框架编程 131 t.Start(); Go(); } static void Go() { for (char c='a'; c<='z'; c++ ) Console.Write(c); } } 此例中,通过传递一个ThreadStart委托(包裹了指定线程执行起始位置的方法) 来构造线程。然后启动线程,调用 Go,于是有两个线程在并行地运行 Go。但是 此处存在一个问题:两个线程都共享同一个资源:控制台。如果运行ThreadTest, 会得到这样的输出: abcdabcdefghijklmnopqrsefghjiklmnopqrstuvwxyztuvwxyz 线程同步 线程同步主要包括一些技术以确保多个线程能互相协调对共享资源的访问。 lock 语句 C#提供了lock语句确保一次只有一个线程访问某个代码块。思考一下这个例子: using System; using System.Threading; class LockTest { static void Main() { LockTest lt = new LockTest (); Thread t = new Thread(new ThreadStart(lt.Go)); t.Start(); lt.Go(); } void Go() { lock(this) for ( char c='a'; c<='z'; c++) Console.Write(c); } }第三章132 运行 LockTest 将生成以下输出: abcdefghijklmnopqrstuvwzyzabcdefghijklmnopqrstuvwzyz lock 语句将获得了用类型实例的锁。如果另一线程已获得了锁,则该线程不能 继续执行,直至另一线程取消了该实例的锁。 lock 语句实际上是调用 BCL Monitor 类的 Enter 和 Exit 方法(参见后面 “Monitor 类”一节)的语法缩写。 System.Threading.Monitor.Enter(expression); try { ... } finally { System.Threading.Monitor.Exit(expression); } Pulse 和 Wait 操作 与锁相结合最常见的线程操作是Pulse和Wait。这两种操作用于在线程之间通过 监视器通信,其中,监视器维护着一个等待获取对象的锁的线程列表: using System; using System.Threading; class MonitorTest { static void Main() { MonitorTest mt = new MonitorTest(); Thread t = new Thread(new ThreadStart(mt.Go)); t.Start(); mt.Go(); } void Go() { for ( char c='a'; c<='z'; c++) lock(this) { Console.Write(c); Monitor.Pulse(this); Monitor.Wait(this); } } } .NET 框架编程 133 运行 MonitorTest 将得到如下结果: aabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz Pulse方法告诉监视器,一旦当前线程释放了,锁就唤醒等待列表中的下一个线 程。当前线程通常有两种释放方式。第一种是执行离开了阻塞 lock语句。第二 种是调用 Wait方法,暂时释放对象上的锁,并使线程休眠,直至另一个线程通 过 Pulse 唤醒。 死锁 上面的 MonitorTest 例子实际上包含了一种称为死锁(dead lock)的错误。当 你运行程序逻辑时,会得到正确输出,但控制台窗口会锁住。这是因为有两个休 眠线程,互相等待,造成死锁。死锁发生的原因是在输出 z 时,每个线程都进入 休眠,但却不再会被唤醒。你可以用以下的新实现代替 Go 的方法,来解决这个 问题: void Go() { for ( char c='a'; c<='z'; c++) lock(this) { Console.Write(c); Monitor.Pulse(this); if (c<'z') Monitor.Wait(this); } } 通常,使用锁的危险就在于两个线程会同时结束阻塞,等待被第二个线程占用的 资源。大多数常见的死锁可以通过确保以相同顺序获取资源来避免。 原子操作(atomic operation) 原子操作是指系统约定不会被中断的操作。在前面的例子中,方法 Go 不是原子 操作,因为它在运行时可以被中断。但是,更新变量的操作是原子操作,因为此 操作必须保证完成,其间不允许控制传给另一个线程。 InterLocked类提供了其第三章134 他原子操作,允许基础的操作无需锁即不执行。这很有用,因为获取锁比简单的 原子操作慢很多倍。 常见的线程类型 线程的许多功能通过 System.Threading 名字空间中的类提供。最基础的类是 Monitor。按下来我们就讲一讲这个类。 Monitor 类 System.Threading.Monitor类实现了Hoare的监视器,可以将引用类型实例用 作监视器。 Enter 和 Exit 方法 Enter和Exit方法分别用于获取和释放对象上的锁。如果对象已经由另一线程 占用,Enter将等待直至锁被释放,或者线程被 ThreadInterruptedException 异常所中断。对线程上的某个给定对象调用 Enter,应与对同一对象的 Exit调 用匹配。 TryEnter 方法 TryEnter 方法与 Enter 相似,但它不需要获取对象的锁。此方法在锁已获得时 返回 true,来获得时返回 false,可以有选择地传递一个超时参数,指定等待 的最大时间。 Wait 方法 拥有锁的线程可以调用 Wait方法暂时地释放锁并阻塞自己,同时它等待另一个 线程执行 Pulse 方法通知自己。这种方式可以告诉工作线程,对象上有任务要执 行。Wait的重载版本允许指定超时,在指定时间内信号还未到来,就重新激活 线程。当线程唤醒时,它重新获得对象的监视器(可能会被阻塞至监视器可用) 。 .NET 框架编程 135 如果线程是被另一线程产生信号给监视器而重新激活的,Wait返回true,如果 是因为超时而并没有收到信号,返回 false。 Pulse 和 PulseAll 方法 拥有对象的锁的线程,可以对该对象调用 Pulse方法唤醒已阻塞线程,一旦调用 Pulse 的线程已释放了监视器上的锁。如果多个线程正在监视器中等待, Pulse 只激活队列中的第一个(连续调用 Pulse 将唤醒其他等待线程,一次一个) 。 PulseAll 方法则用于陆续唤醒所有线程。 配件 配件( assembly)是一个逻辑包(与 Win32 中的 DLL 相类似) ,包含一个清单 (manifest), 一组模块( module)和一组可选的资源( resource)。这种包构成了 部署和版本协调的基本单位,并为类型解析和安全权限控制创建了边界。 配件的元素 每个 .NET 应用程序都至少包含一个配件,配件反过来又是由许多基本元素组成 的。清单中包含一组运行时环境所需的描述配件信息的元数据。这些信息包括: ● 配件的名字 ● 配件的版本号 ● 共享名(可选)和有符号的配件散列 ● 配件中的文件列表(带文件散列) ● 所引用的配件的列表,包括版本协调信息和公共密钥(可选) ● 配件中类型的列表,并带有包含类型的模块的映射 ● 配件所要求的最小和可选安全权限的集合 ● 配件所明确拒绝的安全权限的集合第三章136 ● 文件,处理器和操作系统信息 ● 获取产品名,所有人信息等细节的定制属性信息集合 模块包含用元数据描述,用 MSIL 实现的类型。 资源包含配件中不可执行的数据。例如位图,可局部代的文本,持久对象,等等。 打包(packaging) 最简单的配件包含一个清单和包含程序类型的一个模块,打包成一个以 Main函 数为入口点的 EXE文件。复杂一些的配件可以包含多个模块( PE文件),单独多 个资源文件,多个清单等。 清单通常包含在配件中一个 PE 文件里,虽然也可以是一个单独的 PE 文件。 模块都是 PE文件,通常是 DLL 或 EXE。配件中没有一个模块能包含一个入口点 (Main,WinMain 或 DLLMain)。 配件也可以包含多个模块。这一技巧可以减小程序的工作集,因为 CLR只装载必 需的模块。而且,每个模块可以用不同语言编写,可以混合使用 C#,VB.NET 和 原始 MSIL。虽然不常见,一个模块也可以包含几个不同的配件中。 最后,配件可以包含一组资源,资源可以保存在独立文件或配件中的一个PE文件 中。 部署 配件是.NET最小的部署单元。因为清单可以自我描述,部署时简单地将配件(在 多文件配件中,是所有相关文件)拷贝到一个目录就行了。 这是对传统 COM开发的极大改进。原来组件及其支持 DLL文件,配置信息要散 布在多个目录和 Windows 注册表中。 .NET 框架编程 137 通常,配件部署在应用程序目录,不能共享。这些配件称为私有配件。但是,配 件也可以由应用程序共享,这时它们称为共享配件。要共享配件,你需要给它起 一个共享名(也称“strong”name),并将它部署在全局配件缓存中。 共享名包括一个名字,一个公共密钥和一个数字签证,共享名包含在配件清单中, 构成了配件的唯一标识符。 全局配件缓存是一个机器范围的存储区域,包含要用于多个程序的配件。 更多处理共享配件和全局配件缓存的信息,参见附录五“使用配件”中的“共享 配件”一节。 版本协调(versioning) 应用程序的清单包括一个配件的版本号,和一个带有相关版本信息的所有被引用 的配件的列表。配件版本号分为四部分,如下所示: ... 此信息用于缓解配件随时间变化发展所带来的版本协调问题。 在运行时,CLR使用清单中的版本信息和一组为机器定义的用于决定加载配件哪 个版本的版本协调策略。 默认的共享配件的版本协调策略,将自动使用最新可用版本号(包括主、次版本 号)。通过改变配置文件,应用程序或管理员可以覆盖这一行为。 私有配件没有版本协调策略,CLR 简单地加载应用程序目录中最新的配件就行 了。 类型解析 类型的唯一标识符(称为 TypeRef)由一个指向定义它的配件的引用和全限定类 型名(包括名字空间)组成。例如下面这个本地变量声明:第三章138 System.Net.WebRequest wr; 代表 MSIL 汇编语言中这些语句: .assembly extern System.Net { .ver 1:0:2204:4 ... } .locals(class [System.Net]System.Net.WebRequest wr) 此例中,System.Net.WebRequest 类型的作用域是 System.Net共享配件,由 一个共享名和相关版本号标识。 当程序启动时,CLR试图通过寻找各独立配件的正确版本(通过版本协调策略确 定),验证类型的存在(忽略访问修饰字),来解析所有静态的 TypeRef。 当程序要使用类型时,CLR会验证是否有正确的访问级,如版本不兼容则抛出运 行时的异常。 安全权限 我们说过配件构成了安全权限的边界。 配件清单包含任何所引用的配件的散列(编译时确定),配件所需的安全权限最小 集合的列表,配件所要求的可选权限的列表,和显式拒绝的(即永远不会接受) 权限的列表。 为了说明这些权限如何使用,想像一个用 .NET 框架开发的邮件客户程序(与 Outlook 类似)。它可能需要通过网络与 110 端口( POP3),25 端口( SMTP)和 143端口( IMAP4)通信的能力。需要在沙盒( sandbox)中运行 JavaScript 函数 的能力(这样可以在显示 HTML 邮件时保持交互性)。它可能需要拒绝授权写磁 盘或读取本地通讯簿的能力(这样可以避免爱虫病毒这样的脚本攻击)。 实际上,配件要声明其安全需求,但最终的决定在 CLR,它负责实施本地安全策 略。 .NET 框架编程 139 运行时,CLR使用散列决定是否一个独立配件被篡改了,并结合配件的权限信息 与本地安全策略决定是否加载配件,和授予它何种权限。 这种机制提供了安全的精细控制,也是 .NET框架超出传统 Windows程序的主要 优点之一。 反射 .NET 中以及通过 C#暴露的许多服务(如迟绑定,序列化,远程控制,属性信息 等)都依赖于元数据。应用程序也可以利用元数据,甚至可以用新信息扩展它。 通过其元数据来操作类型被称为反射( reflection),是用 System.Reflection名 字空间中丰富的类型实现的。创建新的类型(以及相关元数据)称为 Reflection.Emit,通过 System.Reflection.Emit名字空间实现。可以用定 制属性信息扩展现有类型的元数据。更多信息参见下面“定制属性信息”一节。 类型的层次 反射包括遍历和操作表示一个程序的对象模型,包括所有编译时和运行时元素。 因此,理解 .NET 程序的各种逻辑单位及其角色和关系,就非常重要了。 一个程序的基本单位是类型,类型包含了成员和内嵌类型。类型之外,程序还包 括一个或多个模块,和一个或多个配件。所有这些元素都是静态的,由编译器在 编译时生成的元数据描述。这里有个例外,就是通过 Reflection.Emit 创建的 元素(如类型、模块、配件等)。参见“运行时创建新类型”一节。 在运行时,这些元素都包含一个 AppDomain。它不是由元数据描述的,但在反 射中却起着举足轻重的作用,因为它是运行时 .NET 应用程序类型层次的根。 在任何给定程序中,这此元素的关系是分层的,如下所示:第三章140 AppDomain(层次的运行时根) 配件 模块 类型 成员 内嵌类型 下面我们将逐一讨论这些元素。 类型,成员和内嵌类型 反射所处理的最基本的元素是类型。这一分类类型,成员和内嵌类型代表了程序 中声明的每个类型的元数据(包括预定义和用户定义类型)。 类型包括成员,即包括构造器、字段、属性、事件和方法。而且,类型还包括内 嵌类型,它存在于外围类型作用域内,通常用作辅助类。类型可以组成模块,而 模块又包含于配件中。 配件和模块 配件是Win32 DLL的逻辑等价物,是类型部署、版本协调和重用的基本单位。而 且配件创建为类型创建成了安全性、可见性和作用域解析的边界(参见前面“配 件”一节)。 模块是一个物理文件,如一个 DLL,一个 EXE 或一个资源(如 GIF和 JPG)。虽 然并不常见,但配件可以由多个模块组成,允许控制程序的工作集的大小,在配 件中使用多种语言以及在多个配件之间共享模块。 AppDomain 从反射的角度看,AppDomain是类型层次的根,在配件和类型运行时载入内存时 作为容器,还可以把 AppDomain看成是 Win32程序中进程( process)的等价物。 AppDomain提供了隔离性,为受管制代码创建了坚固的边界,正如 Win32 中的 .NET 框架编程 141 进程边界。与进程相似, AppDomain 可以独立的启停,程序出错时只能使错误 发生的 AppDomain 崩溃,而不会影响驻留了 AppDomain 的进程。 获取类型的实例 反射的核心是 System.Type, 这是一个抽象基类,提供了对类型元数据的访问。 你可以使用 GetType访问Type类的实例,其中 GetType是 System.Object的 非虚拟方法。当调用 GetType 时,方法返回 System.Type 的一个具体子类型, 这个子类型可以反射和操作类型。 直接获取类型 也可以用名字(无需实例)获取 Type 类,使用静态方法 GetType,例如: Type t = Type.GetType("System.Int32"); 最后,C# 还提供了 typeof 运算符,它可以返回在编译时知道的任何类型: Type t = typeof(System.Int32); 这两种方式的主要区别是,Type.GetType是运行时计算的,动态性更好,按名 字绑定;而 typeof 运行符是编译时计算的,使用一个类型符号( token),调用 起来稍快。 在类型层次上反射 一旦获得了 Type的一个实例,就可以在前面所讲的应用程序层次中定位,通过 类型访问代表成员、模块、配件、名字空间、 AppDomain 和内嵌类型的元数据。 还可以检查元数据和任何定制属性信息,创建类型的新实例,以及调用成员。 下面的例子使用反射显示三个不同类型中的成员。第三章142 using System; using System.Reflection; class Test { static void Main() { object o = new Object(); DumpTypeInfo(o.GetType()); DumpTypeInfo(typeof(int)); DumpTypeInfo(Type.GetType("System.String")); } static void DumpTypeInfo(Type t) { Console.WriteLine("Type: {0}", t); // 获取类型中的成员列表 MemberInfo[] miarr = t.GetMembers(BindingFlags.LookupAll); // 输出细节信息 foreach (MemberInfo mi in miarr) Console.WriteLine(" {0}={1}", mi.MemberType.Format(), mi); } } 类型迟绑定 反射还可以执行迟绑定(late binding),程序在运行时动态加载、实例化和使用 类型。这是以调用开销为代价的,提供了更多灵活性。 本节我们创建一个例子,使用迟绑定,在运行时动态寻找新类型,并使用它们。 在例子中,一个或多个配件用名字(命令行中指定)加载,遍历配件中的类型寻 找 Greeting 抽象基类的子类型。当找到时,实例化类型,调用其 SayHello 方 法,该方法显示一个欢迎语。 为在运行时寻找类型,使用如下已编译成一个配件的抽象基类(参见源代码注释 中的文件名和编译信息): // Greeting.cs - 同 /t:library 一起编译 public abstract class Greeting { public abstract void SayHello(); } .NET 框架编程 143 编译此代码将生成名为 Greeting.dll 的文件,可以被其他的代码使用。 现在创建一个包含抽象类型 Greeting 的两个具体子类型的新配件,如下所示 (参见源代码注释中的文件名和编译信息): // English.cs - 同 /t:library /r:Greeting.dll 一起编译 using System; public class AmericanGreeting : Greeting { private string msg = "Hey, dude. Wassup!"; public override void SayHello() { Console.WriteLine(msg); } } public class BritishGreeting : Greeting { private string msg = "Good morning, old chap!"; public override void SayHello() { Console.WriteLine(msg); } } 编译源文件 English.cs 将生成一个名为 English.dll的文件,现在主程序可以动态 反射和使用它了。 接下来创建如下的主程序(参见源代码注释中的文件名和编译信息): // SayHello.cs - compile with /r:Greeting.dll // Run with SayHello.exe ... using System; using System.Reflection; class Test { static void Main (string[] args) { // 遍历命令行选项 // 试图加载每个配件 foreach (string s in args) { Assembly a = Assembly.LoadFrom(s); // 遍历所有公共类型 // 寻找 Greeting 的子类型 foreach (Type t in a.GetTypes()) if (t.IsSubclassOf(typeof(Greeting))) {第三章144 // 找到一个子类型,创建它 object o = Activator.CreateInstance(t); // 获得 SayHello.MethodInfo,调用 MethodInfo mi = t.GetMethod("SayHello"); mi.Invoke(o, null); } } } } 用命令“SayHello English.dll”运行实例将生成以下输出; Hey, dude. Wassup! Good morning, old chap! 这个例子有一点很有趣,它是完全迟绑定的,也就是就,在 SayHello程序发布以 后很长时间,你还可以创建一个新类型,让 SayHello自动地使用它,只需在命令 行中指定。这是通过反射的迟绑定的关键优点之一。 激活(activation) 在上面的例子中,我们手工加载配件,并使用 System.Activator类创建基于一 个类型的新实例。CreateInstance方法有许多覆盖版本,提供了各种创建选择, 包括绕过进程直接创建类型的能力: object o = Activator.CreateInstance("Assem1.dll", "Friendly.Greeting"); Activator类型的其他能力还包括:创建远程机器上的类型,在某个 AppDomain (沙盒)中创建类型,以及调用某个构造器(而不是上面例子中使用的默认构造 器)创建类型。 反射的高级用法 上面的例子演示了反射的用法,但并没有完成什么用普通C#语言结构完成不了的 .NET 框架编程 145 任务。实际上,反射是可以用 C#不直接支持的方式来操作类型。下面将演示这一 点。 虽然 CLR 要对类型成员进行访问控制(用 private等访问修饰字指定),但这些 限制不适于反射。假如你有正确的权限,就可以使用反射访问和操作私有数据和 函数成员,就像下例中这样使用前几节里的 Greeting 子类型(参见源代码注释 中的文件名和编译信息): // InControl.cs - 同 /r:Greeting.dll,English.dll 一起编译 using System; using System.Reflection; class TestReflection { // 注意:此方法需要 ReflectionPermission 权限 static void ModifyPrivateData(object o, string msg) { // 获取 FieldInfo 类型的私有数据成员 Type t = o.GetType(); FieldInfo fi = t.GetField("msg", BindingFlags.NonPublic| BindingFlags.Instance); // 使用 FieldInfo 调整数据成员的值 fi.SetValue(o, msg); } static void Main() { // 创建两个类型的实例 BritishGreeting bg = new BritishGreeting(); AmericanGreeting ag = new AmericanGreeting(); // 通过反射调整私有数据 ModifyPrivateData(ag, "Things are not the way they seem"); ModifyPrivateData(bg, "The runtime is in total control!"); // 显示修改后的欢迎语 ag.SayHello(); // "Things are not the way they seem" bg.SayHello(); // "The runtime is in total control!" } } 运行后,将产生以下输出: Things are not the way they seem第三章146 The runtime is in total control! 这说明,两个类型中的私有数据成员 msg通过反射被改变了,虽然类型中没有定 义允许此操作的公共成员。注意虽然此技巧可以避开访问控制,但它没有违反类 型安全。 虽然这个例子只是一个小技巧,但在创建诸如类浏览器、自动化测试套装工具等, 需要在比公共接口更深的层次查看和操作类型的时候,这种能力会非常有用。 运行时创建新类型 System.Reflection.Emit 名字空间包含许多类,可以在运行时创建全新的类 型。这些类可以在内存中定义一个动态配件;在配件中定义动态模块;在模块中 定义新类型,包括其所有成员;并且生成在成员中实现程序逻辑所需的 MSIL 操 作码。 下面的例子创建和使用了一个称为HelloWorld的新类型,它有一个称为SayHello 的成员: using System; using System.Reflection; using System.Reflection.Emit; public class Test { static void Main() { // 在当前 AppDomain 中创建动态配件 AppDomain ad = AppDomain.CurrentDomain; AssemblyName an = new AssemblyName(); an.Name = "DynAssembly"; AssemblyBuilder ab = ad.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run); // 创建模块中的类型和配件中的模块 Assembly a = (Assembly)ab; ModuleBuilder modb = a.DefineDynamicModule("DynModule"); TypeBuilder tb = modb.DefineType("AgentSmith", TypeAttributes.Public); .NET 框架编程 147 // 在类型中增加 SayHello 成员 MethodBuilder mb = tb.DefineMethod("SayHello", MethodAttributes.Public, null, null); // 为 SayHello 成员生成 MSIL ILGenerator ilg = mb.GetILGenerator(); ilg.EmitWriteLine("Never send a human to do a machine's job."); ilg.Emit(OpCodes.Ret); // 终结类型以创建它 tb.CreateType(); // 创建新类型实例 Type t = Type.GetType("AgentSmith"); object o = Activator.CreateInstance(t); // 输出 "Never send a human to do a machine's job." t.GetMethod("SayHello").Invoke(o, null); } } 使用 Reflection.Emit 的常见例子,是 BCL 中的对正则表达式的支持,可以生 成新类型,并调整以搜索特定的正则表达式,减少了运行时解释正则表达式的开 销。 BCL 中的其他使用,还包括动态生成远程控制的透明代理( transparent proxy), 和以最小的运行时开销执行特定的 XSLT 转换。 定制属性信息 类型、成员、模块和配件都有相关的元数据(所有主要 CLR 服务都要使用)。元 数据是程序的一个独立部分,可以通过反射访问(参见前面“反射”一节) 。元数 据的关键特征是可以扩展。可以用定制属性信息( custom attribute)来扩展元数 据,这样就能用保存与元素相关的元数据中的附加信息“修饰”代码元素。第三章148 这种附加信息于是可以在运行时获取,并用于创建声明性( declaratively)地进行 工作的服务。所谓声明性地工作指的是 CLR实现核心功能(如序列化和拦截)的 一种方式。 定制属性信息的语言支持 用定制属性信息修饰元素,也称指定( specify)定制属性信息,是通过在元素声 明之前将名字写入方括号内实现的,如下所示: [Serializable] public class Foo{...} 本例中,Foo 类被指定为可序列化的。此信息存入 Foo 的元数据,并影响 CLR 处理此类实例的方式。 还可以将定制属性信息看成是,它们扩展了C#中的内置声明结构集合,如public, private 和 sealed。 定制属性信息的编译器支持 事实上,定制属性信息就是用指定它们的语言结构,从 System.Attribute 派生 而来的类型(参见第二章中“属性信息”一节)。 这些语言结构是编译器所认可的,会发送小量数据到元数据。定制数据包括对定 制属性信息类型(包括位置性参数的值)的构造器的序列化调用,以及一个属性 设置运算集合(包含已命名参数的值)。 编译器还认可少量的伪定制属性信息( pseudo-custom attribute)。它们是元数据 中有直接表示的特殊属性信息,并在本机保存(也就是说,它们不是大量定制数 据)。这主要是一种运行时性能优化,虽然它也与通过反射获取属性信息(将在后 面讨论)有关。 为了理解这一点,思考下面有两个指定属性信息的类: .NET 框架编程 149 [Serializable ,Obsolete] class Foo{...} 在编译的时候,类 Foo 的元数据在 MSIL 中如下所示: .class private auto ansi serializable Foo { .custom instance void System.ObsoleteAttribute::.ctor() = ( 01 00 00 00 ) ... } } 请比较编译器对 Obsolete 属性信息与 Serializable 属性信息处理的差异。前 者是定制属性信息,保存为对 System.ObsoleteAttribute 类型的序列化构造 器调用;后者是伪定制属性信息,直接用 serializable 符号(token)在元数 据中表示。 定制属性信息的运行时支持 在运行时,诸如序列化和远程控制等核心CLR服务将检查定制属性信息和伪定制 属性信息,以决定如何处理类型的实例。 对于定制属性信息,通过创建属性信息的一个实例(调用相关构造器和属性设置 操作),然后执行必要的步骤以决定如何处理类型的实例。 对于伪定制属性信息,通过直接检查元数据来决定如何处理。因此,处理伪定制 属性信息效率更高。 要注意,这些步骤直到服务或用户程序真正试图访问这些属性信息时才开始,因 此,除非必需,运行时开销很小。 预定义属性信息 .NET框架广泛将属性信息用于从简单的文档到高级线程支持、远程控制、序列化第三章150 和 COM互操作各方面。这些属性信息都是在 BCL 中定义的,可以在用户自己的 代码中使用、扩展和获取。 但是,编译器和运行时环境对有些属性信息的处理方式很特别。C#规范中定义了 三个通用属性信息 AttributeUsage,Conditional 和Obsolete。其他属性信 息如 CLSCompliant,Serializable 和NonSerialized 的处理方式也很特殊。 AttributeUsage 属性信息 [AttributeUsage(target-enum [, AllowMultiple=[true|false]]? [, Inherited=[true|false]]? ] (用于类) AttributeUsage用在新的属性信息类声明中,用于控制编译器如何处理新属性 信息。说得更具体些,就是新属性信息可以被指定于何种目标(类,接口,属性, 方法,参数等等)上,新属性信息的多个实例是否可以应用于同种目标,此属性 信息是否可以传递给目标的子类型。 target-enum是一个来自System.AttributeTargets枚举的位掩码值,如下: namespace System { [Flags] public enum AttributeTargets { Assembly = 0x0001, Module = 0x0002, Class = 0x0004, Struct = 0x0008, Enum = 0x0010, Constructor = 0x0020, Method = 0x0040, Property = 0x0080, Field = 0x0100, Event = 0x0200, Interface = 0x0400, Parameter = 0x0800, Delegate = 0x1000, .NET 框架编程 151 ReturnValue = 0x2000, All = 0x3fff, ClassMembers = 0x17fc, } } Conditional 属性信息 [Conditional(symbol)](用于方法) Conditional属性信息可用于返回类型为void的任何方法,告诉编译器有条件 地忽略方法调用,除非在调用代码中定义了 symbol。这与用 #if和#endif预 处理指令将对方法的所有调用包裹起来很相似,但Conditional的好处是只需在 一处指定。 Obsolete 属性信息 [Obsolete([Message=]? message IsError= [true |false]] ](用于所有属性信息目标) 用于所有合法属性信息目标,表示此目标是过时的。 Obsolete可以包含一条消 息 message,解释应使用哪些替代的类型或成员,还包括一个标志 IsError,告诉 编译器将这种类型或成员按警告或错误处理。 例如,在下例中引用类型 Bar 会使用编译器显示一条错误信息并停止编辑: [Obsolete("Don't try this at home", IsError=true)] class Bar { ... } CLSCompliant 属性信息 [CLSCompliant(true|false)] (用于所有属性信息的目标)第三章152 用于配件时,告诉编译器是否验证配件中所有导出类型的 CLS兼容性。用于任何 其他属性信息目标时,允许目标声明自身是否是 CLS兼容的。如要将目标标记为 CLS 兼容的,则整个配件也需要这样。 下例中,用 CLSCompliant属性信息指定一个配件为CLS兼容的,而其中的一个 类不是 CLS 兼容的: [assembly:CLSCompliant(true)] [CLSCompliant(false)] public class Bar { public ushort Answer { get {return 42;} } } Serializable 属性信息 [Serializable] (用于类,结构,枚举和委托) Serializable 属性信息用于类、结构、枚举或委托,将其标记为可序列化的。 此属性信息是一个伪定制属性信息,在元数据中有特殊表示。 NonSerialized 属性信息 [NonSerialized](用于字段) 用于字段,防止其与所包含的类或结构一起序列化。此属性信息是一个伪定制属 性信息,在元数据中有特殊表示。 定义一个新的定制属性信息 除了使用 .NET 框架提供的预定义属性信息以外,也可以自己创建。 要创建定制属性信息,需要以下步骤: .NET 框架编程 153 1.从System.Attribute及其派生中派生一个类。按惯例类名应以“Attribute” 结尾,虽然并不是非要如此。 2. 用公共构造器提供类。构造器的参数定义属性信息的位置性参数,并且在指 定某元素的属性信息时是必需的。 3. 声明公共实例字段,或公共实例读 / 写属性,或公共实例只写属性,以指定 已命名的属性信息参数。与位置性参数不同,这些参数在指定某元素的属性 信息时是可选的。可用于属性信息构造器参数和属性的类型是 bool,byte, char,double,float,int,long,short,string,object,Type 类 型,enum,或以上类型的一维数组。 4. 最后,定义属性信息,以可使用 AttributeUsage属性信息指定,如上节所 述。 思考一下,下面的例子定制属性信息 CrossRefAttribute消除了CLR元数据只 能包含静态链接类型信息的限制。 using System; [AttributeUsage(AttributeTargets.ClassMembers, AllowMultiple=true)] class CrossRefAttribute : Attribute { Type xref; string desc = ""; public string Description { set { desc=value; } } public CrossRefAttribute(Type xref) { this.xref=xref; } public override string ToString() { string tmp = (desc.Length>0) ? " ("+desc+")" : ""; return "CrossRef to "+xref.ToString()+tmp; } } 从属性信息用户的观点来看,此属性信息可以应用于任何类成员多次(注意可使 用AttributeUsage属性信息控制)。CrossRefAttribute接收一个必需的位置 性参数(即相互参考类型)和一个可选的命名参数(描述),可这样使用: [CrossRef(typeof(Bar), Description="Foos often hang around Bars")] class Foo {...}第三章154 实际上,此属性信息将嵌入相互参考元数据中的动态链接类型(常有可选描述) 。 此信息就可以在运行时被类浏览器获取,以显示类型依存关系的更完整视图。 运行时获取定制属性信息 运行时获取属性信息,是通过对象 Type实例重载的一个GetCustomAttribute, 使用反射完成的。这也是少数几个定制属性信息和伪定制属性信息区别显著的环 境之一,因为伪定制属性信息能用 GetCustomAttribute 获取。 下例使用反射确定某个类型有什么属性信息: using System; [Serializable, Obsolete] class Test { static void Main() { Type t = typeof(Test); object[] caarr = t.GetCustomAttributes(); Console.WriteLine("{0} has {1} custom attribute(s)", t, caarr.Length); foreach (object ca in caarr) Console.WriteLine(ca); } } 虽然上例中 Test 类指定了两个属性信息,但输出如下: Test has 1 custom attribute(s) System.ObsoleteAttribute 这说明,Serializabe属性信息(伪定制属性信息)不能通过反射访问,而Obsolete 属性信息(定制属性信息)却可以。 自动内存管理 几乎所有现代编程语言都用两种特殊结构分配内存:堆栈( stack)和堆( heap)。 .NET 框架编程 155 堆栈分配的内存用于存储局部变量、参数和返回值,通常由操作系统自动管理。 堆分配的内存的处理方式因语言而异。在 C 和 C++ 中,是手工管理的。而在 C# 和 Java 中是自动管理的。 虽然手工内存管理有运行时实现简单的优点,但系统没有自动内存管理总是有很 多弊端。例如, C/C++ 程序中很多 bug就源于在对象删除后又使用它们(称为悬 挂指针),或者在对象没用的时候忘了删除(称为内存泄漏)。 自动管理内存的过程也称为无用资源回收( garbage collection)。虽然通常运行 时实现要比传统的手工内存管理要复杂,但无用资源回收器极大地简化了开发, 并消除了与手工内存管理相关的许多常见错误。 例如,在 C#中出现传统的内存泄漏几乎是不可能的,像传统 COM开发中常见的 bug,如循环引用就更不会出现了。 无用资源回收器 C# 依靠 CLR 提供许多运行时服务,无用资源回收器也不例外。 CLR 中有一个高性能的“标记并压缩式( mark-and-compact)”的无用资源回收 器,可分为几代,对受管制堆中存储的类型实例进行自动内存管理。无用资源回 收器可以看成是跟踪型的,因为它并不干涉对对象的访问,只是间歇地启动并跟 踪存于堆中的对象的映像( graph),以确定哪些对象已经成为无用资源而应该收 集。 无用资源回收器通常在内存分配的同时开始收集工作,而且内存太少将不能完成 请求。此过程也可以用 System.GC类型手工地启动。启动将冻结进程中的所有 线程,这样可以给无用资源回收器足够的时间来检查堆。 无用资源回收器从被认为是根的对象引用集合开始,遍历对象的映像,将所有碰 到的对象标记(mark)为“可达到的”。一旦此过程结束,所有未标记的对象就 被认为是无用的。第三章156 没有终结器的无用对象将马上丢弃,并回收其占用内存。有终结器的无用对象将 进行标记,以便使另一个线程上的异步处理在下一次收集前,调用其 Finalize 方法。 然后有用对象会被移到堆的底部(称为压缩, compact),希望能释放足够空间成 功地进行内存分配。 此时内存分配将再次进行,进程中的线程被解冻,接下来要么继续正常处理,要 么抛出一个 OutOfMemoryException。 优化技巧 虽然无用资源回收器看上去效率没有手工内存管理高,但通过引入各种优化技巧, 可以减少程序等待无用资源回收器的冻结时间(也称中断时间,pause time)。 这些优化技巧中最要的是使用无用资源回收器。此技巧利用了这样一个事实,虽 然许多对象分配和丢弃都很快,但一些对象生存期很长,无须在每次收集时都进 行跟踪。 无用资源回收器基本上把受管制的堆分为三代( generation)。刚刚分配的对象处 于Gen0代,已经经历过一次回收循环而仍然存在的对象处于 Gen1代,所有其他 对象处于 Gen2 代。 在进行回收时,无用资源回收器开始只回收 Gen0代对象。如果回收的内存不够, 就回收 Gen0 代和 Gen1 代。如果还不行,就要把 Gen0 代、Gen1 代和 Gen2 代都 回收。 提高自动内存管理性能还有很多其他优化方法,一般说来,自动内存管理有可能 接近手动内存管理的性能。 .NET 框架编程 157 终结器(finalizer) 在实现自定义类型时,可以选择增加一个终结器。终结器是在对象被定为无用资 源时,由无用资源回收器异步调用的方法。 虽然在某些情况下终结器是必需的,但通常有很多技术原因就避免使用终结器。 如上节所述,有终结器的对象在收集时会产生很大的开销,需要异步调用其 Finalizer 方法,而且回收其内存也需要两个完整的无用资源回收器循环。 其他原因还包括: ● 有终结器的对象,比没有的对象在受管制的堆中分配所花时间更长。 ● 有终结器的对象引用其他对象(即使是没有终结器的对象) ,会不必要地拉 长被引用对象的生存期。 ● 不可能预测一组对象终结器调用的顺序。 ● 对何时甚至是否调用对象的终结器,你所拥有的控制都十分有限。 总之终结器有些像律师:虽然有些情况下确实需要,但通常你并不想用,除非不 可避免。而且如果非要用的话,应该百分之百地理解其作用和弊端。 如果必须实现一个终结器,请遵循以下原则,除非你有充分理由不遵循: ● 确保终结器快速执行。 ● 千万不要因终结器而阻塞。 ● 自己释放所有未受管制的资源。 ● 不要引用其他对象。 ● 不要抛出其他未处理的异常。 ● 在运行前调用基类的 Finalize 方法。第三章158 Dispose 或 Close 方法 通常,在决定某个对象不再使用时,应该显式调用清除代码。 Microsoft推荐写一 个方法(名为 Dispose 或Close,取决于类型的语法)去执行所需的清除。如果 还有一个Finalize方法,还要对System.GC类型调用静态的SuppressFinalize 方法,表示 Finalize 方法不再需要调用了。通常,真正的 Finalize 方法要编 写成调用 Dispose/Close 方法,如下所示: public class Worker { ... public void Dispose() { // 执行正常清除 ... // 标记此对象已终结了 GC.SuppressFinalize(this); } protected override void Finalize() { Dispose(); base.Finalize(); } } 同本机 DLL 互操作 PInvoke,平台调用服务( Platform Invocation Service)的缩写,可以允许 C# 访 问未管制DLL的函数、结构和回调。例如,如果你想调用 Windows user32.dll 的 MessageBox 函数: int MessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCation, UINT uType); 要调用此函数,可以写一个带 DLLImport 属性信息的 static extern 方法: using System.Runtime.InteropServices; [DllImport("user32.dll")] static extern int MessageBox(int hWnd, string text, string caption, int type); .NET 框架编程 159 PInvoke 然后会找到并载入所需 Win32 DLL,并解析所请求的函数的入口点。 CLR 中有一个列集器,知道如何在 .NET 类型和未管制类型之间转换参数以及返 回值。上例中 int参数直接转换为函数所需的 4 字节整数,而 String 参数转换 为以 null 结尾的字符数组(在 Win9x 中是 1 字节 ANSI 字符,WinNT/2000 中是 2 字节 Unicode 字符)。 列集常见类型 CLR 列集器是一个 .NET 设施,知道 COM 和 Windows API 所有的核心类型,并 提供到 CLR 类型的默认转换。例如 bool类型可以转换为2 字节 Windows BOOL 类型或 4 字节 Boolean 类型。可以使用 MarshalAs 属性信息覆盖默认转换。 using System.Runtime.InteropServices; static extern int Foo([MarshalAs(UnmanagedType.LPStr)] string s); 上例中,通知列集器使用LRStr,因此应使用ANSI字符。数组类和StringBuilder 类将从外部函数把列集值复制回受管制值,如下所示: [DllImport("kernel32.dll")] static extern int GetWindowsDirectory(StringBuilder sb, int maxChars); class Test { static void Main() { StringBuilder s = new String(256); GetWindowsDirectory(s, 256); Console.WriteLine(s); } } 列集类和结构 将类或结构传递 C 函数需要用 StructLayout 属性信息生成类或结构: using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential)]第三章160 class SystemTime { public ushort wYear; public ushort wMonth; public ushort wDayOfWeek; public ushort wDay; public ushort wHour; public ushort wMinute; public ushort wSecond; public ushort wMilliseconds; } class Test { [DllImport("kernel32.dll")] static extern void GetSystemTime(SystemTime t); static void Main() { SystemTime t = new SystemTime(); GetSystemTime(t); Console.WriteLine(t.wYear); } } 在 C 和 C# 中,对象中的字段都位于距对象地址几个字节处。区别在于, C# 程序 通过字段名查寻偏移地址,C 字段则直接编译为偏移地址。例如,在 C 中,WDay 只是一个符号,表示位于 SystemTime 实例地址加上 24 字处所存的内容。 为了快速访问并且在将来扩展数据类型,这些偏移通常是称为包大小(pack size) 的最小字节数的倍数。对于.NET 类型而言,包大小通常由运行时环境决定,但 可以通过StructLayout属性信息控制字段偏移。使用此属性信息时默认包大小 是 8 字节,但可以设置为 1,2,4,8 或 16 字节,而且也可以显式选择某个字段 的偏移。这样 .NET 类型就可以传递给 C 函数。 in /out 列集 前面的Test例子在SystemTime是一个结构,而 t是一个ref参数时可以工作, 但实际上这样的效率更低: struct SystemTime {...} static extern void GetSystemTime(ref SystemTime t); .NET 框架编程 161 这是因为列集器必须总是为外部参数创建新值,因此在进入( in)函数时前一个 方法要复制 t,从函数出来( out)时又要复制已列集的 t。默认时,传值参数要 在 in 时复制,C# ref参数在 in/out时复制,而 C# out参数要在 out时复制, 但有定制转换的类型与此不同。例如,数组类和 StringBuilder 类需要从函数 出来时复制,因此,属于 in/out属性信息。偶尔覆盖此行为,及 in/out属性信 息会很有用。例如,如果一个数组应该只读, in修饰字表示仅在进函数进复制数 组,而出函数时不复制: static extern void Foo([in] int[] array); 未管制代码的回调 C#不仅能调用 C 函数,也可以使用回调而由 C 函数调用,C#中,delegate类型 用在有函数指针时。 class Test { delegate bool CallBack(int hWnd, int lParam); [DllImport("user32.dll")] static extern int EnumWindows(CallBack hWnd, int lParam); static bool PrintWindow(int hWnd, int lParam) { Console.WriteLine(hWnd); return true; } static void Main() { CallBack e = new CallBack(PrintWindow); EnumWindows(e, 0); } } 预定义的互操作支持属性信息 BCL 提供了一组属性信息用于标记对象,这些属性信息信息 CLR 列集服务用来 改变对象的默认列集行为。 本节讲述最常用的与本机 Win32 DLL 互操作的属性信息。这些属性信息都存于 System.Runtime.InteropServices 名字空间。第三章162 DllImport 属性信息 [DllImport (dll-name [, EntryPoint=function-name]? [, CharSet=charset-enum]? [, SetLastError=true|false]? [, ExactSpelling=true|false]? [, CallingConvention=callconv-enum]?)] (用于方法) DllImport属性信息用于说明定义了DLL入口点的外部函数。此属性信息的参数 有: dll-name 指定 DLL 名的字符串。 function-name 指定 DLL 函数名的字符串,如果需要 C# 函数名与 DLL 函数名不同,这会 很有用。 charset-name 指定如何列集字符串的 Charset 枚举。默认值是 Charset.Auto,将字符 串转成 Win 9x ANSI 字符,或 WinNT/2000 的 Unicode 字符。 SetLastError 如为 true,保留 Win32 错误信息。默认值为 false。 ExactSpelling 如为 true,EntryPoint必须精确匹配函数。如为 false,使用名字匹配探 索(heuristics)的方式。默认值为 false。 callconv-enum CallingConvention枚举,指定用于EntryPoint的模式。默认值为Stdcall。 .NET 框架编程 163 StructLayout 属性信息 [StructLayout(layout-enum [, Pack=packing-size]? [, CharSet=charset-enum]? [, CheckFastMarshal=[true | false])?] (用于类和结构) structLayout属性信息用于指定类或结构的数据成员如何在内存中布局。虽然 此属性信息常用于声明传递给或从中返回的本机DLL的结构,它也能定义适于文 件和网络 I/O 的数据结构。此属性信息的参数有: layout-enum Layoutkind 枚举,它可以是: 1)顺序的(sequential)字段一个接着一个 相距最小包大小排列;2)联合(union),只要字段为值类型,所有的偏移 都为 0;3)显式的,让每个字段有定制的偏移。 packing-size int 类型,指定包大小为 1,2,4,8 或 16 字节,默认值为 8。 charset-enum CharSet 枚举,指定如何列集字符串,默认值是 CharSet.Auto,将字符 串转成 Win 9x ANSI 字符,或 WinNT/2000 的 Unicode 字符。 CheckFastMarshal 逻辑值,指定在类型未标记为可传输( “blittable”)时,是否产生编译时警 告。默认值为 false。 FieldOffset 属性信息 [FieldOffset (byte-offset)](用于字段) FieldOffset属性信息用于在有显式字段布局的类或结构中。此属性信息可以用 于字段,指定字段距离类或结构开始处地址的偏移。注意,这些偏移不是非要递 增,也可以重叠,因此可以创建一个联合数据结构。第三章164 MarshalAs 属性信息 [MarshalAs(unmanaged-type) [, named-parameters]?] (用于字段,参数,返回值) MarshalAs属性信息覆盖了列集器应用于参数或字段的默认列集行为。unmanaged- type 值取自 UnmanagedType 枚举,允许取值如下: Bool LPStr VBByRefStr I1 LPWStr AnsiBStr U1 LPTStr TBStr I2 ByValTStr VariantBool U2 Iunknown FunctionPtr I4 Idispatch LPVoid U4 Struct AsAny I8 Interface RPrecise U8 SafeArray LPArray R4 ByValArray LPStruct R8 SysInt CustomMarshaler BStr SysUInt NativeTypeMax Error 对如何和何时使用这些枚举值以及其他named-parameters的详细的叙述。参见 .NET 框架 SDK 文档。 In 属性信息 [In](用于参数) In 属性信息指定数据应列集入调用者,可以占 Out 属性信息结合使用。 .NET 框架编程 165 Out 属性信息 [Out](用于参数) Out 属性信息指定数据从被调用方法到调用者列集出来,可以与In 属性信息结 合使用。 与 COM 互操作 CLR 支持将 C# 对象暴露为 COM 对象,以及在 C# 中使用 COM 对象。 绑定 COM 和 C# 对象 COM与C#的互操作是通过早或迟绑定实现的。早绑定允许使用编译时知道的类 型编程,而迟绑定要求使用通过动态发现的类型编程,在C#一侧使用反射,COM 一侧使用 IDispatch。 当从 C#中调用 COM 程序时,早绑定通过为 COM 对象及其接口提供配件形式的 元数据实现。TlbImp.exe 获取 COM 类型库并在配件中生成等价的元数据。使用 已生成的配件,就可能实例化并调用 COM 对象,就像其他 C# 对象一样。 当从 COM中调用C#程序时,早绑定通过类型库实现。 TlbExp.exe和RegAsm.exe 可以从配件生成 COM 类型库。然后可以合使用支持通过类型库进行早绑定的工 具(如 VB6)处理这个类型库。 将 COM 对象暴露给 C# 当实例化 COM 对象时,实际上是在处理一个称为 RCW(Runtime Callable Wrapper,运行时可调用包裹器)的代理。 RCM 负责管理 COM 对象的生存期需 求,并将其上调用的方法转换为 COM 对象上的相应调用。当无用资源回收器终 结 RCW时,会释放其对象的所有引用。对于需要不等无用资源回收器终结 RCW第三章166 就释放 COM 对象的场合,可以使用 System.Runtime.InteropServices. Marshal 类型的静态 ReleaseComObject 方法。 下例说明了从 C# 中通过 COM 互操作在 Contacts 中增加一个 MSN Instant Messenger 用户: // SetFN.cs - 与 /r:Messenger.dll 一起编译 // 运行 SetFN.exe ,为当前登录的用户设置 FriendlyName // 运行 TlbImp.exe "C:\Program Files\Messenger\msmsgs.exe" // 以创建 Messenger.dll using Messenger; // MSN Instant Messenger 的 COM API public class MyApp { public static void Main(string[] args) { MsgrObject mo = new MsgrObject(); IMsgrService im = mo.Services.PrimaryService; im.FriendlyName = args[0]; } } 将 C# 对象暴露给 COM 与从C#访问COM对象时需要RCW代理包裹COM对象一样,将C#对象当作COM 对象访问的代码也要通过一个代理实现。当 C#对象被列集出到 COM时,运行时 环境创建一个 CCW(COM Callable Wrapper,COM 可调用包裹器) 。CCW 遵 循其他COM对象相同的生存期规则,只要仍存活, CCW就维护着一个指向其包 裹的对象的可跟踪引用,它在无用资源回收器运行时仍然保持对象存活。 下例说明了如何从 C# 导出类和接口,并控制 GUID 和 DISPID 的赋值。在编译 IRunInfo 和StackSnapshot 后,可以用 RegAsm.exe 注册二者。 [GuidAttribute("aa6b10a2-dc4f-4a24-ae5e-90362c2142c1")] public interface : IRunInfo { [DispId(1)] string GetRunInfo(); } [GuidAttribute("b72ccf55-88cc-4657-8577-72bd0ff767bc")] public class StackSnapshot : IRunInfo { public StackSnapshot() { .NET 框架编程 167 st = new StackTrace(); } [DispId(1)] public string GetRunInfo() { return st.ToString(); } private StackTrace st; } 在 C# 中映射 COM 当从 C#中使用一个 COM对象时,RCW使该对象的方法就像一个普通的C#实例 方法一样。在 COM中,方法正常返回一个 HRESULT,表示成功或失败,并使用 out 参数返回一个值。但在 C# 中,方法正常返回其结果值,使用异常报告错误。 RCW 检测从 COM 方法的调用返回的 HRESULT,在失败时抛出 C# 异常。如果 成功,RCW 返回标记为 COM 方法 S 中返回值的参数。 注意: 更多参数修饰字和 COM 类型库类型到 C# 类型的默认映射,参见附录四。 常见 COM 互操作支持属性信息 BCL 支持一组用于标记对象的属性信息,这些信息是 CLR 互操作服务将管制类 型作为 COM 对象,暴露给未管制领域所需要的。 本节讲述这方面最常用的属性信息。这些属性信息都存在 System.Runtime. InteropServices 名字空间中。 ComVisible 属性信息 [ComVisible(true | false)] (用于配件,类,结构,枚举,接口,委托)第三章168 当生成一个类型库时,配件中所有公共类型默认都是暴露的。 ComVisible 属性 信息指定了不应暴露的公共类型(甚至是整个配件)。 DispId 属性信息 [DispId(dispatch-id)] (用于方法,属性,字段) DispId属性信息指定赋值给方法、属性、字段的DispID,以用于通过IDispatch 接口访问。 ProgId 属性信息 [ProgId(progid)](用于类) ProgId 属性信息指定类所用的 ProgID。 Guid 属性信息 [GuidAttribute(guid)] (用于配件,模块,类,结构,枚举,接口,委托) Guid 属性信息指定用于类或结构的 COM GUID。此属性信息应用其完整的类型 名指定,以避免与 Guid 类型冲突。 HasDefaultInterface 属性信息 [HasDefaultInterface](用于类) HasDefaultInterface属性信息指定类的第一个继承接口应用作默认接口(而 不生成唯一接口)。 .NET 框架编程 169 InterfaceType 属性信息 [InterfaceType(ComInterfaceType)] (用于接口) 默认情况下,在类型库中生成的接口都是双重的( dual),但使用此属性信息可以 生成任一种 COM 接口类型(dual,dispatch,或传统的 Iunknown 派生接口)。 ComRegisterFunction 属性信息 [ComRegisterFunction](用于方法) 在注册配件过程中,请求 RegAsm.exe 调用一个方法。 NoIDispatch 属性信息 [NoIDispatch](用于类) NoIDispatch 属性信息指定对类的 IID_Idispatch 的请求应返回 E_ NOINTERFACE。170 第四章 BCL 综述 第三章中,我们关注的是.NET框架几个关键的方面,以及如何在C#中使用它们。 但是,这些方面并不仅局限于在 C# 中使用。 几乎所有.NET框架的性能都是通过一套称为BCL(基类库)的管理类型暴露的。 因为这些类型是遵循 CLS 的,它们可以被几乎任何 .NET 语言访问。BCL 类型可 按名字空间分组,并从 .NET 平台的一部分 —— 一个配件( DLL)集合导出。在 C#程序使用这些类型需要在编译时引用合适的配件(参见附录六“名字空间和配 件”)。 为了使用 C#高效地在 .NET 平台上工作,理解预定义库的通用性能是很重要的。 但是,这个库极为庞大,不可能在本书中完整讲述,因为它包括分为 120个名字 空间的 4500 多个类型,从 40 个不同配件导出。 本章中,我们将对整个 BCL 进行综述(按逻辑分开) ,并提供相关名字空间和类 型的参考,这样你自己也能够进一步探索 .NET 框架 SDK 的细节。 这里提到的具体类型和名字空间是基于 .NET 框架 beta 1,在未来的 beta 版和发 布版中可能会发生变化。BCL 综述 171 有些名字空间和类型是尚无正式文档说明过的,所以未被微软支持。为清晰起见, 这些名字空间以等宽斜体字体表示。 探索BCL的有用工具有:.NET框架SDK文档,WinCV.exe类浏览器,和ILDsm.exe 反汇编程序(参见第五章)。 核心类型 核心类型都包含在System名字空间中。这个名字空间是 BCL的心脏,包含所有 其他类型作为基础的类、接口和属性信息。 BCL的根是 Object类型,所有其他 .NET 类型都是由此派生而来的。其他基本类型是 ValueType(结构的基类型), Enum(枚举的基类型),Convert(用于基类型之间转换),Exception(所有 异常的基类型)以及预定义值类型的装箱版本。 BCL 中使用的接口,如 ICloneable,IComparable,IFormattable和IConvertible在此定义(参见第 三章中的“字符串” 一节)。还有扩展类型如DateTime,TimeSpan和DBNull。 以及其他支持单一和组播委托(参见第二章中的“委托” ),基本数学运算(参见 第二章中的“数学”一节) ,属性信息(参见第二章中“属性信息” )和异常处理 (参见第二章中的“try 语句和异常”)。 更多字符串的信息,参见 String 名字空间。 文本 BCL 为文本提供了丰富的支持。重要类型包括 String类,用于处理不可变字符 串,StringBuilder类提供支持地区比较运算和多字符串编码格式( ASC II, Unicode,UTF-7 和 UTF-8)的字符串处理运算,还有一组支持正则表达式的类 (参见第三章中“字符串”一节)。 System.Text System.Text.RegularExpressions 其他名字空间中重要类型包括 System.String。第四章172 集合 BCL提供了一组通用数据结构,如ArrayList,Dictionary,Hashtable,Queue, Stack,BitArray 及其他。使用公用基类型和公开接口的标准化设计模式,可以 持续处理整个 BCL 的集合,包括预定义和用户自定义集合类型(参见第三章中 “集合”一节)。更多信息,参见以下名字空间: System.Collections System.Collections.Bases 其他名字空间中的重要类型包括 System.Array。 流和输入输出 BCL提供了对访问标准输入输出和错误流的良好支持。还提供了执行二进制和文 本文件输入输出、为文件系统事件通知注册、访问称为“隔离存储”的安全用户 存储区的类(参见第三章中的“输入输出”)。 更多信息,参见以下名字空间: System.IO System.IO.IsolatedStorage System.Console 联网 BCL提供了一组用于网络通信的分类,使用不同的抽象级,包括原始套接字访问; TCP、UDP 和 HTTP 协议支持;基于 URI 和流的高级请求 / 响应机制,可插拔的 协议处理器(参见第三章中的“联网”)。 更多信息,参见以下名字空间: System.Net System.Net.SocketsBCL 综述 173 其他名字空间中的重要类型包括 System.IO.Stream。 线程 BCL 为创建多线程程序提供了丰富的支持,包括:线程和线程池管理;线程同步 机制如监视程序、互斥、事件、读 / 写块,等等;以及访问 I/O端口和系统时钟等 底层平台组件(参见第三章的“线程”一节)。 更多信息,参见以下名字空间: System.Threading System.Timers System.Timers.Design 其他名字空间中的重要类型包括 System.ThreadStaticAttribute。 安全 BCL 提供了操作 .NET 运行时代码访问安全模型( Code Access Security Model) 的所有元素,包括安全策略、安全原则、权限集合和 evidence。这些类还支持加 密算法,如 DES,3DES,RC2,RSA,DSig,MDS,SHA1 和用于流或转换的 Base64 编码。 更多信息,参见以下名字空间: System.Security System.Security.Cryptography System.Security.Cryptography.X509Certificates System.Security.Cryptography.Xml System.Security.Permissions System.Security.Policy System.Security.Principal第四章174 反射 .NET 运行时极为依赖元数据以及动态检查和操作它的能力。BCL 通过一组抽象 类实现了这一点,这些类映射应用程序的重要元素(配件、模块、类型和成员) , 并支持 BCL 类型和新类型的实例的创建(参见第三章“反射”一节)。 更多信息,参见以下名字空间: System.Reflection System.Reflection.Emit 其他名字空间中的重要类型包括: System.Type System.Activator System.AppDomain. 序列化 BCL 支持将任意对象映像从 / 到流中序列化。这种序列化可以通过文件或网络存 储并传送复杂数据结构。默认序列化器提供了二进制和基于 XML 的格式化,但 可以用用户自定义格式化器扩展。 更多信息,参见以下名字空间: System.Runtime.Serialization System.Runtime.Serialization.Formatters System.Runtime.Serialization.Formatters.Soap System.Runtime.Serialization.Formatters.Binary 其他名字空间中的重要类型包括: System.NonSerializedAttribute System.SerializableAttributeBCL 综述 175 远程调用 远程调用是分布式程序的基础,BCL极好地支持了执行和接受远程方法调用。调 用可以是同步,也可以是异步的;支持请求 / 响应或单向横式,可以在多种传输 层( TCP, HTTP, SMTP)上传递;还可以以多种格式( SOAP和二进制)序列化。 远程控制基础架构支持多种激活模式,基于租用( lease-based)的对象生命期, 分布式对象身份,按引用和接值的对象列集,以及消息截获。这些类型可以用用 户自定义的通道、序列化器、代理和调用环境扩展。 更多信息,参见以下名字空间: System.Runtime.Remoting System.Runtime.Remoting.Channels.Core System.Runtime.Remoting.Channels.HTTP System.Runtime.Remoting.Channels.MetadataServices System.Runtime.Remoting.Channels.SMTP System.Runtime.Remoting.Channels.TCP System.Runtime.Remoting.Services 其他名字空间中的重要类型包括: System.AppDomain System.CallContext System.ContextBoundObject System.ContextStaticAttribute System.MarshalByRefObject Web 服务 从逻辑上说,Web 服务只是远程控制的一个实例。实际上,BCL 的 Web 服务支 持是 ASP.NET的一部分,可以从 CLR 远程基础架构分离出来。包括的类和属性 信息有:用于描述和发布 Web 服务的,在特定位置( URI)上发现有何 Web服务 的,以及调用 Web 服务方法的。 更多信息,参见以下名字空间:第四章176 System.Web.Services System.Web.Services.Description System.Web.Services.Discovery System.Web.Services.Interop System.Web.Services.Protocols 数据访问 BCL包括一组访问数据源和管理复杂数据集的类。称为ADO.NET(又称ADO+)。 这些类将替代 Win32上的 ADO.NET支持连接和断开操作,多数据供应程序(包 括非关系型数据源),和从 / 到 XML 的序列化。 更多信息,参见以下名字空间: System.Data System.Data.ADO System.Data.ADO.Interop System.Data.CodeGen System.Data.Design System.Data.Internal System.Data.SQL System.Data.SQLTypes XML BCL 提供了对 XML1.0,XML 架构,XML 名字空间,两种不同的 XML 解析模 型(基于DOM2的模型和 SAX2的推模式变体),以及 XSLT、XPath和SOAP1.1 的广泛支持。 更多信息,参见以下名字空间: System.Xml System.Xml.Serialization System.Xml.Serialization.IO System.Xml.Serialization.Schema System.Xml.XPath System.Xml.XslBCL 综述 177 图形 BCL 包含支持图形图像的类,称为 GDI。这些类等价于Win32上的 GDI,包括对 画刷、字体、位图、文本绘制、绘制原语、图像转换和印前浏览等功能的支持。 更多信息,参见以下名字空间: System.Drawing System.Drawing.Design System.Drawing.Drawing2D System.Drawing.Imaging System.Drawing.Printing System.Drawing.Text 丰富的客户应用程序 BCL 支持创建传统的GUI应用程序,称为 Windows Forms,包括一个窗体包,一 个预定义 GUI组件集合,一个适于快速开发工具的组件模型。这些类提供了各种 抽象层,从低层消息循环控制器类到高层布局控制器,以及虚拟继承。 更多信息,参见以下名字空间: System.WinForms System.WinForms.ComponentModel System.WinForms.ComponentModel.COM2Interop System.WinForms.Design System.WinForms.PropertyGridInternal Web 应用程序 BCL 支持创建 Web 应用程序,称为 Web Forms,包括一个可生成 HTML 用户界 面的服务器端窗体包,一个预定义的基于 HTML的GUI部件集合,一个适于开发 工具的组件模型。还包括一个类集合,管理 Web 程序的会话状态、安全、缓存、 调试、跟踪、局部化、配置和部署。最后, BCL 还包括生成和使用 Web服务的类第四章178 和属性信息,我们已在前面“ Web 服务”一节中讲过了。这些功能总称为 ASP.NET,将完全替代 Win32 上的 ASP。 更多信息,参见以下名字空间: System.Web System.Web.Caching System.Web.Configuration System.Web.Handlers System.Web.Hosting System.Web.Security System.Web.SessionState System.Web.UI System.Web.UI.Design System.Web.UI.Design.Util System.Web.UI.Design.WebControls System.Web.UI.Design.WebControls.ListControls System.Web.UI.HtmlControls System.Web.UI.WebControls System.Web.UI.WebControls.Design System.Web.Util 全球化 BCL提供了用于全球化的类,支持编码页( code page)转换,区分地区( locale) 的字符串运算,日期 / 时间转换,以及使用资源文件将全局化工作集中。 更多信息,参见以下名字空间: System.Globalization System.Resources 配置 BCL 支持访问 .NET 的配置系统,该系统包括一个带配置设置继承的、各用户各 程序的配置模型,一个已处理安装程序框架。还包括使用和扩展配置框架的类。BCL 综述 179 更多信息,参见以下名字空间: System.Configuration System.Configuration.Assemblies System.Configuration.Core System.Configuration.Design System.Configuration.Install System.Configuration.Interceptors System.Configuration.Internal System.Configuration.Schema System.Configuration.Web 高级组件服务 BCL 支持创建分布式事务,JIT 激活,对象池化,入队列和事件等 COM+ 服务。 含有通过现有消息队列基础架构( MSMQ)访问可靠、异步、单向消息收发的类 型。BCL 还包含访问现有目录服务(活动目录)的类。 更多信息,参见以下名字空间: Microsoft.ComServices System.DirectoryServices System.Messaging System.Messaging.Design 配件 BCL支持在配件的元数据上标记目标操作系统和处理器汇编器版本及其他信息。 更多信息,参见以下名字空间: System.Runtime.CompilerServices System.Runtime.CompilerServices.CSharp第四章180 诊断与调试 BCL包含用于诊断调试的类,包括:带多监听器支持的调试跟踪;事件日志访问; 进程线程和栈帧信息的访问;可创建和使用性能计数器。 更多信息,参见以下名字空间: System.Diagnostics System.Diagnostics.Design System.Diagnostics.SymbolStore 与未管制代码互操作 .NET运行时通过COM、COM+和本地 Win32 API调用支持与未管制代码的双向 互操作。BCL为此提供了一套类和属性信息,包括受管制对象生存期的精确控制 和创建用户定义列集器处理特定互操作情况的选择。 更多信息,参见以下名字空间: System.Runtime.InteropServices System.Runtime.InteropServices.CustomMarshalers System.Runtime.InteropServices.Expando Microsoft.Win32.Interop Microsoft.Win32.Interop.Trident 其他名字空间中的重要类型包括 System.Buffer。 组件和工具支持 在 .NET 运行时中,组件是通过附加元数据和其他辅助使用元件窗体包(如 Windows Forms 和 Web Forms)的工具,来与类相区别的。 BCL 提供支持组件 和使用组件的工具的创建的类和属性信息。这些类还具有生成和编译C#,JScript 和 VB.NET 源码的能力。BCL 综述 181 更多信息,参见以下名字空间: System.CodeDOM System.CodeDOM.Compiler System.ComponentModel System.ComponentModel.Design System.ComponentModel.Design.CodeModel System.ComponentModel.Interop 运行时设施 BCL 提供了控制运行时行为的类。经典的例子是控制无用资源回收器和支持强 / 弱引用支持的类。 更多信息,参见 System 名字空间。 其他名字空间中的重要类型包括System.Runtime.InteropServices.GCHandle。 本地操作系统设施 BCL 支持控制现有 NT 服务和创建新服务。还支持访问特定 Win32 设施,如 Windows 注册表和 Windows 管理工具(WMI)。 更多信息,参见以下名字空间: Accessibility Microsoft.Win32 System.Core System.Management System.ServiceProcess182 第五章 核心 .NET 工具 .NET 框架 SDK中有许多有用的编程工具。下面按照字母顺序列出了我们认为开 发 C# 程序最有用或必不可少的编程工具。除非特别注明,列出的工具都能在 .NET 框架 SDK 安装文件的 \bin目录中找到。但一旦安装了 .NET 框架,可以从 任何目录访问它们。使用时打开 Command Prompt(命令提示)窗口并输入工具 名。要得到某工具的所有命令行开关的完整列表,输入工具名(如 csc)并按回 车键。 Adepends.exe: 配件依赖关系列表 Adepends用于显示某个配件所依赖的所有配件。这是一个很有用的C#程序, 可以在 .NET 框架目录树中 \tool 开发者指南目录中找到。使用前需要安装, 因为它们不是默认安装的。 Al.exe: 配件链接工具 用于从命名的模块和资源文件创建配件清单。还要导入 Win32资源文件。比 如: al /out:c.dll a.dll b.dll CorDbg.exe: 运行时调试器 MSIL 程序的通用源码级命令行调试工具。对 C# 源码调试非常有用。 CorDbg.exe 的源代码可以在 \tool 开发者指南目录中找到。核心 .NET 工具 183 Csc.exe: C# 编译器 编译 C# 源码,并包含资源文件和单独编译的模块。还可以指定条件编译选 项、XML 文档和路径信息。比如: csc foo.cs /r:bar.dll /win32res:foo.res csc foo.cs /debug /define:TEMP DbgUrt.exe: GUI 调试器 基于Windows的源码级调试器。可以在 .NET框架SDK安装文件的\GuiDebug 目录中找到。 GACUtil.exe:全局配件缓存工具 可用来安装、卸载全局配件缓存工具并列出其内容。比如: gacutil /i c.dll ILAsm.exe: MSIL 汇编器 直接从 MSIL 文本表示创建 MSIL 模块和配件。 ILDasm.exe: MSIL 反汇编器 反汇编模块和配件。默认是显示树状表示形式,但也可以指定输出文件。比 如: ildasm b.dll ildasm b.dll /out=b.asm InstalUtil.exe:安装工具 执行配件中的安装器和卸载器。可以写日志文件并保存状态信息。 nmake.exe: make 工具 一种常见工具,用于编写脚本编译连接多个组件和源文件,并跟踪重编连的 依赖信息,参见附录五。 PEVerify.exe: PE 文件验证器 验证编译器是否生成了类型安全的 MSIL。C# 总是生成类型安全的 MSIL。 可用于与基于 ILAsm 的程序互操作。 RegAsm.exe:配件注册工具 在系统注册表中注册配件。允许 COM 客户调用受管制方法。也可用于生成 注册表文件,以备将来注册。比如:第五章184 regasm /regfile:c.reg c.dll RegSvcs.exe:服务注册工具 向 COM+ 1.0 注册配件 , 将其类型库安装到已有程序。也可以用于生成类型 库。比如: regsvcs foo.dll comapp newfoo.tlb Sn.exe:共享名工具 验证配件及其关键信息,也可用于生成关键文件。比如: sn -k mykey.snk SoapSuds.exe: SoapSuds 工具 为配件中的服务创建 XML Schema,以及从 Schema创建配件。也可以通过 URL 引用 Schema。比如: soapsuds -url:http://localhost/myapp/app.soap?SDL -os:app.xml TlbExp.exe:类型库导出器 从所提供的配件中的公共类型导出COM类型库。与 RegAsm的区别在于,它 不进行注册。比如: tlbexp /out:c.tlb c.dll TlbImp.exe:类型库导入器 从所提供的COM类型库中创建受管制的配件,并将类型定义映射为.NET类 型。使用时需要将该配件导入 C# 程序。比如: tlbimp /out:MyOldCom.dll MyCom.tlb WebServiceUtil.exe: Web 服务工具 为 ASP.NET Web 服务方法创建服务描述并生成代理。更多细节参见 .NET 框架 SDK 中的 ASP.NET 文档。 WinCV.exe:类浏览器 搜索所提供的配件中的匹配名字。如果没有提供,使用默认库。名字空间和 类在列表框中显示,而所选择的类型信息在另一个窗口中显示。核心 .NET 工具 185 WinDes.exe:窗口设计器 一个基于 WinForms 的组件设计器,用于 C# 或 Visual Basic 表单,带有工 具箱、属性编辑器和显示表单。 Xsd.exe: XML Schema 定义工具 用于从XDR, XML文件或类信息生成XML Schema,还可以从XML Schema 生成 DataSet 或类信息。比如: xsd foo.xdr xsd bar.dll187 附录一 C# 关键字 abstract 类修饰字,用于指定类必须通过派生进行实例化。 as 二进制运算符类型,将左边的运算数转换为右边运算数指定的类型,在转换 失败时返回 null,而不是抛出异常。 base 变量,除了访问成员的基类实现以外,与 this 意思相同。 bool 逻辑数据类型,取值为 true 或false。 break 跳转语句,可退出循环或 switch 语句块。 byte 单字节无符号整型数据类型。 case 在 switch 语句中定义特定选择的选择语句。附录一188 catch try 语句的一部分,用来捕捉 catch 子句中定义的特定类型的异常。 char 两字节 Unicode 字符数据类型。 checked 语句或运算符,对表达式或语句块进行算术边界测试。 class 可扩展的引用类型,将数据和功能结合在一个单元中。 const 用于局部变量或字段声明的限定词,表示值是一个常量。const在编译时计 算,而且只能是预定义类型。 continue 跳转语句,用于在循环中跳过语句块中剩余语句,继续下一次循环。 decimal 16 字节精确十进制数据类型。 default switch语句中的标号,在没有 case语句匹配 switch表达式时指定执行 动作。 delegate 一种类型,用于定义方法签名,委托实例可以保存和调用一个或一组与其签 名匹配的方法。 do 循环语句,用于循环执行语句块,直至循环尾部的循环表达式值为 false。 double 8 字节浮点数据类型。 else 条件语句,在前一个 if 表达式的值为 false 时,定义执行动作。C# 关键字 189 enum 一种值类型,用于定义一组已命名的数字常量。 event 用于委托字段或属性的成员修饰字,表示只有委托的“+=”和“ -=”方法 可以访问。 explicit 用于定义显式转换的运算符。 extern 一个方法修饰字,表示方法是由不受管理的代码实现的。 false 逻辑直接量。 finally try 语句的一部分,总是在控制离开 try 语句块时执行。 fixed 一条语句,用于固定引用类型,以使无用资源回收器在指针算术运算期间不 移动该类型。 float 四字节浮点数据类型。 for 循环语句,由初始化语句、停止语句、循环条件和迭代语句合成。 foreach 循环语句,用于迭代实现了 IEnumerable 的集合。 get 访问器的名字,返回属性的值。 goto 跳转语句,用于跳转到与跳转点作用域和方法相同的一个行号。附录一190 if 条件语句,如果表达式值为真时,执行语句块。 implicit 用于定义隐式转换的运算符。 in 放在一个类型和 foreach 语句中的 IEnumerable 之间的运算符。 int 4 字节有符号整型数据类型。 interface 一种语法结构,用于指定类或结构可以实现的一些成员,以接受该类型的一 般服务。 internal 访问修饰字,表示类型或类型成员只能被同一配件的其他类型访问。 is 关系运算符,在左运算子与右运算子类型匹配、从右运算子派生而来或实现 了右运算子指定的类型时,结果值为 true。 lock 一条语句,用于获取引用类型的对象的锁,以帮助多个线程协作。 long 8 字节有符号整型数据类型。 namespace 用于将一组类型映射到一个公用名字的关键字。 new 用于在一个类型上调用构造器的运算符。如果此类型是引用类型,将在堆中 分配一个新对象;如果是值类型,初始化此对象。可重载此关键字以隐藏继 承成员。C# 关键字 191 null 引用类型直接量,表示未引用对象。 object 派生所有其他类型的类型。 operator 重载运算符的方法修饰字。 out 参数修饰字,用于指定按引用传递的参数,必须通过被调用的方法赋值。 override 方法修饰字,表示一个类的方法覆盖了一个类或接口的虚拟方法。 params 参数修饰字,用于指定方法的最后一个参数可以接受同类型的多个参数。 private 访问修饰字,表示只有包含自己的类型可以访问这个成员。 protected 访问修饰字,表示只有包含自己的类型或派生类型可以访问这个成员。 public 访问修饰字,表示类型和类型成员可以被所有其他类型访问。 readonly 字段修饰字,用于指定字段只能赋值一次,要么在声明时,要么在包含它的 类型的构造器。 ref 参数修饰字,用于指定参数按引用传递,在传给方法之前赋值。 return 跳转语句,用于退出一个方法,并在方法不是 void的时,指定一个返回值。 sbyte 1 字节有符号整型数据类型。附录一192 sealed 类修饰字,用于表示一个类不能派生其他类。 set 访问器的名字,用于设置属性的值。 short 2 字节有符号整型数据类型。 sizeof 用于返回结构大小(以字节计)的运算符。 stackalloc 运算符,用于返回这样的指针,它指向堆栈中分配的指定数量的值类型。 static 类型名字修饰字,用于表示成员应该应用在类型而不是类型的实例。 string 预定义引用类型,表示 Unicode 字符的不变序列。 struct 值类型,用于将数据和功能结合在一个单元中。 switch 选择语句,可以用来按一个预定义类型的值在许多选项中进行选择。 this 引用类或结构的当前实例的变量。 throw 跳转语句,在异常情况发生时,抛出一个异常。 true 逻辑直接量。 try 一条语句,用于处理一个语句块中的异常或提前退出。C# 关键字 193 typeof 以一个 System.Type 对象的形式,返回对象类型的运算符。 uint 4 字节无符号整型数据类型。 ulong 8 字节无符号整型数据类型。 unchecked 禁止对一个表达式进行边界检测的语句或运算符。 unsafe 方法修饰字或语句,允许在特定语句块中执行指针运算。 ushort 2 字节无符号整型数据类型。 using 关键字,用于指定无需使用全限定名,即可引用某个名字空间中的类型。 value 通过属性的 set 访问器设置的隐式变量名。 virtual 类方法修饰字,用于表示一个方法可以被派生类覆盖。 void 关键字,用于没有返回值的方法中的类型。 while 循环语句,在每次循环开始循环表达式值为 false 时,执行语句块。194 附录二 正则表达式 以下各表总结了System.Text.RegularExpression中的正则表达式类所支持的 正则表达式语法。表中每个修饰字和限定符都会充分改变匹配和搜索模式。要知 道更多正则表达式的知识,我们推荐 Jeffrey E. F. Friedl 的《Mastering Regular Expressions》一书(O'Reilly & Associates 公司出版)。 表中列出的语法与 Perl5 相同,例外的地方会特别指出。 表 B-1 字符转义 转义码 意义 十六进制值 \a 响铃 \u0007 \b 回退 \u0008 \t 制表 \u0009 \r 回车 \u000A \v 垂直制表 \u000B \f 换页 \u000C \n 换行 \u000D \e ESC \u001B \040 八进制 ASCII \x20 十六进制 ASCII 字符正则表达式 195 表 B-1 字符转义(续) 转义码 意义 十六进制值 \cC ASCII 控制字符 \u0020 十六进制 Unicode 字符 hex \non-escape 非转义符 特例:在正则表达式中, \b 表示单词的边界,而在 []中,\b 表示退格。 表 B-2 替换 表达式 意义 $group-number 用 group-number 替换匹配的子串 ${group-name} 用替换(?) 匹配的子串 替换仅在替换模式内指定。 表 B-3 字符集 表达式 意义 . 匹配任何字符,\n 除外 [characterlist] 匹配列表中的一个字符 [^characterlist] 匹配不在列表中的一个字符 [char0-char1] 匹配范围内的一个字符 \w 匹配一个单词字符,同 [a-zA-Z_0-9] \W 匹配一个非单词字符 \s 匹配空白字符,同 [\n\r\t\f] \S 匹配非空白字符 \d 匹配数字,同 [0-9] \D 匹配非数字附录二196 表 B-4 定位 表达式 意义 ^ 行首 $ 行末 \A 字符开始 \Z 行或字符的结尾 \z 字符结尾 \G 搜索开始位置 \b 单词边界 \B 非单词边界 表 B-5 定量符( quantifier) 定量符 意义 * 0 或更多匹配 + 1 或更多匹配 ? 0 或 1 个匹配 {n} n 个匹配 {n,} 至少 n 个匹配 {n,m} n~m 个匹配 *? 宽松的(Lazy)*,寻找重复次数最少的第一个匹配 +? 宽松的 +, 重复次数最少,至少为 1 ?? 宽松的?, 0 或重复次数最少 {n}? 宽松的{n},n 个匹配 {n,}? 宽松的{n}, 重复次数最少,至少为 n {n,m}? 宽松的{n,m}, 重复次数最少,至少为 n, 不超过 m 表 B-6 分组结构 语法 意义 ( ) 获取匹配子串 (?) 获取匹配子串并放入组 name 中 a正则表达式 197 表 B-6 分组结构(续) 语法 意义 (?) 获取匹配子串并放入组 number 中 a (?) 取消name2的定义, 并将当前组和间隔存入name1; 如 果 name2 未定义,往回匹配; name1 是可选的 a (?: ) 不获取的组 (?imnsx-imnsx: ) 使用或禁用匹配选项 (?= ) 获取子串直至子表达式与右边不匹配 (?! ) 获取子串直至子表达式与右边匹配 (?<= ) 获取子串直至子表达式与左边不匹配 b (? ) 子表达式匹配一次,但不返回 a 其中的尖括号可用单引号替代,如 (?'name')。 b 此结构不能返回,与 Perl5 保持兼容。 注意: 已命名的组获取语法,遵循 Friedl 在《Mastering Regular Expressions》一书中 的建议。其他分组结构,使用 Perl5 语法。 表 B-7 反向引用 参数语法 意义 \count 反向引用发生 count 次 \k 已命名的反向引用 表 B-8 选择( Alternation) 表达式语法 意义 | 逻辑 OR (?(expression)yes|no) 如果 expression 匹配,匹配 yes,否则 no;no 是可选的 (?(name)yes|no) 如果已命名字符串匹配,匹配 yes,否则 no;no 是可选的附录二198 表 B-9 其他结构 表达式语法 意义 (?imnsx-imnsx) 设置或禁用模式中的选项 (?# ) 内联注释 # [到行末] X 模式的注释 表 B-10 正则表达式选项 选项 意义 i 不区分大小写的匹配 m 多行模式;改变 ^ 和 $,以匹配任何行的开头和结尾 n 获取显式已命名和已编号的组 c 编译为 MSIL s 单行模式;改变“ .”的意义,以匹配每个字符 x 从模式中删除未转义的空格 r 从左到右搜索,不能在中间指定199 附录三 格式限定符 表 C-1 列出了 Format 方法支持的预定义数字类型的数字格式限定符(参见第三 章)。 表 C-1 数字格式限定符 限定符 字符串结果 数据类型 C[n] $XX,XX.XX 货币 ($XX,XXX.XX) D[n] [-]XXXXXXX 十进制整数 E[n]或 e[n] [-]X.XXXXXXE+xxx 幂 [-]X.XXXXXXe+xxx [-]X.XXXXXXE-xxx [-]X.XXXXXXe-xxx F[n] [-]XXXXXXX.XX 定点数(两位小数) G[n] 通用或科学 通用 N[n] [-]XX,XXX.XX 数 X[n]或 x[n] 十六进制 十六进制附录三200 下例中只使用了数字格式限定符,而未使用精确限定符(译注 1)。 using System; class TestDefaultFormats { static void Main() { int i = 654321; Console.WriteLine("{0:C}", i); // $654,321.00 Console.WriteLine("{0:D}", i); // 654321 Console.WriteLine("{0:E}", i); // 6.543210E+005 Console.WriteLine("{0:F}", i); // 654321.00 Console.WriteLine("{0:G}", i); // 654321 Console.WriteLine("{0:N}", i); // 654,321.00 Console.WriteLine("{0:X}", i); // 9FBF1 Console.WriteLine("{0:x}", i); // 9fbf1 } } 下例中使用了数字格式限定符以及精确限定符获得 int 值的各种形式。 using System; class TestIntegerFormats { static void Main() { int i = 123; Console.WriteLine("{0:C6}", i); // $123.000000 Console.WriteLine("{0:D6}", i); // 000123 Console.WriteLine("{0:E6}", i); // 1.230000E+002 Console.WriteLine("{0:G6}", i); // 123 Console.WriteLine("{0:N6}", i); // 123.000000 Console.WriteLine("{0:X6}", i); // 00007B i = -123; Console.WriteLine("{0:C6}", i); // ($123.000000) Console.WriteLine("{0:D6}", i); // -000123 Console.WriteLine("{0:E6}", i); // -1.230000E+002 Console.WriteLine("{0:G6}", i); // -123 Console.WriteLine("{0:N6}", i); // -123.000000 Console.WriteLine("{0:X6}", i); // FFFF85 i = 0; Console.WriteLine("{0:C6}", i); // $0.000000 Console.WriteLine("{0:D6}", i); // 000000 Console.WriteLine("{0:E6}", i); // 0.000000E+000 Console.WriteLine("{0:G6}", i); // 0 译注 1: 代码中每一行的注释即为此行的输出结果,以下各例同。格式限定符 201 Console.WriteLine("{0:N6}", i); // 0.000000 Console.WriteLine("{0:X6}", i); // 000000 } } 下例中使用了数字格式限定符以及精确限定符获得 double 值的各种形式。 using System; class TestDoubleFormats { static void Main() { double d = 1.23; Console.WriteLine("{0:C6}", d); // $1.230000 Console.WriteLine("{0:E6}", d); // 1.230000E+000 Console.WriteLine("{0:G6}", d); // 1.23 Console.WriteLine("{0:N6}", d); // 1.230000 d = -1.23; Console.WriteLine("{0:C6}", d); // ($1.230000) Console.WriteLine("{0:E6}", d); // -1.230000E+000 Console.WriteLine("{0:G6}", d); // -1.23 Console.WriteLine("{0:N6}", d); // -1.230000 d = 0; Console.WriteLine("{0:C6}", d); // $0.000000 Console.WriteLine("{0:E6}", d); // 0.000000E+000 Console.WriteLine("{0:G6}", d); // 0 Console.WriteLine("{0:N6}", d); // 0.000000 } } 图形格式限定符 表 C-2列出了 Format方法支持的预定义数字类型的图形( picture)格式限定符 (参见 .NET SDK 中 System.Iformattable 的文档)。 表 C-2 图形格式限定符 限定符 字符串结果 0 0 占位符( placeholder) # 数字占位符 . 小数点附录三202 表 C-2 图形格式限定符(续) 限定符 字符串结果 , 分组符或多组符 % 百分号 E+0, E-0 e+0, e-0 幂符号 \ 字符直接量 'xx'"xx" 字符串直接量 ; 分隔符 下例中使用了数字格式限定符处理 int 值。 using System; class TestIntegerCustomFormats { static void Main() { int i = 123; Console.WriteLine("{0:#0}", i); // 123 Console.WriteLine("{0:#0;(#0)}", i); // 123 Console.WriteLine("{0:#0;(#0);}", i); // 123 Console.WriteLine("{0:#%}", i); // 12300% i = -123; Console.WriteLine("{0:#0}", i); // -123 Console.WriteLine("{0:#0;(#0)}", i); // (123) Console.WriteLine("{0:#0;(#0);}", i); // (123) Console.WriteLine("{0:#%}", i); // -12300% i = 0; Console.WriteLine("{0:#0}", i); // 0 Console.WriteLine("{0:#0;(#0)}", i); // 0 Console.WriteLine("{0:#0;(#0);}", i); // Console.WriteLine("{0:#%}", i); // % } } 下例中使用了数字格式限定符处理 double 值。 using System; class TestDoubleCustomFormats { static void Main() { double d = 1.23;格式限定符 203 Console.WriteLine("{0:#.000E+00}", d); // 1.230E+00 Console.WriteLine( "{0:#.000E+00;(#.000E+00)}", d); // 1.230E+00 Console.WriteLine( "{0:#.000E+00;(#.000E+00);}", d); // 1.230E+00 Console.WriteLine("{0:#%}", d); // 123% d = -1.23; Console.WriteLine("{0:#.000E+00}", d); // -1.230E+00 Console.WriteLine( "{0:#.000E+00;(#.000E+00)}", d); // (1.230E+00) Console.WriteLine( "{0:#.000E+00;(#.000E+00);}", d); // (1.230E+00) Console.WriteLine("{0:#%}", d); // -123% d = 0; Console.WriteLine("{0:#.000E+00}", d); // 0.000E-01 Console.WriteLine( "{0:#.000E+00;(#.000E+00)}", d); // 0.000E-01 Console.WriteLine( "{0:#.000E+00;(#.000E+00);}", d); // Console.WriteLine("{0:#%}", d); // % } } DateTime 格式限定符 表 C-3 列出了 Format 方法支持的合法的 DateTime 类型格式限定符(参见 System.Iformattable 文档)。 表 C-3 DateTime 格式限定符 限定符 字符串结果 D MM/dd/yyyy d dddd, MMMM dd, yyyy f dddd, MMMM dd, yyyy HH:mm F dddd, MMMM dd, yyyy HH:mm:ss g MM/dd/yyyy HH:mm G MM/dd/yyyy HH:mm:ss m, M MMMM dd附录三204 表 C-3 DateTime 格式限定符(续) 限定符 字符串结果 r, R Ddd, dd MMM yyyy HH':'mm':'ss `GMT' s yyyy-MM-dd HH:mm:ss S yyyy-MM-dd HH:mm:ss GMT t HH:mm T HH:mm:ss u yyyy-MM-dd HH:mm:ss U dddd, MMMM dd, yyyy HH:mm:ss y, Y MMMM, yyyy 下例使用定制格式限定符处理 DateTime 值。 using System; class TestDateTimeFormats { static void Main() { DateTime dt = new DateTime(2000, 10, 11, 15, 32, 14); // 输出 "2000-10-11T15:32:14" Console.WriteLine(dt.ToString()); // 输出 "Wednesday, October 11, 2000" Console.WriteLine("{0}", dt); // 输出 "10/11/2000" Console.WriteLine("{0:d}", dt); // 输出 "Wednesday, October 11, 2000" Console.WriteLine("{0:D}", dt); // 输出 "Wednesday, October 11, 2000 3:32 PM" Console.WriteLine("{0:f}", dt); // 输出 "Wednesday, October 11, 2000 3:32:14 PM" Console.WriteLine("{0:F}", dt); // 输出 "10/11/2000 3:32 PM" Console.WriteLine("{0:g}", dt); // 输出 "10/11/2000 3:32:14 PM" Console.WriteLine("{0:G}", dt); // 输出 "October 11" Console.WriteLine("{0:m}", dt); // 输出 "October 11" Console.WriteLine("{0:M}", dt); // 输出 "Wed, 11 Oct 2000 22:32:14 GMT"格式限定符 205 Console.WriteLine("{0:r}", dt); // 输出 "Wed, 11 Oct 2000 22:32:14 GMT" Console.WriteLine("{0:R}", dt); // 输出 "3:32 PM" Console.WriteLine("{0:t}", dt); // 输出 "3:32:14 PM" Console.WriteLine("{0:T}", dt); // 输出 "2000-10-11 22:32:14Z" Console.WriteLine("{0:u}", dt); // 输出 "Wednesday, October 11, 2000 10:32:14 PM" Console.WriteLine("{0:U}", dt); // 输出 "October, 2000" Console.WriteLine("{0:y}", dt); // 输出 "October, 2000" Console.WriteLine("{0:Y}", dt); // 输出 "Wednesday the 11 day of October in the year 2000" Console.WriteLine( "{0:dddd 'the' d 'day of' MMMM 'in the year' yyyy}", dt); } }206 附录四 数据列集 当在运行时环境和已有COM 接口之间相互调用时,CLR将CLR类型的数据自动 列集到兼容的 COM 类型中。 表 D-1 说明了 C# 到 COM 的默认数据类型映射。 表 D-1 C# 到 COM 的类型映射 C# 类型 COM 类型 bool VARIANT_BOOL char unsigned short sbyte Char byte Unsigned char shor Short ushort Unsigned short int Int uint Unsigned int long Hyper ulong Unsigned hyper float Single double Double数据列集 207 表 D-1 C# 到 COM 的类型映射(续) C# 类型 COM 类型 decimal DECIMAL object VARIANT string BSTR System.DateTime DATE 注 System.Guid GUID System.Currency CURRENCY 一维数组 SAFEARRAY 值类型 等价命名的结构 enum 等价命名的枚举 interface 等价命名的接口 class 等价命名的 CoClass 注: COM 数据精确性差一些,会有一些比较上的问题。 表 D-2 说明了 C# 修饰字到等价 COM 接口属性信息的映射。 表 D-2 C# 修饰字到等价 COM 接口属性信息的映射 C# 修饰字 COM 属性信息 <无> [in] out [out] ref [in, out] <返回值> [out, retval]208 附录五 使用配件 本附录将讲述使用配件的技术。主题包括如何创建模块,如何管理全局配件缓存, 如何使配件可共享,如何使用 nmake 工具自动化创建过程。 创建共享的配件 在大多数应用中,都需要创建独立的配件 EXE 或 DLL 这样的组件。要创建多个 程序都能共享的组件,应为配件(使用 sn 工具)取一个共享名(strong name), 并将它安装在共享配件中(使用 al 工具)。 创建模块 al 工具不能从其他配件重新创建配件,因此需要将 C# 文件编译为模块,使用一 定的标志(/target:module)。如下所示(译注 1): csc /target:module a.cs 译注 1: 此命令行将 a.cs 文件编译为模块 a.dll。使用配件 209 将模块与配件链接 一旦模块已经创建,可以用 al (alink)工具创建一个共享的配件。还要为新的配 件和资源模块取名。如下所示(译注 2): al /out:c.dll a.dll b.dll 创建新的配件 现在可以用这个新的配件创建程序了,在命令行中指出就行。如下所示 : csc /r:c.dll d.cs 注意,要使用上例中 c.dll 配件所含名字空间的中的类,需要用 using 关键字指 明该名字空间。 共享配件 如果需要多个程序共享保存目录不同的配件,应将它安装在配件缓存中。将配件 安装在缓存中时,应为其取好共享名。 步骤如下: 1. 生成一个键( key)文件 : sn -k key.snk 2. 创建一个有共享名的配件: al /out:e.dll /keyfile:key.snk a.dll b.dll 3. 共享名取好后,就可以安装到全局配件缓存中了。 注意,将配件安装到共享缓存中,并不能将它拷贝到任何地方,因此在指定编译 对象时,需要为编译器指定配件文件位置: 译注 2: 此命令行将模块 a.dll 和配件 b.dll 链接为共享配件 c.dll。附录五210 csc /r:..\e.dll f.cs 管理全局配件缓存 使用配件缓存有两种方式。一种是 Windows Explorer, 它的 shell 扩展可以显示 缓存并可以操作其中的项。在浏览c:\winnt\assembly目录时, 可以显示当前缓存: start c:\winnt\assembly 另一种方式是使用gacutil工具,它可以用来安装、卸载和列出全局配件缓存的内 容。这个例子: gacutil /i e.dll 将安装 e.dll。 而这个例子: gacutil /u e 卸载了所有名为 e 的配件。 而这个例子: gacutil /u e,ver=0.0.0.0 卸载了名为 e 而且版本号为 0.0.0.0 的配件。 使用 nmake 使用nmake可以使创建配件和模块的任务自动化。下例集中说明了前面的几个命 令行。nmake 特别要注意的特性,是使用 .SUFFIXES 关键字为 .cs 扩展名增加定 义,以及使用 C#编译器的响应文件( response file),这样在有更多源文件名时, 可以都在命令行中列出。使用配件 211 REF=/r:c.dll DEBUG=/debug .SUFFIXES: .exe .dll .cs .cs.dll: csc /t:module $*.cs .cs.exe: csc $(DEBUG) $(REF) @< 命令行时非常有用。注意编译器会隐含地引用 mscorlib.dll,除非使用 /nostdlib 命令行选项。 名字空间 DLL Accessibility Accessibility.dll IIEHost IEHost.dll, IIEHost.dll Microsoft.ASPXCompiler Microsoft.ASPXCompiler.dll Microsoft.ComServices Microsoft.ComServices.dll Microsoft.CSharp cscompmgd.dll Microsoft.JScript Microsoft.JScript.dll Microsoft.VisualBasic Microsoft.VisualBasic.dll Microsoft.VisualBasic. Microsoft.VisualBasic.dll, Compatibility.VB6 Microsoft.VisualBasic.Compatibility.dll Microsoft.VisualBasic.Helpers Microsoft.VisualBasic.dll Microsoft.VisualC Microsoft.VisualC.dll Microsoft.Vsa Microsoft.Vsa.dll Microsoft.Win32 mscorlib.dll名字空间与配件 213 名字空间 DLL Microsoft.Win32.Interop Microsoft.Win32.Interop.dll Microsoft.Win32.Interop. Microsoft.Win32.Interop.dll Trident System mscorlib.dll, System.Net.dll System.CodeDOM System.dll System.CodeDOM.Compiler System.dll System.Collections mscorlib.dll,System.dll System.Collections.Bases System.dll System.ComponentModel mscorlib.dll, System.dll System.ComponentModel.Design System.dll, System.ComponentModel.Design.dll System.Component- System.dll Model.Design.CodeModel System.ComponentModel.Interop System.dll System.Configuration System.Configuration.dll System.Configuration. mscorlib.dll Assemblies System.Configuration.Core System.Configuration.Objects.dll System.Configuration.Design System.Configuration.Design.dll System.Configuration.Install System.Configuration.Install.dll System.Configuration. System.Configuration.dll Interceptors System.Configuration.Internal System.Configuration.dll System.Configuration.Schema System.Configuration.dll, System.Configuration.Objects.dll System.Configuration.Web System.Configuration.Objects.dll System.Core System.dll System.Data System.Data.dll System.Data.ADO System.Data.dll System.Data.ADO.Interop System.Data.dll附录六214 名字空间 DLL System.Data.CodeGen System.Data.dll System.Data.Design System.Data.Design.dll System.Data.Internal System.Data.dll System.Data.SQL System.Data.dll System.Data.SQLTypes System.Data.dll System.Diagnostics mscorlib.dll, System.dll, System.Diagnostics.dll System.Diagnostics.Design System.Diagnostics.dll, System.Diagnostics.Design.dll System.Diagnostics. mscorlib.dll SymbolStore System.DirectoryServices System.DirectoryServices.dll System.Drawing System.Drawing.dll System.Drawing.Design System.Drawing.dll, System.Drawing.Design.dll, System.WinForms.dll System.Drawing.Drawing2D System.Drawing.dll System.Drawing.Imaging System.Drawing.dll System.Drawing.Printing System.Drawing.dll System.Drawing.Text System.Drawing.dll System.Globalization mscorlib.dll System.IO mscorlib.dll, System.IO.dll System.IO.IsolatedStorage mscorlib.dll System.Management System.Management.dll System.Messaging System.Messaging.dll System.Messaging.Design System.Messaging.dll System.Net System.Net.dll System.Net.Sockets System.Net.dll System.Reflection mscorlib.dll System.Reflection.Emit mscorlib.dll名字空间与配件 215 名字空间 DLL System.Resources mscorlib.dll, System.dll, System.WinForms.dll System.Runtime. mscorlib.dll CompilerServices System.Runtime. mscorlib.dll CompilerServices.CSharp System.Runtime. mscorlib.dll InteropServices System.Runtime.Interop- CustomMarshalers.dll Services.CustomMarshalers System.Runtime.Interop- mscorlib.dll Services.Expando System.Runtime.Remoting mscorlib.dll System.Runtime.Remoting. System.Runtime.Remoting.dll Channels.Core System.Runtime.Remoting. System.Runtime.Remoting.dll Channels.HTTP System.Runtime.Remoting. System.Runtime.Remoting.dll Channels.SMTP System.Runtime.Remoting. System.Runtime.Remoting.dll Channels.TCP System.Runtime.Remoting. System.Runtime.Remoting.dll MetadataServices System.Runtime.Remoting. System.Runtime.Remoting.dll Services System.Runtime.Serialization mscorlib.dll System.Runtime. mscorlib.dll Serialization.Formatters System.Runtime.Serializa- mscorlib.dll tion.Formatters.Binary System.Runtime.Serializa- System.Runtime.Serialization. tion.Formatters.Soap Formatters.Soap.dll附录六216 名字空间 DLL System.Security mscorlib.dll System.Security.Cryptography mscorlib.dll, System.Security.dll System.Security.Crypto- mscorlib.dll graphy.X509Certificates System.Security. System.Security.dll Cryptography.Xml System.Security.Permissions mscorlib.dll System.Security.Policy mscorlib.dll System.Security.Principal mscorlib.dll System.ServiceProcess System.ServiceProcess.dll System.Text mscorlib.dll System.Text. System.Text.RegularExpressions.dll RegularExpressions System.Threading mscorlib.dll,System.dll System.Timers System.Timers.dll System.Timers.Design System.Timers.dll System.Web System.Web.dll System.Web.Caching System.Web.dll System.Web.Configuration System.Web.dll System.Web.Handlers System.Web.dll System.Web.Hosting System.Web.dll System.Web.Security System.Web.dll System.Web.Services System.Web.Services.dll System.Web.Services. System.Web.Services.dll Description System.Web.Services.Discovery System.Web.Services.dll System.Web.Services.Interop System.Web.Services.dll System.Web.Services.Protocols System.Web.Services.dll System.Web.SessionState System.Web.dll名字空间与配件 217 名字空间 DLL System.Web.UI System.Web.dll System.Web.UI.Design System.Web.UI.Design.dll System.Web.UI.Design.Util System.Web.UI.Design.dll System.Web.UI.Design. System.Web.UI.Design.dll WebControls System.Web.UI.Design. System.Web.UI.Design.dll WebControls.ListControls System.Web.UI.HtmlControls System.Web.dll System.Web.UI.WebControls System.Web.dll System.Web.UI. System.Web.dll WebControls.Design System.Web.Util System.Web.dll System.WinForms System.WinForms.dll System.WinForms. System.WinForms.dll ComponentModel System.WinForms. System.WinForms.dll ComponentModel.COM2Interop System.WinForms.Design System.WinForms.dll, System.WinForms.Design.dll System.WinForms. System.WinForms.dll PropertyGridInternal System.Xml System.Data.dll,System.XML.dll System.Xml.Serialization System.Xml.Serialization.dll System.Xml.Serialization.IO System.Xml.Serialization.dll System.Xml. Serialization.Schema System.Xml.Serialization.dll System.Xml.XPath System.XML.dll System.Xml.Xsl System.XML.dll WbemClient_v1 wbemclient_v1.dll WbemUtilities_v1 WbemUtilities_v1.dll219 词汇表 access modifier 访问修饰字 activation 激活 assembly 配件 attribute 属性信息 backing store 后备存储 BCL(base class libraries) 基类库 bounds checking 边界检查 boxing 装箱 callback 回调 CCW (COM callable wrapper) COM 可调用包裹器 CLR (Common Language Runtime) 公用语言运行时(环境) CLS (Common Language Specification) 公用语言规范 CTS (Common Type System) 公用类型系统 contract 合约 delegate 委托 exception 异常 finalizer 终结器 GAC (global assembly cache) 全局配件缓存词汇表220 garbage collection (GC) 无用资源回收器 instance 实例 intern 保留 jagged array 不规则数组 JIT (just-in-time) 即时(指必要时才会发生的动作, 如即时编译和即时对象激活等) late binding 迟绑定 lazy quantifier 松散定量器 literal 直接量 lifetime 生命期 locale 地区 manifest (配件的)清单 managed code 受管制代码 marshaler 列集器 metadata 元数据 Microsoft intermediate language (MSIL) Microsoft 中间语言 modifier 修饰字 module 模块 multicast delegate 组播委托 named parameter 已命名参数 namespace 名字空间 .NET Framework .NET 框架(也可简称框架) PInvoke (platform invoke service) 平台调用服务 positional parameter 位置性参数 property 属性 pseudo-custom attribute 伪定制属性信息 quantifier 定量符 RCW(Runtime Callable Wrapper) 运行时可调用包裹器 reference 引用 reflection 反射 regular expression 正则表达式词汇表 221 resource 资源 runtime 运行时 sealed class 密封类 shared name 共享名 signature 签名 SOAP (Simple Object Access Protocol) 简单对象访问协议 specifier 限定符 strong name 即 shared name,共享名 unboxing 拆箱 unmanaged code 未受管制代码 unsafe code 不安全代码 versioning 版本协调
还剩229页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

ged6

贡献于2014-01-09

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!