C#本质论(第4版)

978-7-115-33675-0
作者: 【美】Mark Michaelis tEric Lippert
译者: 周靖
编辑: 杨海玲
分类: C#

图书目录:

详情

这是C#领域中一部广受好评的名作,作者用一种易于理解的方式详细介绍了C#语言的各个方面。全书共有 21章和 3个附录,介绍了 C#语言的基本概念,深入讨论了高级主题,还介绍了LINQ技术,以及与其相关的扩展方法、分部方法、Lambda表达式、标准查询操作符和查询表达式等内容。每章开头的“思维导图”指明本章要讨论的主题,以及各个主题之间的层次关系。

图书摘要

PEARSON

C#本质论(第4版)

[美]Mark Michaelis Eric Lippert 著

周靖 译

人民邮电出版社

北京

图书在版编目(CIP)数据

C#本质论:第4版/(美)米凯利斯(Michaelis,M.),(美)利珀特(Lippert,E.)著;周靖译,--北京:人民邮电出版社,2014.3

书名原文:Essential C#5.0

ISBN 978-7-115-33675-0

Ⅰ.①C… Ⅱ.①米…②利…③周… Ⅲ.①C语言一程序设计 Ⅳ.①TP312

中国版本图书馆CIP数据核字(2013)第273860号

内容提要

这是C#领域中一部广受好评的名作,作者用一种易于理解的方式详细介绍了C#语言的各个方面。全书共有21章和4个附录,介绍了C#语言的数据类型、操作符、方法、类、接口、异常处理等基本概念,深入讨论了泛型、迭代器、反射、线程和互操作性等高级主题,还介绍了LINQ技术,以及与其相关的扩展方法、分部方法、Lambda表达式、标准查询操作符和查询表达式等内容。每章开头的“思维导图”指明本章要讨论的主题,以及各个主题之间的层次关系。为了帮助读者理解各种C#构造,书中用丰富的示例演示每一种特性,而且为每个概念都提供了相应的规范和最佳实践,以确保代码能顺利编译、避免留下隐患,并获得最佳的可维护性。

本书是一本语言参考书,遵循核心C#5.0语言规范,适合对C#感兴趣的各个层次的读者。无论对初学者还是具有一定编程经验的开发者,本书都是一本很有价值的参考书。

◆著 [美]Mark Michaelis Eric Lippert

译 周靖

责任编辑 杨海玲

责任印制 程彦红 杨林杰

◆人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

北京艺辉印刷有限公司印刷

◆开本:787×1092 1/16

印张:42.25

字数:901千字  2014年3月第4版

印数:13501-17000册  2014年3月北京第1次印刷

著作权合同登记号 图字:01-2012-9275号

定价:108.00元

读者服务热线:(010)81055410 印装质量热线:(010)81055316

反盗版热线:(010)81055315

广告经营许可证:京崇工商广字第0021号

版权声明

Authorized translation from the English language edition,entitled Essential C# 5.0,9780321877581 by MICHAELIS,MARK; LIPPERT,ERIC,published by Pearson Education,Inc.,publishing as Addison-Wesley Professional,Copyright © 2013 by Pearson Education,Inc.

All rights reserved.No part of this book may be reproduced or transmitted in any form or by any means,electronic or mechanical,including photocopying,recording or by any information storage retrieval system,without permission from Pearson Education,Inc.

CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD.and POSTS & TELECOM PRESS Copyright © 2014.

本书中文简体字版由Pearson Education Asia Ltd.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签,无标签者不得销售。

版权所有,侵权必究。

其他

献给我的家人Elisabeth、Benjamin、Hanna和Abigail。

感谢你们容忍我花费漫长的时间来写作,

而且经常是在你们最需要我的时候。谢谢你们!

强强联手,造就了这本伟大的C#书籍 !Mark Michaelis的《C#本质论》系列早已成为经典,如今更与著名的C#博主Eric Lippert携手,使之升华为无与伦比的大师级著作。

一般人的印象是Eric写博客,而Mark写书,但我刚认识他们的时候可不是这样的。

2005年LINQ(语言集成查询)公布时,我才刚加入Microsoft,正好见证了PDC会议上的令人激动的公开发布。虽然对技术本身几乎没有什么贡献,但它的宣传造势我可是全程参加了。那个时候人人都在谈论它,宣传小册子满天飞。那是C#和.NET的大日子,至今仍然令人难忘。

但会场的实验室区域却相当安静,在那儿,人们可以自行按照精心编写的实验指南进行技术预览。我就是在那儿遇见Mark的。不用说,他一点儿没有按部就班的意思。他在做自己的实验,梳理文档,和别人沟通,忙着收集自己的照片。

作为C#社区的新人,我在那次会议上见到了许多名人。但老实说,后来都记不大清楚了。唯一记得住的就是Mark。因为当我问他是否喜欢这个新技术时,他不像别人那样马上开始滔滔不绝,而是非常冷静地说:“还不确定,我还没有完全了解它。”他希望完整地理解并消化这一技术,而在这之前,他不希望别人的先入之见影响他的思考。

所以并没有像我预想的那样发生一次快餐式的对话。相反,我们的对话相当坦诚而且颇有裨益。像这样的交流好多年都没有发生了。新技术的细节、造成的后果和存在的问题都涉及了。对我们这些语言设计者来说,Mark是最有价值的社区成员,因为他非常聪明,会打破砂锅问到底,能深刻理解一种技术对于真正的开发人员的影响。但是,最根本的原因可能还是他的坦诚,他从不惧怕说出自己的想法。如果我们开发的某种技术能够通过Mark的测试,就没有什么好担心的了!

这些特质也使Mark成为一名出色的作家。他的文字直指技术的本质,向读者提供最完整的信息,没有废话,能敏锐地看出技术的真正价值和问题。

Eric是C#团队中和我共事7年的同事。他在这里的资格比我老得多。回想第一次见到他时,他正在向团队解释如何理清乱局。确切地说,当时C#编译器代码库正需要在架构上进行一些重大调整,而新功能却很难加入进来——我们迫切需要LINQ来解决这些问题。Eric已经调查好了我们需要有什么样的架构(阶段啊阶段!当时甚至根本就没有这个概念!)。更重要的是,如何逐步跟进。令人惊讶的是,虽然这个主题是如此复杂,作为团队和代码库新人的我,居然立即就明白了他所讲的东西!

从博客就能看出,他讲东西超清楚,而且结构合理,读者能快速地把一个问题搞明白,除了能收获解决方案之外,阅读过程同样令人愉悦。还不止如此!每次Eric钻研一个复杂的问题并与团队分享见解时,他的电子邮件都是那么一丝不苟又充满乐趣。基本上不可能忽视Eric提出的任何问题,因为你会迫不及待地想要看完他的文章。除此之外,他的文字还非常大气!所以,我超喜欢看他的东西,包括经常更新的博客文章。作为C#编译器团队和语言设计团队的一员,他为我们带来的愉悦和帮助真是太多了。

总之,能和这两位出色的伙伴共事,我深感荣幸。Eric帮我理清思路,Mark帮我说出真心话。他们帮助读者认清楚问题并掌握解决方案。通过分别主攻C#的“内”和“外”,本书的这个版本在完整性方面达到了前所未有的高度。没有人能像这两位大师一样帮助你正确地理解C# 5.0。

请好好享用本书!

——Mads Torgersen

微软公司C#项目经理

前言

在软件工程的发展历史中,用于编写计算机程序的方法经历了几次范型的重大转变。每一种范型都是以前一种为基础的,其宗旨都是增强代码的组织,并降低复杂性。本书将带领你体验这样的范型转变过程。

本书开始的几章指导你学习顺序编程结构。在这种编程结构中,语句是按照编写的顺序来执行的。这种结构的问题在于,随着需求的增加,复杂性会按指数级增长。为了降低复杂性,将代码块转变成方法,产生了结构化编程模型。在这种模型中,可以从一个程序中的多个位置调用同一个代码块,而不必在程序中重复这些代码。然而,即使有这种结构,程序还是会很快变得臃肿不堪,需要进行进一步抽象。所以,在此基础上,人们又提出了面向对象编程的概念,这将在第5章进行讨论。在此之后,你将继续学习其他编程方法,比如基于接口的编程和LINQ(以及它促使集合API发生的改变),并最终学习通过特性进行初级的声明性编程 [1](第17章)。

本书有以下3种主要职能。

全面讲述C#语言,其内容已经远远超过了一本简单的教程,为你进行高效率软件开发打下坚实的基础。

对于已经熟悉了C#的读者,本书探讨了一些较为复杂的编程范型,并深入讨论了语言最新版本(C# 5.0和.NET 4.5)的新功能。

它是你永远的案头参考——即便在你精通了这种语言之后。

成功学习C#的关键在于,要尽可能快地开始编程。不要等自己成为一名理论方面的“专家”之后,才开始写代码。所以,不要犹豫,马上开始写程序吧。作为迭代开发 [2]思想的笃信者,我希望即使是一名刚开始学习编程的新手,在学到本书第2章末尾的时候,也能动手开始写基本的C#代码。

有许多主题都没有在本书中进行讨论。你在本书找不到ASP.NET、ADO.NET、智能客户端开发以及分布式编程等主题。虽然这些主题与.NET Framework有关,但它们都值得用专门的书分专题进行讲述。幸运的是对于这些主题,都已经有丰富的图书供读者选择了。本书的重点在于C#以及基类库中的类型。读完本书之后,你在上述任何领域继续深入学习都会有游刃有余的感觉。

本书面向的读者

写作本书时,我面临的一个挑战是如何在持续吸引高级开发人员眼球的同时,不因使用assembly、link、chain、thread和fusion等字眼而打击初学者的信心,否则许多人会误以为这是一本讲冶金而不是程序设计的书 [3]。本书主要读者是已经有一定编程经验,并想多学一种语言来“傍身”的开发者。但我还是认真编排了本书的内容,以便使各种层次的开发者都能够从中获益。

初学者:假如你是编程新手,本书将帮助你从入门级程序员过渡成为C#开发者,消除以后在面临任何C#编程任务时的害怕心理。本书不仅要教会你语法,还要教你养成良好的编程习惯,为将来的编程生涯打下良好基础。

熟悉结构化编程的程序员:学习外语最好的方法就是“沉浸法”[4]。类似地,学习一门计算机语言最好的方法就是在动手中学习,而不是等熟知了它的所有“理论”之后再动手。基于这个前提,本书最开始的内容是那些熟悉结构化编程的开发者很容易上手的。到第4章结束时,这些开发者应该可以开始写基本的控制流程序。然而,要成为真正的C#开发者,记住语法只是第一步。为了从简单程序过渡到企业级开发,C#开发者必须熟练地从对象及其关系的角度来思考问题。为此,第5章的“初学者主题”开始介绍类和面向对象开发。对于C、COBOL和FORTRAN等结构化编程语言,虽然它们仍在发挥作用,但作用会越来越小。所以,软件工程师们应该逐渐开始了解面向对象开发。C#是进行这一思维模式转变的理想语言,因为它本来就是基于“面向对象开发”这一中心思想来设计的。

熟悉“基于对象”和“面向对象”理念的开发者:C++和Java程序员以及许多有经验的Visual Basic程序员都可归于此类。对于分号和大括号,他们可是一点儿都不陌生!简单浏览一下第1章的代码,你会发现,从核心上讲,C#类似于你熟知的C和C++风格的语言。

C#专家:对于已经精通C#的人,本书可供你参考不太常见的语法。此外,对于在其他地方强调较少的一些语言细节以及微妙之处,我提出了自己的见解。最重要的是,本书提供了编写可靠和易维护代码的规范及模式。你教别人学C#时,本书也颇有助益。随着C# 3.0、C# 4.0和C# 5.0的出现,一些最重要的增强包括:

隐式类型的变量(参见第2章);

扩展方法(参见第5章);

分部方法(参见第5章);

匿名类型(参见第11章);

泛型(参见第11章);

Lambda语句和表达式(参见第12章);

表达式树(参见第12章);

标准查询操作符(参见第14章);

查询表达式(参见第15章);

动态编程(参见第17章);

用任务编程库和async进行多线程编程(参见第18章);

用PLINQ进行并行查询处理(参见第18章);

并发集合(第19章)。

考虑到许多人还不熟悉这些主题,因此本书围绕它们展开了详细的讨论。涉及高级C#开发的还有“指针”这一主题,该主题将在第21章讨论。就算是有经验的C#开发者,也未必能很透彻地理解这一主题。

本书特色

本书是一本语言参考书,它遵循核心C#5.0 语言规范。为了帮助读者理解各种C#构造,书中用大量例子演示了每一种特性,而且为每个概念都提供了相应的规范和最佳实践,以确保代码能顺利编译、避免留下隐患,并获得最佳的可维护性。

为了增强可读性,所有代码均进行了特殊格式处理,而且每一章的内容都使用思维导图来概括。

C#编码规范

本书新版本最重大的改进之一就是增加了大量编码规范,例如第16章中的一个规范如下所示:

规范

要确保相等的对象有相同的散列码。

要确保对象的散列码在一个散列表中永不变化。

要确保散列算法快速产生良好分布的散列码。

要确保散列算法在任何可能的对象状态中的健壮性。

一名知道语法的程序员和一名能因时宜地写出最高效代码的专家的区别,关键就是这些编码规范。专家不仅让代码通过编译,还遵循最佳实践,降低产生bug的概率,并使代码的维护变得更容易。编码规范强调了一些关键原则,开发时务必注意。

代码示例

本书大多数代码都能在公共语言基础结构(Common Language Infrastructure,CLI)的任何实现上运行,其中包括Mono、Rotor和Microsoft .NET平台。除了在解释只和某些平台有关的重要概念的地方(例如,解释如何正确处理Windows单线程用户界面),我很少使用平台或厂商特有的库。

下面是一个示例代码清单。

代码清单1-9 变量的声明和赋值

下面简单介绍一下代码格式。

注释以斜体表示。

/*Display a greeting to the console

using composite formatting.*/

关键字加粗。

static void Main()

有的代码被突出显示,是为了指明这些代码与之前列出的有所区别,或是为了演示正文中介绍的概念。

System.Console.Write /* No new line */ (

突出显示的内容可能是一整行,也可能仅仅是一行中的几个字符。

System.Console.WriteLine(

"Your full name is {0} {1}.",

不完整的程序清单包含一个省略号,表示无关的代码已省略。

//…

在代码清单之后,列出了对应的控制台输出,如下例所示:

输出1-4

>HeyYou.exe

Hey you!

Enter your first name: Inigo

Enter your last name: Montoya

执行程序时要由用户输入的内容加粗显示。

虽然提供完整的代码示例可以方便地复制到自己的程序中,但这样做会降低学习特定主题的意义。因此,需要自行修改代码示例,然后再把它们集成到自己的程序中。代码示例中最大的问题是省略了像异常处理这样的错误检查。此外,书中的代码示例没有显式地包含using System语句;在所有的例子中,这个语句都是必需的。

请访问intellitect.com/essentialcsharp和informit.com/mswinseries下载示例代码。[5]

思维导图

每章开头都有一幅思维导图 [6]。作为提纲,它的作用是为读者提供对每章内容的快速参考。下面是一个例子(摘自第5章)。

每一章的主题显示在思维导图的中心,高级主题围绕中心展开。利用思维导图,读者可以方便地搭建自己的知识体系,可以从一个主题出发,更清楚地理解其周边的各个具体概念,避免中途纠缠于一些不相干的枝节问题。

分类解说

根据自己的经验水平,书中特殊的代码块和页面边缘的灰色竖线条可以帮你轻松地找到适合自己的内容。

初学者主题:特别针对入门级程序员提供的定义或解释。

高级主题:可以让有经验的开发者将注意力放在他们最关心的内容上。

标注:用标注框 [7]来强调关键原则,使读者对其重要性一目了然。

语言对比:分散在正文中的补充内容,描述了C#和其他语言的关键差异,为熟悉其他语言的读者提供指引。

本书内容组织

总地说来,软件工程的宗旨就是对复杂性进行管理。本书正是基于这个宗旨来组织内容的。第1章~第4章介绍的是结构化编程,学习了这些内容后,你可以立即开始写一些功能简单的代码。第5章~第9章介绍了C#的面向对象构造,新手应在完全理解了这几章的内容之后,再开始接触本书其余部分更高级的主题。第11章~第13章介绍了更多用于降低复杂性的构造,讲解了当今几乎所有程序都要用到的通用设计模式。在理解了它们之后,你可以更加轻松地理解如何通过反射和特性来实现动态编程。在后续的章节中,将广泛运用反射和特性来实现线程处理和互操作性。

本书最后专门用一章(第21章)来讲述CLI。这一章针对开发平台对C#语言进行描述。之所以要放到最后,是因为它并不是C#特有的,而且不会涉及语法和编程风格问题。不过,本章适合在任何时候阅读,或许最恰当的时机是在阅读了第1章之后。

下面是每一章的内容提要(使用黑体的章号表明那一章的内容在C# 3.0~C#5.0版本中都有)。

第1章─C#概述:这一章在展示了用C#编写的HelloWorld程序之后,进一步剖析了C#。这应当能使读者熟悉C#程序的“外观和感觉”。这一章提供了让读者编译和调试自己的程序所需的足够多的细节。此外,还简单描述了执行C#程序的上下文及其中间语言(intermediate language,IL)。

第2章─数据类型:任何有用的程序都要处理数据,这一章介绍了C#的两种基本数据类型,即值类型和引用类型,另外还讨论了类型间的转换以及C#对数组的支持。

第3章─操作符和控制流:计算机擅长重复性操作,为了利用计算机的这个能力,需要知道如何在自己的程序中添加循环和条件逻辑。这一章还讨论了C#操作符、数据转换以及预处理指令。

第4章─方法和参数:这一章研究了有关方法及其参数的细节,其中包括通过参数来传值、传引用和返回数据。C# 4.0添加了对默认参数的支持,本章解释了如何使用它们。

第5章─类:前面已经学过了类的基本构成元素,这一章将这些构造合并到一起,从而获得具有完整功能的类型。类是面向对象技术的核心,它为一“类”对象定义了模板。

第6章──继承:虽然继承是许多开发者的基本编程手段,但C#提供了独特的构造,比如new修饰符。这一章讨论了继承语法的细节,其中包括重写(overriding)。

第7章─接口:这一章展示了如何利用接口来定义类之间的“可以进行版本控制的交互契约”(versionable interaction contract)。C#同时包含显式和隐式的接口成员实现,可以实现一个额外的封装等级,这是其他大多数语言所不支持的。

第8章─值类型:尽管不如引用类型那么流行,但有些情况下仍然有必要定义行为类似于C#内置基本类型的值类型。这一章要介绍如何定义结构(structure),同时揭示它们的特性。

第9章─合式类型:这一章讨论了一些更高级的类型定义,解释了如何实现操作符,比如“+”和转型操作符,并描述了如何将多个类封装到一个库中。除此之外,这一章还演示了如何定义命名空间和XML注释,并讨论了如何基于垃圾回收机制来设计令人满意的类。

第10章─异常处理:这一章是对第4章引入的异常处理机制的一个延伸讨论,描述了如何利用异常层次结构来创建自定义异常。此外,它还强调了异常处理的一些最佳实践。

第11章─泛型:从某种意义上说,泛型或许是C# 1.0缺少的一个最重要的特性。这一章全面讨论了自2.0引入的这个特性。除此之外,C# 4.0增加了对协变和逆变的支持。本章将在泛型的背景中探讨它们。

第12章─委托和Lambda表达式:正是因为委托,才使C#与其前身语言(C和C++等)有了显著的不同,它定义了在代码中处理事件的模式。这几乎完全消除了写轮询例程的必要。Lambda表达式是使C# 3.0的LINQ成为可能的关键概念。通过这一章的学习,你将知道Lambda表达式是在委托的基础上构建起来的,它提供了比委托更加优雅和简洁的语法。本章的内容是第14章讨论的新的集合API的基础。

第13章─事件:封装起来的委托(称为事件)是公共语言运行时(Common Language Runtime,CLR)的核心构造。本章还探讨了匿名方法,这也是C# 2.0新增的。

第14章─支持标准查询操作符的集合接口:我们通过讨论新的Enumerable类的扩展方法,向你介绍C# 3.0引入的一些简单但又非常强大的改变。Enumerable类使一个全新的集合API成为可能,这个API称为“标准查询操作符”,本章对它进行了详细讨论。

第15章─使用查询表达式的LINQ:如果只使用标准查询操作符,就会形成让人难以辨认的长语句。然而,查询表达式提供了一种类似SQL风格的语法,能够有效地解决这个问题。这一章会详细讨论这种表达式。

第16章─构建自定义集合:在构建用于操纵业务对象的自定义API时,经常都需要创建自定义的集合。本章讨论了具体如何做;同时,还介绍了能使自定义集合的构建变得更简单的上下文关键字。

第17章─反射、特性和动态编程:20世纪80年代末,程序结构的思维模式发生了根本性的变化,面向对象的编程是这个变化的基础。类似地,特性使说明性编程和嵌入元数据成为可能,因而引入了一种新的思维模式。这一章探讨了特性的方方面面,并讨论了如何通过反射机制来获取它们。这一章还讨论了如何通过基类库(Base Class Library,BCL)中的序列化框架来实现文件的输入和输出。C# 4.0增加了一个新的关键字,即dynamic。该关键字将所有类型检查都移至运行时进行,因而极大扩展了C# 能做的事情。

第18章──多线程处理:大多数现代的程序都要求使用线程来执行长时间运行的任务,还要确保对并发的事件进行快速响应。随着程序变得越来越复杂,必须采取其他措施来保护这些高级环境中的数据。多线程应用程序的编写是一项复杂的任务。这一章讨论了如何操纵线程,并讲述了如何采取一些必要的措施来防止将多线程应用程序弄得一团糟。

第19章──线程同步:这一章以第18章为基础,演示了如何利用一些内建的线程处理模式来简化对多线程代码的显式控制。

第20章——平台互操作性和不安全的代码:必须认识到的是,C#是相对年轻的一种语言,许多现有的代码是用其他语言写成的。为了用好这些现有的代码,C#通过P/Invoke提供了对互操作性(非托管代码的调用)的支持。除此之外,C#允许使用指针,也允许执行直接内存操作。虽然使用了指针的代码要求特殊的权限才能运行,但它具有与C风格的API完全兼容的能力。

第21章——CLI:事实上,C#被设计成一种在CLI的顶部工作的最有效的编程语言。这一章讨论了C#程序与底层“运行时”及其规范的关系。

附录A──下载和安装C#编译器与CLI平台:这个附录介绍了如何安装微软.NET和Mono,它们是编译和运行C#代码的基础平台。

附录B──Tic-Tac-Toc源代码清单:第3章和第4章用到的程序在这里提供了完整源代码。

附录C──使用TPL和C# 5.0之前的多线程处理模式:这个附录详细说明了在使用C# 5.0和/或任务并行库(TPL)之前的版本进行开发时的多线程处理模式。

附录D──C# 5.0的Async/Await模式之前的计时器:这个附录描述了在.NET 4.5/C# 5.0之前使用的三种不同类型的计时器。

希望本书成为你学习和掌握C#技能的一个好帮手。另外,以后需要了解C#的一些特殊主题及其内部工作原理的时候,本书也是一本出色的参考书。

——Mark Michaelis

IntelliTect.com/mark

Twitter: @Intellitect,@MarkMichaelis

[1].与声明性编程或者宣告式编程(declarative programming)对应的是命令式编程;前者表述问题,后者实际解决问题。──译者注

[2].简单地说,迭代开发是指分周期、分阶段进行一个项目,以增量方式逐渐对其进行改进的过程。──译者注

[3].上述每个单词在计算机和冶金领域都有专门的含义,所以作者用它们开了一个玩笑。例如,assembly既是“程序集”,也是“装配件”;thread既是“线程”,也是“螺纹”。──译者注

[4].沉浸法,即immersion approach,是指想办法让学习者泡到一个全外语的环境中,比如孤身一人在国外生活或学习。──译者注

[5].本书中文版的资源下载和勘误也通过译者博客(transbot.blog.163.com)提供。——译者注

[6].思维导图,即mind map,又称脑图、心智图,其作用是帮助你学习、组织和存储你想要的所有信息。它以自然的方式对信息进行分类,使你能够立即得到你想要的一切。你可以将其想象成一幅帮助自己记忆和思考的思维路线图。——译者注

[7].标注框,即callout box,是一种需要准确排版的书稿元素。比如:。──译者注

致谢

世界上没有任何一本书是作者单枪匹马就能出版的,在此,我要向此过程中帮助过我的所有人致以衷心的感谢。

表达感激之情的顺序并不重要,我是想到谁就感谢谁。到现在为止,为了让我顺利完成此书,我的家人做出了巨大的牺牲。在Banjamin、Hanna和Abigail眼中,他们的爸爸经常因为此书而无暇顾及他们,但Elisabeth承受的更多。家里的大事小情全靠她一个人,她独自承担家庭的重任。我原本希望本书每一次出版都变得更容易一些,但遗憾的是,实情并非如此。随着孩子们越来越大,生活越来越紧张和忙碌,没有我,Elisabeth几乎时时刻刻都在承受高强度的压力。我感到万分抱歉,谢谢你!

为保证本书技术上的准确性,许多技术编辑对本书中的各章都进行了仔细审阅。我常常惊讶于他们的认真程度,任何不易察觉的小错误都逃不过他们的火眼金睛,他们是Paul Bramsman、Kody Brown、Ian Davis、Doug Dechow、Gerard Frantz、Thomas Heavey、Anson Horton、Brian Jones、Shane Kercheval、Angelika Langer、Eric Lippert、John Michaelis、Jason Morse、Nicholas Paldino、Jon Skeet、Michael Strokesbary、Robert Stokesbary、John Timney和Stephen Toub。还要感谢Mandy Frei辛苦记录大量再版时需要的改动。

Eric给了我太多的惊奇。他对C#术语的掌握程度令人“望而生畏”,我非常欣赏他的修改,尤其是他在术语方面表现出的力求完美。他对C# 3.0相关章节做了不小的改进,在本书的第2版中,我唯一感到遗憾的就是未能让他审阅所有章节。然而,这个遗憾终于得到了弥补。Eric兢兢业业地审阅了这一版的每一章,他审得非常仔细,也非常严谨。正是因为他的辛勤付出,才使本书变得比前两版还要好,我对此致以由衷的感谢。谢谢你,Eric!我想象不出还有谁能比你做得更出色。正因为你,本书才真正实现了从“很好”到“极好”的飞越。

就像Eric之于C#,很少有人像Stephen Toub那样对.NET Framework多线程处理有如此深刻的理解。Stephen专门审阅了重写的(嗯,这是第三次了)关于多线程的两章,并重点检查了C# 5.0的异步支持。谢谢你,Stephen!

感谢Addison-Wesley的所有员工,感谢他们在与我合作期间表现出来的耐心,容忍我将注意力频频转移到书稿之外的其他事情。感谢Elizabeth Ryan、Audrey Doyle、Vicki Rowland、Curt Johnson和Joan Murray。尤其要感谢Joan,虽然很多次我不仅交稿延误,而且回邮件还特别慢,但她却总是富有耐心。

第1章 C#概述

C#是一种成熟的语言,它基于C风格语言(C、C++和Java)的特性而设计,有经验的程序员很快就能熟悉它。[1]作为构建软件组件和应用程序的编程语言,C#是更大、更复杂的执行平台——公共语言基础结构(Common Language Infrastructure,CLI)——的一部分。

本章使用传统HelloWorld程序介绍C#,重点是C#语法基础,包括在C#程序的可执行文件中定义入口。通过本章的学习,将熟悉C#的语法风格和结构,并能写最简单的C#程序。讨论C#语法基础之前,将简单介绍托管执行环境,并解释C#程序在运行时如何执行。本章最后会讨论如何声明变量、如何在控制台上写入和获取数据以及如何为C#代码添加注释。

1.1 Hello,World

学习新语言最好的办法就是动手写代码。第一个例子是经典HelloWorld程序。这个程序在屏幕上显示一些文本。

代码清单1-1展示了完整的HelloWorld程序,在之后的小节中还将介绍代码的编译方法。

代码清单1-1 用C#编写的HelloWorld [2]

class HelloWorld

{

static void Main()

{

System.Console.WriteLine("Hello.My name is Inigo Montoya.");

}

}

注意

C#是区分大小写的语言;大小写不正确,会使代码无法成功编译。

有Java、C或者C++编程经验的读者很快就能看出相似的地方。类似于Java,C#也从C和C++继承了基本的语法 [3]。语法标点(如分号和大括号)、特性(如区分大小写)和关键字(如class、public和void)对于这些程序员来说并不陌生。初学者和有其他语言背景的程序员通过这个程序能很快体会到这些构造的直观性。

编译并运行应用程序

C#编译器允许为C#源代码文件使用任何文件扩展名,但一般使用.cs。将源代码保存到文件后,开发者必须编译它(附录A会指导你安装编译器)。由于C#标准没有规定命令机制,所以实际的编译命令取决于C#编译器的实现。

假定将代码清单1-1的代码放到一个名为HelloWorld.cs的文件中,而且使用的是Microsoft.NET编译器,编译器的路径也已设置好,就可以执行如输出1-1所示的编译命令。[4]

输出1-1

>csc.exe HelloWorld.cs

Microsoft (R) Visual C# Compiler version 4.0.30319.17626

for Microsoft (R) .NET Framework 4.5

Copyright (C) Microsoft Corporation.All rights reserved.

具体输出将取决于所用的编译器的版本。

运行生成的程序HelloWorld.exe,将显示如输出1-2所示的消息。

输出1-2

>HelloWorld.exe

Hello.My name is Inigo Montoya.

C#编译器创建的程序HelloWorld.exe是一个程序集(assembly)。虽然在这个例子中创建的是一个能独立运行的程序,但也可以不这么做,而是创建能由另一个较大的程序引用的代码库。库(或称为类库)的文件扩展名是.dll,其中dll代表“动态链接库”(Dynamic Link Library,DLL)。库也是一个程序集。换言之,一次成功的C#编译生成的肯定是程序集,无论它是程序还是库。

语言对比:Java——文件名必须和类名匹配

在Java中,文件名必须和类名一致。C#虽然也常常遵守这一约定,但却不是必需的。在C#中,一个文件可以包含多个类;而且从C# 2.0开始,一个类的代码可以拆分到多个文件中。

1.2 C#语法基础

成功编译并运行HelloWorld程序之后,我们来分析代码,了解它的各个组成部分。首先熟悉一下C#关键字以及开发者选用的标识符。

初学者主题:关键字

为了帮助编译器解释代码,C#中的某些单词具有特殊地位和含义,我们将其称为关键字。关键字提供了具体的语法,编译器根据这些语法来解释程序员书写的表达式。在HelloWorld程序中,class、static和void均是关键字。

编译器利用关键字来识别代码的结构与组织方式。因为编译器对这些单词有着严格的解释,所以开发人员只能按照C#的语言规则将关键字放在特定的位置。一旦程序员违反规则,编译器就会报错。

1.2.1 C#关键字

表1-1总结了C#关键字。

续表

* 上下文关键字,括号中的数字代表加入该上下文关键字的版本。

C# 1.0之后没有引入任何新的保留关键字,但在后续版本中,一些构造使用了上下文关键字(contextual keyword),它们在特定位置才有意义。除了那些位置,上下文关键字没有任何特殊意义。[5]这样所有C# 1.0代码都完全兼容于后续的版本。[6]

1.2.2 标识符

和其他语言一样,C#用标识符标识程序员编写代码的构造。在代码清单1-1中,HelloWorld和Main都是标识符。分配标识符之后,以后就能用它引用所标识的构造。因此,开发人员应分配有意义的名称,不要随意分配。

好的程序员总能选择简洁而有意义的名称,这使代码更容易理解和重用。清晰和一致是如此重要,以至于.NET Framework Guidelines建议不要在标识符中使用单词缩写 [7],甚至不要使用不被广泛接受的首字母缩写词。即使被广泛接受(如HTML),使用时也要一致。不要忽而这样用,忽而那样用。为避免滥用,可限制所有首字母缩写词都必须包含到术语表中。总之,要选择清晰(甚至是详细)的名称,尤其是在团队中工作,或者开发别人要使用的库的时候。

标识符有两种基本的大小写风格。第一种风格是CLI创建者所谓的Pascal大小写(PascalCase),它在Pascal编程语言中很流行,要求标识符中每个单词的首字母大写,例如ComponentModel、Configuration和HttpFileCollection。注意在HttpFileCollection中,由于首字母缩写词HTTP的长度超过两个字母,所以仅首字母大写。第二种风格是camel大小写(camelCase),即除了第一个字母小写,其他约定与Pascal大小写风格一样,例如quotient、firstName、httpFileCollection、ioStream和theDreadPirateRoberts。

规范

要更注重标识符的清晰而不是简短。

不要在标识符名称中使用单词缩写。

不要使用不被广泛接受的首字母缩写词,即使被广泛接受,非必要也不要用。

下划线虽然合法,但标识符中一般不要包含下划线、连字号或其他非字母/数字字符。此外,C#不像其前辈那样使用匈牙利命名法(为名称附加类型缩写前缀)。这避免了数据类型改变时还要重命名变量,也避免了数据类型前缀经常不一致的情况。

在极少数情况下,有的标识符(如Main)可能在C#语言中具有特殊含义。

规范

要把只包含两个字母的首字母缩写词全部大写,除非它是camel大小写风格标识符的第一个单词。

包含三个或更多字母的首字母缩写词,仅第一个字母才要大写,除非该缩写词是camel大小写风格标识符的第一个单词。

在camel大小写风格标识符开头的首字母缩写词中,所有字母都不要大写。

不要使用匈牙利命名法(也就是,不要为变量名称附加类型前缀)。

高级主题:关键字

虽然罕见,但关键字附加“@”前缀可作为标识符使用,例如可命名局部变量@return。类似地(虽不符合C#大小写规范),可命名方法@throw()。

在微软的实现中,还有4个未文档化的保留关键字,即__arglist、__makeref、__reftype和__refvalue。它们仅在罕见的互操作情形下才会用到,平时完全可以忽略。注意这4个特殊关键字以双下划线开头。C#设计者保留将来把这种标识符转化为关键字的权利。为安全起见,开发人员自己不要创建这样的标识符。

1.2.3 类型定义

C#中所有代码都出现在一个类型定义的内部,最常见的类型定义是以关键字class开头的。如代码清单1-2所示,类定义(class definition)是class <标识符> { ...}形式的一个代码块。

代码清单1-2 基本的类声明

class HelloWorld

{

//...

}

类型的名称(本例是HelloWorld)可以随便取,但根据约定,它应当使用Pascal大小写风格。就本例来说,可以选择的名称包括Greetings、HelloInigoMontoya、Hello或者简单地称为Program。(对于包含Main()方法的类,Program是个很好的名称。Main()方法将在稍后详述。)

规范

要用名词或名词短语命名类。

要为所有类名使用Pascal大小写风格。

一个程序通常包含多个类型,每个类型都包含多个方法。

1.2.4 Main

初学者主题:什么是方法?

从语法上说,C#程序中的方法是一个已命名的代码块,该代码块由一个方法声明(如static void Main())引入,后跟一对大括号({}),其中包含零或多条语句。方法可以执行计算和/或操作。与书写语言中的段落相似,方法提供了结构化和组织代码的一种方式,使之更易读。更重要的是,方法可以重用,可以在多个地方调用,所以避免了代码的重复。方法声明除了负责引入方法之外,还要定义方法名以及要传入和传出方法的数据。在代码清单1-3中,Main()后跟{ ...}便是一个C#方法的例子。

C#程序从Main方法开始执行。该方法以static void Main()开头。在命令控制台中输入HelloWorld.exe执行程序,程序会启动并解析Main的位置,然后执行其中第一条语句,如代码清单1-3所示。

代码清单1-3 HelloWorld分解示意图

虽然Main方法声明可以进行某种程度的改变,但关键字static和方法名Main是始终都是程序必需的。

高级主题:Main方法的声明

C#要求Main方法的返回类型为void或int,而且要么不带参数,要么接收一个字符串数组作为参数。代码清单1-4展示了Main方法的完整声明。

代码清单1-4 带有参数和返回类型的Main方法

static int Main(string[] args)

{

//...

}

args参数是一个字符串数组,用于接收命令行参数。但数组第一个元素不是程序名称,而是可执行文件名称 [8]之后的第一个命令行参数,这一点与C和C++不同。要获取执行程序所用的完整命令,可以使用System.Environment.CommandLine。

Main返回的int值是状态码,标识程序执行是否成功。返回非零值通常意味着错误。

语言对比:C++/Java—— main()是全部小写的

与C风格的“前辈们”不同,C#的Main方法名使用大写M,以便与C#的Pascal大小写风格命名约定保持一致。

将Main方法指定为static意味着这是“静态”方法,可用“类名.方法名”的形式调用它。如果不指定static,用于启动程序的命令控制台还要先对类进行实例化(instantiation),然后才能调用方法。第5章会用一节的篇幅专门讲述静态成员。

Main()之前的void表明该方法不返回任何数据(第2章会进一步解释)。

C#和C/C++一样使用大括号封闭构造(如类或者方法)的主体。例如,Main方法的主体就是用大括号封闭起来的。在本例中,方法的主体只有一条语句。

1.2.5 语句和语句分隔符

Main方法只包含一条语句,即System.Console.WriteLine();,它在控制台上输出一行文本。C#通常用分号标识语句结束,每条语句都由代码要执行的一个或多个行动构成。声明变量、控制程序流程或者调用方法,所有这些都是语句的例子。

语言对比:Visual Basic——基于行的语句

有的语言以行为基本单位,这意味着不加上特殊标记,语句便不能跨行。在Visual Basic 2010以前,Visual Basic一直是典型的基于行的语言。它要求在行末添加下划线表示语句跨越多行。从Visual Basic 2010开始,行连续符在许多时候都变成可选的。

高级主题:没有分号的语句

C#的许多编程元素都以分号结尾。不要求使用分号的例子是switch语句。由于大括号总是包含在switch语句中,所以C#不要求语句后加上分号。事实上,代码块本身就被视为语句(它们也由语句构成),不要求以分号结尾。类似地,有的编程元素(如using指令)虽然末尾有分号但不被视为语句。

由于换行与否不影响语句的分隔,所以可以将多条语句放到同一行,C#编译器会认为这一行包含多条指令。例如,代码清单1-5在同一行包含了两条语句。执行时,它们会在控制台窗口中分两行显示Up和Down。

代码清单1-5 一行中包含多条语句

System.Console.WriteLine("Up");System.Console.WriteLine("Down");

C#还允许一条语句跨越多行。同样地,C#编译器会根据分号判断语句的结束位置。代码清单1-6展示了一个例子。

代码清单1-6 一条语句跨越多行

System.Console.WriteLine(

"Hello.My name is Inigo Montoya.");

代码清单1-6的WriteLine()语句的原始版本来自HelloWorld程序,它在这里跨越了多行。

1.2.6 空白

分号使C#编译器能忽略代码中的空白。除了少许例外情况,C#允许在代码中随意插入空白而不改变其语义。在代码清单1-5和代码清单1-6中,在语句中或语句间换行都可以,对编译器最终创建的可执行文件没有任何影响。

初学者主题:什么是空白?

空白是一个或多个连续的格式字符(如制表符、空格和换行符)。删除单词间的所有空白肯定会造成歧义。删除引号字符串中的任何空白也会。

程序员经常利用空白对代码进行缩进来增强可读性。来看看代码清单1-7和代码清单1-8展示的两个版本的HelloWorld程序。

代码清单1-7 不缩进

class HelloWorld

{

static void Main()

{

System.Console.WriteLine("Hello Inigo Montoya");

}

}

代码清单1-8 删除一切可以删除的空白

class HelloWorld{static void Main()

{System.Console.WriteLine("Hello Inigo Montoya");}}

虽然这两个版本看起来和原始版本颇有不同,但C#编译器认为这几个版本的代码没有任何区别。

初学者主题:用空白来格式化代码

为了增强可读性,利用空白对代码进行缩进是非常重要的。写代码时要遵循已经建立的编码标准和约定,以增强代码的可读性。

本书约定每个大括号都单独占一行,并缩进大括号之间的代码。假如一对大括号之间有第二对大括号,那么第二对大括号中的所有代码也要缩进。

这不是统一的C#标准,只是一种风格偏好。

1.2.7 使用变量

前面已接触了最基本的C#程序,下面让我们来声明一个局部变量。变量声明后就可以被赋值,将值替换成新值,并可在计算和输出等操作中使用。然而,变量一经声明,数据类型就无法改变。在代码清单1-9中,string max就是一个变量声明。

代码清单1-9 变量的声明和赋值

初学者主题:局部变量

变量是一个存储位置的符号名称,程序以后可以对这个存储位置进行赋值和修改操作。局部意味着是在方法内部声明变量。

声明变量就是定义它,需要:

a.指定变量要包含的数据的类型;

b.为它分配标识符(变量名)。

1.2.8 数据类型

代码清单1-9声明了string类型的变量。本章还使用了int和char。

int是指C#的32位整型。

char是字符类型,长度16位,足以表示无代理项的Unicode字符 [9]

下一章将更详细地探讨这些以及其他常见数据类型。

初学者主题:什么是数据类型?

一个变量声明所指定的数据的类型称为数据类型。数据类型,或者简称为类型,是具有相似特征和行为的个体的分类。例如,animal(动物)就是一个类型,它对具有动物特征(多细胞、具有运动能力等)的所有个体(猴子、野猪和鸭嘴兽等)进行了分类。类似地,在编程语言中,类型是被赋予了相似特性的一些个体的定义。

1.2.9 变量的声明

在代码清单1-9中,string max是一个变量声明,它声明了一个名为max的string类型的变量。还可以在同一条语句中声明多个变量,办法是指定数据类型一次,然后用逗号分隔每个标识符,如代码清单1-10所示。

代码清单1-10 在一条语句中声明两个变量

string message1,message2;

由于声明多个变量的语句只允许开发者提供一次数据类型,因此所有变量都具有相同类型。

在C#中,变量名可以用任何字母或者下划线(_)开头,后跟任意数量的字母、数字和/或下划线。但根据约定,局部变量名采用的是camel大小写风格命名(即除了第一个单词,其他每个单词的首字母大写),而且不包含下划线。

规范

要为局部变量使用camel大小写风格的命名。

1.2.10 变量的赋值

局部变量声明后必须在引用之前为其赋值。一个办法是使用=操作符,或者称为简单赋值操作符。操作符是一种特殊符号,标识了代码要执行的操作。代码清单1-11演示了如何利用赋值操作符指定变量max 1和valerie要指向的字符串值。

代码清单1-11 更改变量的值

class MiracleMax

{

static void Main()

{

string valerie;

string max = "Have fun storming the castle!";

valerie = "Think it will work?";

System.Console.WriteLine(max);

System.Console.WriteLine(valerie);

max = "It would take a miracle.";

System.Console.WriteLine(max);

}

}

[10]个代码清单可以看出,既可以在声明变量的同时对它赋值(如变量max),也可以在声明了变量之后用另一条语句赋值(如变量valerie)。要赋的值必须放在赋值操作符右侧。

运行编译好的MiracleMax.exe程序,会生成如输出1-3所示的结果。

输出1-3

>MiracleMax.exe

Have fun storming the castle!

Think it will work?

It would take a miracle.

C#要求局部变量在读取之前“明确赋值”。此外,一次赋值会返回一个值。所以,C#允许在同一条语句中进行多个赋值操作,如代码清单1-12所示。

代码清单1-12 赋值会返回一个值,该值可用于再次赋值

class MiracleMax

{

static void Main()

{

// ...

string requirements,max;

requirements = max = "It would take a miracle.";

// ...

}

}

1.2.11 变量的使用

赋值后就能用变量标识符引用值。因此,在System.Console.WriteLine(max)语句中使用变量max时,程序在控制台上显示Have fun storming the castle!,也就是max的值。更改max的值并执行相同的System.Console.WriteLine(max)语句,会显示max的新值,即It would take a miracle.。

高级主题:字符串不可变

所有string类型的数据,不管是不是字符串字面量(literal)[11],都是不可变的(或者说是不可修改的)。例如,不可能将字符串"Come As You Are"更改为"Come As You Age"。也就是说,不能修改变量最初引用的数据,只能重新赋值,让它指向内存中的新位置。

1.3 控制台输入和输出

本章已多次使用System.Console.WriteLine将文本输出到命令控制台。除了能输出数据,程序还需要能接收用户输入的数据。

1.3.1 从控制台获取输入

获取用户在控制台输入文本的一种方法是使用System.Console.ReadLine()。此方法将暂停程序执行以便用户输入字符。一旦用户按回车键创建新的一行,程序就会继续执行。System.Console.ReadLine()方法的输出,也称为返回值,就是用户输入的文本字符串。下面来看一下代码清单1-13以及输出1-4。

代码清单1-13 使用System.Console.ReadLine()

class HeyYou

{

static void Main()

{

string firstName;

string lastName;

System.Console.WriteLine("Hey you!");

System.Console.Write("Enter your first name: ");

firstName = System.Console.ReadLine();

System.Console.Write("Enter your last name: ");

lastName = System.Console.ReadLine();

...

}

}

输出1-4

>HeyYou.exe

Hey you!

Enter your first name: Inigo

Enter your last name: Montoya

在每条提示信息之后,程序都用System.Console.ReadLine()方法获取用户输入的文本,并将其赋给恰当的变量。在第二个System.Console.ReadLine()赋值操作完成之后,firstName引用值Inigo,而lastName引用值Montoya。

高级主题:System.Console.Read()

除了System.Console.ReadLine()还有System.Console.Read()方法。但System.Console.Read()方法返回的是与读取的字符值对应的整数,如果没有更多的字符可用,就返回-1。为了获取实际字符,需要先将整数转型为字符,如代码清单1-14所示。

代码清单1-14 使用System.Console.Read()

int readValue;

char character;

readValue = System.Console.Read();

character = (char) readValue;

System.Console.Write(character);

注意,除非用户按回车键,否则System.Console.Read()方法不会返回输入。按回车键之前不会对字符进行处理,即使用户已经输入了多个字符。

C# 2.0新增了System.Console.ReadKey()方法。它和System.Console.Read()方法不同,返回的是用户所按键输入。可用它拦截用户按键操作,并执行相应行动,比如校验按键,限制只能按数字键。

1.3.2 将输出写入控制台

代码清单1-13中是用System.Console.Write()而不是System.Console.WriteLine()方法提示用户输入他的名和姓。System.Console.Write()方法不在显示文本之后添加一个换行符,而是将当前位置保持在同一行上。这样用户输入的内容就会与提示内容处于同一行。代码清单1-13的输出清楚地演示了System.Console.Write()的效果。

下一步是将使用System.Console.ReadLine()获取的值写回控制台。在代码清单1-15所示的例子中,程序将在控制台上输出用户的全名。但这段代码使用的是System.Console.WriteLine()的一个变体。输出1-5展示了对应的输出。

代码清单1-15 使用System.Console.WriteLine()进行格式化

class HeyYou

{

static void Main()

{

string firstName;

string lastName;

System.Console.WriteLine("Hey you!");

System.Console.Write("Enter your first name: ");

firstName = System.Console.ReadLine();

System.Console.Write("Enter your last name: ");

lastName = System.Console.ReadLine();

System.Console.WriteLine(

"Your full name is {0} {1}.",firstName,lastName);

}

}

输出1-5

Hey you!

Enter your first name: Inigo

Enter your last name: Montoya

Your full name is Inigo Montoya.

在代码清单1-15所示的例子中,不是先用Write语句输出your full name is,再用Write语句输出firstName,然后用第三条Write语句输出空格,最后用WriteLine语句输出lastName。相反,这个例子利用复合格式化(composite formatting)来进行一次性输出。在复合格式化中,代码首先提供格式字符串(format string)来定义输出格式。本例的格式字符串是"Your full name is {0} {1}."。它为要在字符串中插入的数据标识了两个索引占位符。

注意索引值是从零开始的。每个要插入的参数,或者称为格式项(format item),按照与索引值对应的顺序排列在格式字符串之后。在本例中,由于firstName是紧接在格式字符串之后的第一个参数,所以它对应于索引值0。类似地,lastName对应于索引值1。

注意,占位符在格式字符串中不一定按顺序出现。例如,代码清单1-16中的代码交换了两个索引占位符的位置并添加了一个逗号,从而改变了姓名的显示方式(参见输出1-6)。

代码清单1-16 交换索引占位符和对应的变量

System.Console.WriteLine("Your full name is {1},{0}",

firstName,lastName);

输出1-6

Hey you!

Enter your first name: Inigo

Enter your last name: Montoya

Your full name is Montoya,Inigo

占位符除了能在格式字符串中按任意顺序出现之外,同一个占位符还能在一个格式字符串中多次使用。另外,也可以忽略占位符。然而,每个占位符都必须对应一个参数,不能使用没有对应参数的占位符。

1.3.3 注释

本节将修改代码清单1-15中的程序,在其中添加注释。提供注释不会改变程序的执行,只是使代码变得更容易理解。代码清单1-17中展示了新的代码,输出1-7展示了对应的输出。

代码清单1-17 在代码中添加注释

输出1-7

Hey you!

Enter your first name: Inigo

Enter your last name: Montoya

Your full name is Inigo Montoya.

虽然插入了注释,但编译和执行新程序之后生成的输出和以前是一样的。

程序员使用注释来描述和解释他们写的代码,尤其是在语法本身难以理解的时候,或者是在另辟蹊径实现一个算法的时候。只有检查代码的程序员才需要看注释,编译器会忽略注释,因而生成的程序集中看不到源代码中的注释的一丝踪影。

表1-2总结了4种不同的C#注释。代码清单1-17使用了其中的两种。

第9章将更全面地讨论XML注释。届时将讨论XML标准的各种XML标记。

编程史上,确实有一段时期认为,如果一个程序员编写的代码没有详尽的注释,都不好意思说自己是专业和有经验的程序员。但是时代变了。没有注释但可读性好的代码,比需要注释才能说清楚的代码更具价值。如果开发人员发现需要写注释才能说清楚代码块的功用,应该考虑更清楚地重写代码,而不是洋洋洒洒写一堆注释。写注释其实就是重复代码思路的过程。不仅会造成混乱,降低可读性,还很容易过时,因为将来可能更改代码却没有及时更新注释。

规范

不要使用注释,除非代码本身“一言难尽”。

要尽量编写清晰的代码,而不是通过注释澄清复杂的算法。

初学者主题:XML

XML(Extensible Markup Language,可扩展标记语言)是一种简单而又灵活的文本格式,常用于Web应用程序以及应用程序间的数据交换。XML之所以是“可扩展”的,是因为XML文档中包含的是对数据进行描述的信息,也就是所谓的元数据(metadata)。下面是一个示例XML文件:

<?xml version="1.0" encoding="utf-8" ?>

<body>

<book title="Essential C# 5.0">

<chapters>

<chapter title="Introducing C#"/>

<chapter title="Operators and Control Flow"/>

...

</chapters>

</book>

</body>

文件以Header元素开始,它描述了XML文件的版本和字符编码方式。之后是“book”元素。元素以尖括号中的单词开头,比如<body>。为了结束元素,要将同一个单词放在尖括号中,同时为单词添加一个正斜杠前缀,比如</body>。除了元素,XML还支持属性。title="Essential C# 5.0"就是XML属性的例子。注意,XML文件包含了对数据(如“Essential C# 5.0”、“Operators and Control Flow”等)进行描述的元数据(书名、章名等)。这可能形成相当臃肿的文件,但优点是提供了描述来帮助解释数据。

1.3.4 托管执行和公共语言基础结构

处理器不能直接解释程序集。程序集采用的是另一种语言,即公共中间语言(Common Intermediate Language,CIL),或简称为中间语言(IL)[12]。C#编译器将C#源代码文件转换成这种中间语言。为了将CIL代码转换成处理器能理解的机器码,还要完成一个额外的步骤(通常在执行时进行)。该步骤涉及C#程序执行中的一个重要元素:VES(Virtual Execution System,虚拟执行系统)。VES偶尔也被人称为运行时,它根据需要编译CIL代码,这个过程称为即时编译或者JIT编译(just-in-time compilation)。假如代码在像“运行时”这样的一个“代理”的上下文中执行,就将这些代码称为托管代码(managed code),而在“运行时”的控制下执行的过程称为托管执行(managed execution)。之所以称为托管代码,是因为“运行时”管理着诸如内存分配、安全性和JIT编译等方面,从而控制了主要的程序行为。执行过程中不需要“运行时”的代码称为本机代码(native code)或者非托管代码(unmanaged code)。

注意

“运行时”既可能指“程序执行的时候”,也可能指“虚拟执行系统”。为了明确起见,本书使用“执行时”来表示“程序执行的时候”,用“运行时”表示负责管理C#程序执行的代理。

VES规范被包含在一个包容面更广的规范——CLI(Common Language Infrastructure,公共语言基础结构)规范 [13]中,作为国际标准,CLI包含了以下几方面的规范。

VES或“运行时”。

CIL。

为语言互操作性提供支持的类型系统,称为CTS(Common Type System,公共类型系统)。

如何编写能通过CLI兼容语言访问的库的指导原则,这部分内容具体放在公共语言规范(Common Language Specification,CLS)中。

使各种服务能被CLI识别的元数据(包括程序集的布局或文件格式规范)。

一个公共编程框架,称为基类库(Base Class Library,BCL),所有语言的开发者都能利用它。

如果是在一个CLI实现的上下文中运行,程序员不需要直接写代码就能使用大量服务和功能,包括如下几个。

语言互操作性:不同源语言间的互操作性。语言编译器将每种源语言转换成相同中间语言(CIL)来实现这种互操作性。

类型安全:检查类型间转换,确保只在兼容类型之间转换。这有助于防止发生缓冲区溢出——安全问题的一个主要诱因。

代码访问安全性:程序集开发者的代码有权在计算机上执行的证明。

垃圾回收:一种内存管理机制,自动释放“运行时”为数据分配的空间。

平台可移植性:允许程序集在多种操作系统中运行。要做到这一点,一个显而易见的限制就是不能使用平台相关的库。所以,同Java一样,不可避免地有一些“个性”问题需要解决。

BCL:提供开发者能够(在所有CLI实现中)依赖的大型代码库,使他们不必亲自编写这些代码。

注意

本节只是简单介绍了CLI,目的是让你熟悉C#程序的执行环境。此外,本节还提及了本书后面将会用到的一些术语。第21章将专门探讨CLI及其与C#开发者的关系。虽然那一章在本书的最后,但它的内容实际上并不依赖于之前的任何一章。所以,假如想多了解一下CLI,随时都能直接翻到那一章。

1.3.5 C#和.NET版本

你大概注意到了输出1-1中的“.NET Framework 4.5”字样。截至本书写作时为止,微软对.NET Framework进行了6次大的升级,但C#编译器只进行了5次大的升级。.NET Framework 3.0是在C#编译器的两次大的升级之间(以及Visual Studio 2005和2008之间)发布的,它补充了许多新的API。结果,和C# 3.0对应的.NET Framework版本是3.5。C# 4.0和.NET Framework 4.0发布时,两者的版本号再次同步。但和C# 5.0对应的是.NET Framework 4.5,所以现在的版本号又不同步了。表1-3简单总结了C#和.NET的版本。

只要编译器版本和代码要求的版本匹配,本书大多数代码都能在非微软平台上运行。虽然提供每种C#平台的完整细节能帮到一些人,但也会使人分心,所以本书最终只包含和Microsoft .NET平台有关的细节。原因很简单,微软公司的实现到目前为止仍占统治地位。而且即使需要转移到其他平台,转移的过程也相当容易。

.NET 4.5新增的“重头戏”是支持调用Windows Runtime(WinRT)中的组件。WinRT内部都是一些本机代码(native code),提供了.NET的一个全新执行环境。但无论API风格,还是“运行时”的行为,都像极了.NET。出于现实考虑,WinRT中的组件是一套全新API——大多数功能都和.NET Framework重叠。区别是新API只能在Windows 8上使用,而且都是从无到有设计出来的,旨在提供与.NET相似的编程体验,提供以前只能通过.NET包装器由“Win32”API提供的功能。虽然是“重头戏”,但作为平台的WinRT和学习.NET/C#基本无关。所以本书不打算深入讨论它。

1.3.6 CIL和ILDASM

前面说过,C#编译器将C#代码转换成CIL代码而不是机器码。处理器只能理解机器码,因此CIL代码必须先转换成机器码才能由处理器执行。给定一个程序集(DLL文件或可执行文件),可以使用CIL反汇编程序将其析构成对应的CIL表示,从而查看其CIL代码。通常使用微软特有的文件名ILDASM来称呼这种CIL反汇编程序(ILDASM是IL Disassembler的简称),它能对程序或者它的类库执行反汇编,显示由C#编译器生成的CIL代码。

在不同的CLI实现中,使用CIL反汇编程序的命令也有所区别。可以像输出1-8展示的那样,在命令行中执行.NET CIL反汇编程序:

输出1-8

>ildasm /text HelloWorld.exe

使用/text选项的目的是让输出在命令控制台上显示,而不是在新窗口中显示。执行上述命令得到的输出流是HelloWorld.exe程序所含CIL代码的一个“转储”(dump)。CIL代码比机器码更容易理解。许多开发人员害怕即使别人没有拿到源代码,程序也容易被反汇编并曝光其算法。

无论是否基于CLI,任何程序防止反编译唯一安全的方法就是禁止访问编译好的程序(例如,只在网站上存放程序,而不把它分发到用户机器上)。但假如目的只是减小别人获得源代码的可能性,可以考虑使用一些混淆器(obfuscator)产品。这些混淆器会打开IL代码,将代码加密成一种功能不变但更难于理解的形式。这可以防止普通开发者访问代码,使程序集难以被反编译成容易理解的代码。除非程序需要对算法进行高级安全防护,否则混淆器足矣。

高级主题:HelloWorld.exe的CIL输出

代码清单1-18展示了ILDASM创建的CIL代码。

代码清单1-18 示例CIL输出

// Microsoft (R) .NET Framework IL Disassembler. Version 4.0.30319.17369

// Copyright (c) Microsoft Corporation. All rights reserved.

// Metadata version: v4.0.30319

.assembly extern mscorlib

{

.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )             //

.z\V.4..

.ver 4:0:0:0

}

.assembly HelloWorld

{

.custom instance void [mscorlib]System.Runtime.CompilerServices.Compilat

ionRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 )

.custom instance void [mscorlib]System.Runtime.CompilerServices.Runtime

CompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F

6E 45 78  // ....T..WrapNonEx

63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 )    // ceptionThrows.

.hash algorithm 0x00008004

.ver 0:0:0:0

}

.module HelloWorld.exe

// MVID: {D229AC10-1DEC-47A1-AA62-3BA19389E37E}

.imagebase 0x00400000

.file alignment 0x00000200

.stackreserve 0x00100000

.subsystem 0x0003    // WINDOWS_CUI

.corflags 0x00000001  // ILONLY

// Image base: 0x00490000

// =============== CLASS MEMBERS DECLARATION ===================

.class private auto ansi beforefieldinit HelloWorld

extends [mscorlib]System.Object

{

.method private hidebysig static void Main() cil managed

{

.entrypoint

// Code size    13 (0xd)

.maxstack 8

IL_0000: nop

IL_0001: ldstr   "Hello.My name is Inigo Montoya."

IL_0006: call    void [mscorlib]System.Console::WriteLine(string)

IL_000b: nop

IL_000c: ret

} // end of method HelloWorld::Main

.method public hidebysig specialname rtspecialname

instance void .ctor() cil managed

{

// Code size    7 (0x7)

.maxstack 8

IL_0000: ldarg.0

IL_0001: call    instance void [mscorlib]System.Object::.ctor()

IL_0006: ret

} // end of method HelloWorld::.ctor

} // end of class HelloWorld

// =============================================================

// *********** DISASSEMBLY COMPLETE ***********************

程序开头是清单(manifest)信息。其中不仅包括被反编译的模块的全名(HelloWorld.exe),还包括它依赖的所有模块和程序集及其版本信息。

基于这样的一个CIL代码清单,最有趣的可能就是能相对比较容易地理解程序所做的事情,这可比阅读并理解机器码(汇编程序)容易多了。上述代码中出现了对System.Console.WriteLine()的显式引用。CIL代码清单包含许多暂时没有什么用处的外围信息,但如果开发者想要理解C#模块(或者任何基于CLI的程序)的内部工作原理,但又拿不到源代码,那么只要作者没有使用混淆器,理解这样的CIL代码清单还是比较容易的。事实上,一些免费工具(比如Red Gate Reflector、ILSpy、JustDecompile、dotPeek和CodeReflect)可以将CIL自动反编译成C#。

1.4 小结

本章对C#进行了初步介绍。通过本章的学习,你熟悉了基本的C#语法。由于C#与C++风格语言的相似性,本章许多内容可能都是你所熟悉的。然而,C#和托管代码确实有一些独特性,比如会编译成CIL等。C#的另一个关键特征在于它是完全面向对象的。即使是在控制台上读取和写入数据这样的事情,也是面向对象的。面向对象是C#的基础,这一点将贯穿全书。

下一章将探讨C#语言中的基本数据类型,并讨论如何将这些数据类型应用于操作数来构成表达式。

[1].第一次C#设计会议在1998年举行。

[2].如果不知道Inigo Montoya是谁,请找《公主新娘》(The Princess Bride)这部电影来看一看。

[3].设计C#时,语言的创建者从C/C++规范中删除了他们不喜欢的一些特性,同时创建了他们喜欢的一些特性。开发组还有其他语言的资深专家。

[4].使用Mono编译器(开源编译器,网址是www.mono-project.com)进行编译,命令几乎一样,只是编译器名称是mcs.exe,而不是csc.exe。虽然我个人很乐意给出在每种平台下使用的命令,但这样会使读者分心。请参见附录A了解Mono的详情。

[5].例如,在C# 2.0设计之初,语言的设计者们将yield指定成关键字。在微软发布的C# 2.0编译器的alpha版本中(该版本分发给了数千名开发人员),yield以一个新关键字的身份存在。然而,语言的设计者最终选择使用yield return而不是yield,从而避免将yield作为新关键字。除非与return连用,否则yield没有任何特殊意义。

[6].偶尔也有不兼容的情况,比如C# 2.0要求为using语句提供的对象必须实现IDisposable接口,而不能只是实现Dispose()方法。还有一些极少见的泛型表达式,比如F(G<A,B>(7));,在C# 1.0中代表F((G<A),(B>7)),而在C# 2.0中代表调用泛型方法G<A,B>,传递实参7,结果传给F。

[7].有两种单词缩写,一种是“Abbreviation”,比如Professor缩写为Prof.;另一种是“Contraction”,如Doctor缩写为Dr。——译者注

[8].也就是程序名称,比如HelloWorld.exe。——译者注

[9].某些语言的文字编码要用两个16位值表示。第一个代码值称为“高位代理项”(high surrogate),第二个代码值称为“低位代理项”(low surrogate)。在代理项的帮助下,Unicode可以表示100多万个不同的字符。美国和欧洲地区很少使用代理项,东亚各国则很常用。——译者注

[10].里的max不是数学函数,而是变量名。

[11].即literal,是指以文本形式嵌入的数据。literal有多种译法,没有一种占绝对优势。最典型的译法是“字面量”、“文字常量”和“直接量”。本书采用前者。——译者注

[12].CIL的第三种说法是Microsoft IL (MSIL)。本书使用CIL一词,因其是CLI标准所采纳的。C#程序员交流时经常使用IL一词,因为他们都假定IL是指CIL而不是其他中间语言。

[13].参见Miller,J.和S.Ragsdale编著的The Common Language Infrastructure Annotated Standard(Addison-Wesley,2004)。

相关图书

程序员的制胜技
程序员的制胜技
C#开发案例精粹
C#开发案例精粹
C#完全自学教程
C#完全自学教程
C#从入门到精通(第2版)
C#从入门到精通(第2版)
 C#初学者指南
C#初学者指南
C# 5.0入门经典
C# 5.0入门经典

相关文章

相关课程