新書推薦:

《
流浪的君子:孔子的最后二十年 王健文
》
售價:HK$
54.8

《
咨询的奥秘2:咨询师的百宝箱(珍藏版)
》
售價:HK$
76.8

《
中国近代思想与学术的系谱(增订版)
》
售價:HK$
107.8

《
失权者(三联生活周刊文丛)
》
售價:HK$
75.9

《
张元济的生平与事业:从清代改革家到二十世纪出版家
》
售價:HK$
85.8

《
他者中的近代朝鲜(西方韩国研究丛书)
》
售價:HK$
85.8

《
索恩丛书·苏莱曼大帝的崛起:奥斯曼宫廷与16世纪的地中海世界
》
售價:HK$
86.9

《
攀龙附凤:北宋潞州上党李氏外戚将门研究(增订本)宋代将门百年兴衰史
》
售價:HK$
97.9
|
編輯推薦: |
资深C# MVP扛鼎之作
深入理解,探求本源
提升C#编程功力的首选
|
內容簡介: |
《深入理解C#第2版》是C#领域不可多得的经典著作。作者在详尽地展示C#各个知识点的同时,更注重从现象中挖掘本质。本书深入探索了C#的核心概念和经典特性,并将这些特性融入到代码中,让读者能够真正领会到C#之“深入”与“精妙”。在第1版的基础上,书中新增了C# 4的新特性,如动态类型、命名实参和可选参数等,这些特性将C#语言提升到了一个新的层次。
《深入理解C#第2版》适合中高级.NET开发人员阅读。
|
關於作者: |
“阅读一本单纯讲解语法的书会让人痛苦不堪,幸运的是,我们有了Jon的这本书。它以独特的方式讲解C#,值得每一个C#开发人员细读和体味。对于想在实际开发中应用C#,而不仅仅是了解必需语法的中高级开发人员,本书绝对不容错过!”
——Sam Shaw,得克萨斯州大学达拉斯分校.Net用户组副总裁
“第2版新增了动态类型、代码契约和其他一些新特性,是Jon延续第1版辉煌的力作。我对本书推崇至极,.NET社区的读者早就开始争相预订了。如果你对C#感兴趣,还等什么呢?”
——Kirill Osenkov,微软公司
“我向所有关注C#的读者推荐本书,不论是高级开发人员还是初学者都能从中各取所需。只要Jon继续关注C#,我们就能‘站在巨人的肩上’一窥究竟。”
——Ferdinando Santacroce,DZone评论
|
目錄:
|
目 录
第一部分 基础知识
第1章 C#开发的进化史 2
1.1 从简单的数据类型开始 3
1.1.1 C# 1中定义的产品类型 3
1.1.2 C# 2中的强类型集合 4
1.1.3 C# 3中自动实现的属性 5
1.1.4 C# 4中的命名实参 6
1.2 排序和过滤 7
1.2.1 按名称对产品进行排序 7
1.2.2 查询集合 10
1.3 处理未知数据 11
1.3.1 表示未知的价格 12
1.3.2 可选参数和默认值 12
1.4 LINQ简介 13
1.4.1 查询表达式和进程内查询 13
1.4.2 查询XML 14
1.4.3 LINQ to SQL 15
1.5 COM和动态类型 16
1.5.1 简化COM互操作 16
1.5.2 与动态语言互操作 17
1.6 剖析.NET平台 18
1.6.1 C#语言 18
1.6.2 运行时 19
1.6.3 框架库 19
1.7 怎样写出超炫的代码 19
1.7.1 采用代码段形式的全能代码 20
1.7.2 教学代码不是产品代码 21
1.7.3 你的新朋友:语言规范 21
1.8 小结 21
第2章 C# 1所搭建的核心基础 23
2.1 委托 23
2.1.1 简单委托的构成 24
2.1.2 合并和删除委托 28
2.1.3 对事件的简单讨论 29
2.1.4 委托小结 30
2.2 类型系统的特征 31
2.2.1 C#在类型系统世界中的位置 31
2.2.2 C# 1的类型系统在什么时候不够用 34
2.2.3 类型系统特征总结 36
2.3 值类型和引用类型 36
2.3.1 现实世界中的值和引用 36
2.3.2 值类型和引用类型基础知识 37
2.3.3 走出误区 38
2.3.4 装箱和拆箱 40
2.3.5 值类型和引用类型小结 41
2.4 C# 1之外:构建于坚实基础之上的新特性 41
2.4.1 与委托有关的特性 41
2.4.2 与类型系统有关的特性 43
2.4.3 与值类型有关的特性 45
2.5 小结 46
第二部分 C# 2:解决C# 1的问题
第3章 用泛型实现参数化类型 48
3.1 为什么需要泛型 49
3.2 日常使用的简单泛型 50
3.2.1 通过例子来学习:泛型字典 50
3.2.2 泛型类型和类型参数 52
3.2.3 泛型方法和判读泛型声明 55
3.3 深化与提高 58
3.3.1 类型约束 58
3.3.2 泛型方法类型实参的类型推断 62
3.3.3 实现泛型 63
3.4 高级泛型 68
3.4.1 静态字段和静态构造函数 69
3.4.2 JIT编译器如何处理泛型 70
3.4.3 泛型迭代 72
3.4.4 反射和泛型 74
3.5 泛型在C#和其他语言中的限制 78
3.5.1 泛型可变性的缺乏 78
3.5.2 缺乏操作符约束或者“数值”约束 82
3.5.3 缺乏泛型属性、索引器和其他成员类型 83
3.5.4 同C++模板的对比 84
3.5.5 和Java泛型的对比 85
3.6 小结 86
第4章 可空类型 88
4.1 没有值时怎么办 88
4.1.1 为什么值类型的变量不能是null 89
4.1.2 在C# 1中表示空值的模式 89
4.2 System.Nullable和System.Nullable 91
4.2.1 Nullable简介 91
4.2.2 Nullable装箱和拆箱 94
4.2.3 Nullable实例的相等性 95
4.2.4 来自非泛型Nullable类的支持 96
4.3 C# 2为可空类型提供的语法糖 96
4.3.1 ?修饰符 97
4.3.2 使用null 进行赋值和比较 98
4.3.3 可空转换和操作符 99
4.3.4 可空逻辑 102
4.3.5 对可空类型使用as操作符 103
4.3.6 空合并操作符 104
4.4 可空类型的新奇用法 106
4.4.1 尝试一个不使用输出参数的操作 107
4.4.2 空合并操作符让比较不再痛苦 108
4.5 小结 111
第5章 进入快速通道的委托 112
5.1 向笨拙的委托语法说拜拜 113
5.2 方法组转换 114
5.3 协变性和逆变性 115
5.3.1 委托参数的逆变性 116
5.3.2 委托返回类型的协变性 117
5.3.3 不兼容的风险 118
5.4 使用匿名方法的内联委托操作 119
5.4.1 从简单的开始:处理一个参数 119
5.4.2 匿名方法的返回值 121
5.4.3 忽略委托参数 123
5.5 在匿名方法中捕捉变量 124
5.5.1 定义闭包和不同的变量类型 124
5.5.2 测试被捕获的变量的行为 126
5.5.3 捕获变量到底有什么用处 127
5.5.4 捕获变量的延长生存期 127
5.5.5 局部变量实例化 128
5.5.6 共享和非共享的变量混合使用 130
5.5.7 捕获变量的使用规则和小结 132
5.6 小结 133
第6章 实现迭代器的捷径 134
6.1 C# 1:手写迭代器的痛苦 135
6.2 C# 2:利用yield语句简化迭代器 137
6.2.1 迭代器块和yield return简介 137
6.2.2 观察迭代器的工作流程 139
6.2.3 进一步了解迭代器执行流程 141
6.2.4 具体实现中的奇特之处 144
6.3 真实的例子:迭代范围值 145
6.3.1 迭代时刻表中的日期 145
6.3.2 迭代文件中的行 146
6.3.3 使用迭代器块和谓词对项进行延迟筛选 148
6.4 使用CCR实现伪同步代码 150
6.5 小结 152
第7章 结束C# 2的讲解:最后的一些特性 153
7.1 分部类型 154
7.1.1 在多个文件中创建一个类型 154
7.1.2 分部类型的使用 156
7.1.3 C# 3独有的分部方法 157
7.2 静态类型 159
7.3 独立的取值方法赋值方法属性访问器 161
7.4 命名空间别名 162
7.4.1 限定的命名空间别名 163
7.4.2 全局命名空间别名 164
7.4.3 外部别名 164
7.5 Pragma指令 166
7.5.1 警告pragma 166
7.5.2 校验和pragma 167
7.6 非安全代码中的固定大小的缓冲区 167
7.7 把内部成员暴露给选定的程序集 169
7.7.1 在简单情况下的友元程序集 169
7.7.2 为什么使用InternalsVisibleTo 170
7.7.3 InternalsVisibleTo和签名程序集 170
7.8 小结 171
第三部分 C# 3:革新写代码的方式
第8章 用智能的编译器来防错 174
8.1 自动实现的属性 175
8.2 隐式类型的局部变量 176
8.2.1 用var声明局部变量 177
8.2.2 隐式类型的限制 178
8.2.3 隐式类型的优缺点 179
8.2.4 建议 180
8.3 简化的初始化 180
8.3.1 定义示例类型 181
8.3.2 设置简单属性 182
8.3.3 为嵌入对象设置属性 183
8.3.4 集合初始化列表 184
8.3.5 初始化特性的应用 186
8.4 隐式类型的数组 187
8.5 匿名类型 188
8.5.1 第一次邂逅匿名类型 188
8.5.2 匿名类型的成员 190
8.5.3 投影初始化列表 191
8.5.4 重点何在 192
8.6 小结 193
第9章 Lambda表达式和表达式树 194
9.1 作为委托的Lambda表达式 195
9.1.1 准备工作:Func委托类型简介 195
9.1.2 第一次转换成Lambda表达式 196
9.1.3 用单一表达式作为主体 197
9.1.4 隐式类型的参数列表 197
9.1.5 单一参数的快捷语法 198
9.2 使用List和事件的简单例子 199
9.2.1 对列表进行筛选、排序并设置其他操作 199
9.2.2 在事件处理程序中进行记录 201
9.3 表达式树 202
9.3.1 在程序中构建表达式树 202
9.3.2 将表达式树编译成委托 204
9.3.3 将C# Lambda表达式转换成表达式树 205
9.3.4 位于LINQ核心的表达式树 208
9.3.5 LINQ之外的表达式树 209
9.4 类型推断和重载决策发生的改变 211
9.4.1 改变的起因:精简泛型方法调用 211
9.4.2 推断匿名函数的返回类型 212
9.4.3 分两个阶段进行的类型推断 213
9.4.4 选择正确的被重载的方法 217
9.4.5 类型推断和重载决策 218
9.5 小结 219
第10章 扩展方法 220
10.1 未引入扩展方法之前的状态 221
10.2 扩展方法的语法 223
10.2.1 声明扩展方法 223
10.2.2 调用扩展方法 224
10.2.3 扩展方法是怎样被发现的 225
10.2.4 在空引用上调用方法 226
10.3 .NET 3.5中的扩展方法 227
10.3.1 从Enumerable开始起步 228
10.3.2 用Where筛选并将方法调用链接到一起 229
10.3.3 插曲:似曾相识的Where方法 231
10.3.4 用Select方法和匿名类型进行投影 232
10.3.5 用OrderBy方法进行排序 232
10.3.6 涉及链接的实际例子 234
10.4 使用思路和原则 235
10.4.1 “扩展世界”和使接口更丰富 235
10.4.2 流畅接口 236
10.4.3 理智使用扩展方法 237
10.5 小结 238
第11章 查询表达式和LINQ to Objects 240
11.1 LINQ介绍 241
11.1.1 LINQ中的基础概念 241
11.1.2 定义示例数据模型 245
11.2 简单的开始:选择元素 246
11.2.1 以数据源作为开始,以选择作为结束 246
11.2.2 作为查询表达式基础的编译器转换 247
11.2.3 范围变量和重要的投影 249
11.2.4 Cast、OfType和显式类型的范围变量 251
11.3 对序列进行过滤和排序 252
11.3.1 使用where子句进行过滤 253
11.3.2 退化的查询表达式 253
11.3.3 使用orderby子句进行排序 254
11.4 let子句和透明标识符 256
11.4.1 用let来进行中间计算 256
11.4.2 透明标识符 257
11.5 联接 258
11.5.1 使用join子句的内联接 258
11.5.2 使用join into子句进行分组联接 262
11.5.3 使用多个from子句进行交叉联接和合并序列 264
11.6 分组和延续 267
11.6.1 使用group by子句进行分组 267
11.6.2 查询延续 270
11.7 在查询表达式和点标记之间作出选择 272
11.7.1 需要使用点标记的操作 273
11.7.2 选择点标记 273
11.7.3 选择查询表达式 274
11.8 小结 275
第12章 超越集合的LINQ 276
12.1 使用LINQ to SQL查询数据库 277
12.1.1 数据库和模型 277
12.1.2 用查询表达式访问数据库 279
12.1.3 包含联接的查询 281
12.2 用IQueryable和IQueryProvider进行转换 283
12.2.1 IQueryable和相关接口的介绍 283
12.2.2 模拟接口实现来记录调用 285
12.2.3 把表达式粘合在一起:Queryable的扩展方法 287
12.2.4 模拟实际运行的查询提供器 289
12.2.5 包装IQueryable 290
12.3 LINQ友好的API和LINQ to XML 290
12.3.1 LINQ to XML中的核心类型 290
12.3.2 声明式构造 292
12.3.3 查询单个节点 294
12.3.4 合并查询操作符 296
12.3.5 与LINQ和谐共处 297
12.4 用并行LINQ代替LINQ to Objects 297
12.4.1 在单线程中绘制曼德博罗特集 297
12.4.2 ParallelEnumerable、Parallel-Query和AsParallel 299
12.4.3 调整并行查询 300
12.5 使用LINQ to Rx反转查询模型 301
12.5.1 IObservable和IObserver 302
12.5.2 简单地开始 303
12.5.3 查询可观察对象 304
12.5.4 意义何在 306
12.6 扩展LINQ to Objects 306
12.6.1 设计和实现指南 307
12.6.2 示例扩展:选择随机元素 308
12.7 小结 309
第四部分 C# 4:良好的交互性
第13章 简化代码的微小修改 312
13.1 可选参数和命名实参 312
13.1.1 可选参数 313
13.1.2 命名实参 317
13.1.3 两者相结合 321
13.2 改善COM互操作性 324
13.2.1 在C# 4之前操纵Word是十分恐怖的 325
13.2.2 可选参数和命名实参的复仇 325
13.2.3 按值传递ref参数 326
13.2.4 调用命名索引器 327
13.2.5 链接主互操作程序集 328
13.3 接口和委托的泛型可变性 330
13.3.1 可变性的种类:协变性和逆变性 331
13.3.2 在接口中使用可变性 332
13.3.3 在委托中使用可变性 334
13.3.4 复杂情况 335
13.3.5 限制和说明 336
13.4 对锁和字段风格的事件的微小改变 339
13.4.1 健壮的锁 339
13.4.2 字段风格的事件 340
13.5 小结 341
第14章 静态语言中的动态绑定 342
14.1 何谓,何时,为何,如何 343
14.1.1 何谓动态类型 343
14.1.2 动态类型什么时候有用,为什么 344
14.1.3 C# 4如何提供动态类型 345
14.2 关于动态的快速指南 345
14.3 动态类型示例 348
14.3.1 COM和Office 348
14.3.2 动态语言 350
14.3.3 纯托管代码中的动态类型 353
14.4 幕后原理 358
14.4.1 DLR简介 358
14.4.2 DLR核心概念 360
14.4.3 C#编译器如何处理动态 362
14.4.4 更加智能的C#编译器 365
14.4.5 动态代码的约束 368
14.5 实现动态行为 370
14.5.1 使用ExpandoObject 370
14.5.2 使用DynamicObject 374
14.5.3 实现IDynamicMetaObject-Provider 380
14.6 小结 383
第15章 使用契约让代码更加清晰 385
15.1 未引入代码契约之前的状态 386
15.2 代码契约 387
15.2.1 前置条件 388
15.2.2 后置条件 389
15.2.3 固定条件 390
15.2.4 断言和假设 392
15.2.5 旧式契约 393
15.3 使用ccrewrite和ccrefgen重写二进制 394
15.3.1 简单重写 394
15.3.2 契约继承 395
15.3.3 契约引用程序集 398
15.3.4 失败行为 399
15.4 静态检查 401
15.4.1 开始静态检查 401
15.4.2 隐式职责 403
15.4.3 有选择性的检查 406
15.5 使用ccdocgen 将契约文档化 408
15.6 契约实战 410
15.6.1 契约中有什么 410
15.6.2 如何开始 411
15.6.3 无处不在的选项 412
15.7 小结 414
第16章 何去何从 415
16.1 C#——传统与现代的结合 415
16.2 计算机科学和.NET 416
16.3 计算机世界 417
16.4 再会 417
附录A LINQ标准查询操作符 419
附录B .NET中的泛型集合 430
附录C 版本总结 440
|
內容試閱:
|
序
世上有两类钢琴家。
一类钢琴家弹琴并不是因为他们喜欢,而是因为父母强迫他们上钢琴课。另一类钢琴家弹琴是因为他们喜欢音乐,想创作音乐。他们不需要被强迫,相反,他们陶醉其中,时常忘记什么时候要停下来。
后一类人中,有人是把弹钢琴当作一种爱好。而有人则是为了生活,因此更需要投入、技巧和天赋。他们有一定的灵活性来选择弹奏哪些音乐流派和风格,不过这些选择主要还是由雇主的需要或者听众的口味来决定的。
后一类人中,有人主要就是为了钱,但也有一些专业人士即便没有报酬,也愿意在公共场合弹奏钢琴。他们喜欢运用自己的技巧和天赋为别人演奏音乐。在这个过程中,他们能找到许多乐趣。如果同时还有报酬,当然更是锦上添花。
后一类人中,有人是自学成材的,他们演奏乐曲是不看谱的。这些人有极高的天赋和能力,但除非通过音乐本身,否则无法向别人传递那种直观的感受。还有一些人无论在理论还是实践上都经过了正统的训练,他们能清楚地理解作曲家是用什么手法得到预期的情绪效果,并相应地改进自己的演绎手法。
后一类人中,有人从来没有打开钢琴看它的内部构造。还有一些人则对钢琴的发声原理好奇不已,最后发现是由于杠杆装置和绞盘在音锤敲击琴弦前的瞬间,牵引制音器的擒纵器,他们为弄明白由5000~10000个运动机件组成的这个乐器装置而感到高兴和自豪。
后一类人中,有人会对自己的手艺和成就心满意足,对它们带来的心灵上的愉悦和经济上的收入感到非常满意。但是,还有一些人不仅仅是艺术家、理论家和技师,他们会抽时间以导师的身份,将那些知识传授给其他人。
我不知道JonSkeet是哪一类钢琴家。但是,我与这位微软C#MVP有多年的电子邮件交流,并经常看他的博客。我本人至少3遍逐字读完他的这本书,我清楚地知道Jon是后一种软件开发者:热情、博学、天资极高、有好奇心以及善于分析——是其他人的好老师。
C#是一种极为实用和快速发展的语言。通过添加查询能力、更丰富的类型推断、精简的匿名函数语法,等等,一种全新风格的编程语言已出现在我们的面前。与此同时,它代表的仍然是一种静态类型的、面向组件的开发方式,C#取得成功的立足之本没有变。
许多新元素会让人有矛盾的感觉。一方面,它们会显得比较“旧”(Lambda表达式可以追溯到20世纪上半叶计算机科学奠基的年代)。与此同时,对于那些习惯了现代面向对象编程的开发者,它们又可能显得太新和太不熟悉。
Jon掌控了一切。对于需要理解C#最新版本“是什么”和“怎么做”的专业开发者,本书是理想的选择。此外,如果开发者还探索语言“为什么”要这样设计,从而加深他们对语言的理解,那么本书更是独一无二的。
为了利用语言提供的所有新能力,需要以全新的方式思考数据、函数以及它们之间的关系。这有点儿像经过多年的古典乐训练之后,开始尝试演奏爵士乐——或者相反。不管怎样,我期待下一代C#程序员能够“谱写”出优秀的乐章。祝你“谱曲”愉快,并感谢你选用了C#这个
“主调”。
EricLippert
微软资深软件工程师
C#开发的进化史
本章概要
一个进化的例子
.NET的组成
使用本书代码
C#语言规范
你知道我喜欢Python、Ruby、Groovy这些动态语言的哪些方面吗?它们去代码之糟粕,仅取其精华——即真正进行处理的那部分内容。繁琐的形式被生成器、Lambda表达式、列表推导式等特性所代替。
有趣的是,那些旨在让动态语言感觉轻巧的特性,却很少与动态有关。当然,像鸭子类型和活动记录(ActiveRecord)中的一些神奇用法还是与动态有关的,但静态类型的语言却完全可以不那么笨拙和繁重。
回到C#。在某些方面,C#1可以被看成是2001年Java语言的升级版。它们的相似之处十分明显,但C#还包含一些额外的特性:作为一级语言特性的属性、委托和事件、foreach循环、using语句、显式方法重载、操作符重载、自定义值类型等,恕不一一列举。语言的偏好显然是个人问题,但在我第一次使用C#1时,就明显感到它比Java要更进一步。
从那时起,C#的开发可谓渐入佳境。C#的每个新版本都添加了重要的特性,不断为开发者排忧解难,而这些特性往往都是经过深思熟虑的,很少有向后不兼容的情况。即使在C#4增添真正实用的动态类型功能之前,C#也已经引入了很多与动态(及函数式)语言相关的特性,使代码更加容易编写和维护。
本书将逐一介绍这些变化,让你真正喜欢上C#编译器准备为你上演的奇迹。不过这些都是后话,本章我将马不停蹄地介绍尽可能多的特性。在比较C#语言和.NET平台时,我会给出我的定义,随后还要重点说明一下本书剩余部分应掌握的关键内容。接着我们就可以深入细节了。
我们不会在这单独的一章中介绍所有变化,但会涵盖以下方面的内容:泛型、设置不同访问修饰符的属性、可空类型、匿名方法、自动实现的属性、增强的集合初始化程序、增强的对象初始化程序、Lambda表达式、扩展方法、隐式类型、LINQ查询表达式、命名实参、可选参数、简化的COM互操作和动态类型。这些将伴随着我们从C#1一路讲到最新发布的C#4。内容显然很多,让我们现在就开始吧。
1.1 从简单的数据类型开始
在本章,我将让C#编译器实现一些神奇的功能,但不会告诉你怎么做,也很少会提及这是什么以及为什么会这样。这是唯一一次我不会解释原理,或者说不会按部就班地展示例子的情形。事实上,我的计划是先给你留下一个不可磨灭的印象,而不是教给你具体的知识。在你读完本节的内容之后,如果对C#能做的事情仍然没有感到丝毫兴奋,那么本书或许真的不适合你。相反,如果你迫切地想知道我的“魔术”是怎么玩的——想让我放慢“手法”,直至看清楚所有发生的事情——那么这正是本书剩余部分要做的事情。
事先要提醒你的是,这个例子可能显得十分牵强——是为了在尽可能短的代码中包含尽可能多的新特性而“生搬硬造”的。此外,这个例子还有点“老生常谈”——但至少你会觉得它比较眼熟。这里展示的是一个产品名称价格(productnameprice)的例子,是“hello,world”程序的电子商务版。我们将看到各种任务是如何实现的,体验随着C#版本的提高,如何更简单、更优雅地完成相同的任务。
1.1.1 C#1中定义的产品类型
我们将以定义一个表示产品的类型作为开始,然后进行处理。在Product类型内部,没有特别吸引人的东西,它只是封装了几个属性。为方便演示,我们还要在这个地方创建预定义产品的一个列表。代码清单1-1展示了用C#1写的Product类型。稍后,还会演示在更高版本中如何达到相同的效果。我们将按照这个模式演示其他所有代码。由于这些代码是在2010年编写的,因此你可能已经熟悉了将要介绍的某些特性——不过回头看看还是值得的,我们可以看看这门语言到底有了多少进步。
代码清单1-1 Product类型(C#1)
代码清单1-1没有什么难以理解的东西——它毕竟只是C#1代码。然而,它确实证明了C#1代码存在如下的3个局限。
ArrayList没有提供与其内部内容有关的编译时信息。不慎在GetSampleProducts创建的列表中添加一个字符串是完全有可能的,而编译器对此没有任何反应。
代码中为属性提供了公共的取值方法,这意味着如果添加对应的赋值方法,那么赋值方法也必须是公共的。
用于创建属性和变量的代码很复杂——封装一个字符串和一个十进制数应该是一个十分简单的任务,不该这么复杂。
来看看C#2作了哪些改进。
1.1.2?C#2中的强类型集合
我们所做的第一组改动(如代码清单1-2所示)针对上面列出的前两项,包含C#2中最重要的改变:泛型。新的内容用粗体列出:
代码清单1-2 强类型集合和私有的赋值方法(C#2)
代码的改动并不大,但有效解决了两个问题。首先,属性拥有了私有的赋值方法(我们在构造函数中使用了这两个赋值方法)。其次,它能非常“聪明”地猜出List是告知编译器列表中只能包含Product。试图将一个不同的类型添加到列表中,会造成编译时错误。C#2解决了原先的3个问题中的2个。代码清单1-3展示了C#3如何解决剩余的1个问题。
1.1.3?C#3中自动实现的属性
我们先从C#3中相对乏味的一些特性开始。下面的代码清单1-3所展示的自动实现的属性和简化的初始化相比Lambda表达式之类的特性来说,有点微不足道,不过他们可以大大地简化代码。
代码清单1-3 自动实现的属性和更简单的初始化(C#3)
现在,不再有任何代码(或者可见的变量)与属性关联,而且硬编码的列表是以一种全然不同的方式来构建的。由于没有name和price变量可供访问,我们必须在类中处处使用属性,这增强了一致性。现在有一个私有的无参数构造函数,用于新的基于属性的初始化。在本例中,实际上可以删除旧的公共的构造函数。但这样一来,外部代码就不能再创建其他的产品实例了。
1.1.4?C#4中的命名实参
对于C#4,在涉及属性和构造函数时,我们需要回到原始代码。其中有一个原因是为了让它不易变:尽管拥有私有赋值方法的类型不能被公共地改变,但如果它也不能被私有地改变,将会显得更加清晰。不幸的是,对于只读属性,没有快捷方式。但C#4允许我们在调用构造函数时指定实参的名称,如代码清单1-4所示,它为我们提供了和C#3的初始化程序一样的清晰度,而且还移除了易变性(mutability)。
代码清单1-4?命名实参带来了清晰的初始化代码(C#4)
在这个特定的示例中,该特性的好处不是很明显,但当方法或构造函数包含多个参数时,它可以使代码的含义更加清楚——特别是当参数类型相同,或为某个参数为null的时候。当然,你可以选择什么时候使用该特性,只在使代码更好理解的时候才指定参数的名称。
图1-1总结了Product类型的演变历程。完成了每个任务之后,我都会提供一幅类似的示意图,便于你体会C#在增强代码的时候,遵循的是一个什么样的模式。
到目前为止,你看到的变化幅度都不大。事实上,泛型的加入(List语法)或许是C#2最重要的一个部分。但是,我们现在只看到了它的部分用处。虽然没有什么让人心跳加快的东西,但我们只是刚刚开始。下一个任务是以字母顺序打印产品列表。
图1-1Product类型的演变历程,展示了更好的封装性、更强的类型化以及更容易的初始化
1.2排序和过滤
在本节中,我们不会改变Product类型,我们会使用示例的产品列表,并按名称排序,然后找出最贵的产品。每个任务都不难,但我们可以看到它到底能简化到什么程度。
1.2.1 按名称对产品进行排序
以特定顺序显示一个列表的最简单的方式就是先将列表排好序,再遍历并显示其中的项。在.NET1.1中,这要求使用ArrayList.Sort,而且在我们的例子中,要求提供一个IComparer实现。也可以让Product类型实现IComparable,但那就只能定义一种排序顺序。很容易就会想到,以后除了需要按名称排序,还可能需要按价格排序。代码清单1-5实现了IComparer,然后对列表进行排序,并显示它。
代码清单1-5 使用IComparer对ArrayList进行排序(C#1)
在代码清单1-5中,要注意的第一件事情是,必须引入一个额外的类型来帮助排序。虽然这并不是一个大问题,但假如在一个地方只是想按名称进行排序,就会感觉编码工作过于繁重。其次,注意Compare方法中的强制类型转换。强制类型转换相当于告诉编译器:“嘿嘿,我知道的比你多一点点。”但是,这也意味着你可能是错误的。如果从GetSampleProducts返回的ArrayList包含一个字符串,那么代码会出错——因为在比较时试图将字符串强制转型为Product。
在给出排序列表的代码中也进行了强制类型转换。这个转换不如刚才的转换明显,因为是编译器自动进行的。foreach循环会隐式将列表中的每个元素转换为Product类型。同样,这种情况在执行时会引发失败,在C#2中,“泛型”可以帮助我们解决这些问题。在代码清单1-6中,唯一的改变就是引入了泛型。
代码清单1-6 使用IComparer对List进行排序(C#2)
在代码清单1-6中,对产品名进行比较的代码变得更简单,因为一开始提供的就是Product(而不可能是其他类型)。不需要进行强制类型转换。类似地,foreach循环中隐式的类型转换也被取消了。编译器仍然会考虑将序列中的源类型转换为变量的目标类型,但它知道这时两种类型均为Product,因此没必要产生任何用于转换的代码。
确实有了一定的改进。但是,我们希望能直接指定要进行的比较,就能开始对产品进行排序,而不需要实现一个接口来做这件事。代码清单1-7展示了具体如何做,它告诉Sort方法如何用一个委托来比较两个产品。
代码清单1-7 使用Comparison对List进行排序(C#2)
注意,现在已经不再需要ProductNameComparer类型了。以粗体印刷的语句实际会创建一个委托实例。我们将这个委托提供给Sort方法来执行比较。第5章会更多地讲解这个特性(匿名方法)。现在,我们已经修正了在C#1的版本中不喜欢的所有东西。但是,这并不是说C#3不能做得更好。首先,让我们将匿名方法替换成一种更简洁的创建委托实例的方式,如代码清单1-8所示。
代码清单1-8 在Lambda表达式中使用Comparison来进行排序(C#3)
你又看到了一种奇怪的语法(一个Lambda表达式),它仍然会像代码清单1-7那样创建一个Comparison委托,只是代码量减少了。这里不必使用delegate关键字来引入委托,甚至不需要指定参数类型。除此之外,使用C#3还有其他好处。现在,可以轻松地按顺序打印名称,同时不必修改原始产品列表。代码清单1-9使用OrderBy方法对此进行了演示。
代码清单1-9 使用一个扩展方法对List进行排序(C#3)
这里似乎调用了一个OrderBy方法,但查阅一下MSDN,就会发现这个方法在List中根本不存在。之所以能调用它,是由于存在一个扩展方法,我们将在第10章讨论扩展方法的细节。这里实际不再是“原地”对列表进行排序,而只是按特定的顺序获取列表的内容。有的时候,你需要更改实际的列表;但有的时候,没有任何副作用的排序显得更“善解人意”。重点在于,现在的写法更简洁,可读性更好(当然是在你理解了语法之后)。我们的想法是“列表按名称排序”,现在的代码正是这样做的。并不是“列表通过将一个产品的名称与另一个产品的名称进行比较来排序”,就像C#2代码所做的那样。也不是使用知道如何将一个产品与另一个产品进行比较的另一个类型的实例来按名称排序。这种简化的表达方式是
C#3的核心优势之一。既然单独的数据查询和操作是如此简单,那么在执行更大规模的数据处理时,仍然可以保持代码的简洁性和可读性,这进而鼓励开发者以一种“以数据为中心”的方式来观察世界。
本节又展示了一小部分C#2和C#3的强大功能,还有许多尚待解释的语法。但是,即使你还不理解这背后的细节,趋势也是相当明朗的:我们正在向更清晰、更简单的代码进步!图1-2展示了这个演变的过程。
图1-2 在C#2和C#3中用于简化排序的特性
到目前为止,我们只讲了排序。现在来讨论一种不同的数据处理方式——查询。
1.2.2 查询集合
下一个任务是找出列表中符合特定条件的所有元素。具体地说,要找出价格高于10美元的产品。在C#1中,需要运行循环,测试每个元素,并打印出符合条件的元素(参见代码清单1-10)。
代码清单1-10 循环、测试和打印(C#1)
好吧,上面的代码写起来不难,也很容易理解。然而,请注意3个任务是如何纠缠在一起的:用foreach进行循环,用if测试条件,再用Console.WriteLine显示产品。这3个任务的依赖性是一目了然的,看看它们是如何嵌套的就明白了。C#2稍微进行了一下改进(参见代码清单1-11)。
代码清单1-11 测试和打印分开进行(C#2)
变量test的初始化使用了上节中介绍的匿名方法,而print变量的初始化使用了C#2的另一个特性——方法组转换,它简化了从现有方法创建委托的过程。
我不是说上述代码要比C#1的代码简单,只是说它要强大得多。具体地说,它使我们可以非常轻松地更改测试条件并对每个匹配项采取单独的操作。涉及的委托变量(test和print)可以传递给一个方法——相同的方法可以用于测试完全不同的条件以及执行完全不同的操作。当然,可以将所有测试和打印都放到一条语句中,如代码清单1-12所示。
代码清单1-12 测试和打印分开进行的另一个版本(C#2)
这样更好一些,但delegateProductp还是很碍事,大括号也是。它们是代码中的不和谐音符,有损可读性。如果一直进行相同的测试和执行相同的操作,我还是喜欢C#1的版本。(虽然说起来很平常,但还是要提醒你记住,完全可以在使用C#2或C#3的时候使用C#1的版本。谁都不会用推土机来种植郁金香,我们这里使用的技术显得有点儿“小题大做”了。)C#3拿掉了以前将实际的委托逻辑包裹起来的许多无意义的东西,从而有了极大的改进(参见代码清单1-13)。
代码清单1-13 用Lambda表达式来测试(C#3)
Lambda表达式将测试放在一个非常恰当的位置。再加上一个有意义的方法名,你甚至能大声念出代码,几乎不用怎么思考就能理解代码的含义。C#2的灵活性也得到了保留——传递给Where的参数值可以来源于一个变量。此外,如果愿意,完全可以使用Action,而不是硬编码的Console.WriteLine调用。
本节的这个任务强调了我们通过前面的排序任务已经明确的一点——使用匿名方法可以轻松编写一个委托,Lambda表达式则更进一步,将这个任务变得更简单。换言之,可以在foreach循环的第一个部分中包含查询或排序操作,同时不会影响代码的可读性。图1-3对这些编程方式上的变化进行了总结。对于这个任务来说,C#4没有提供任何可以进一步简化的特性。
图1-3 在C#2中,匿名方法有助于问题的可分离性;在C#3中,Lambda表达式则增强了可读性
现在,我们已经给出了筛选过的列表,接下来假设我们的数据跟以前不一样了。如果并非总知道一个产品的价格,那么会发生什么?如何在Product类中应对这个问题?
1.3?处理未知数据
我们将要介绍两种不同形式的未知数据。首先,我们要处理确实没有数据信息的场景。其次,再来看看如何从方法调用中移除信息,使用默认值来代替。
1.3.1 表示未知的价格
这一次不打算展示太多的代码,但问题肯定是你熟悉的,尤其是假如你经常使用数据库的话。假定产品列表不仅包含现售的产品,还包括尚未面市的产品。某些情况下,我们可能不知道价格。如果decimal是引用类型,那么只需使用null来表示未知的价格。但是,由于它是值类型,我们不能这样表示。那么,在C#1中如何表示?有3种常见的解决方案:
围绕decimal创建一个引用类型包装器;
维护一个单独的Boolean标志,它表示价格是否已知;
使用一个“魔数”(magicvalue)(比如decimal.MinValue)来表示未知价格。
你得承认,其中没有一个方案是特别好的。神奇的是,在变量和属性声明中添加一个额外的字符,即可解决这个问题。.NET2.0通过引入Nullable结构,C#2通过提供一些语法糖(syntacticsugar),使事情得到了极大的简化。现在可以将属性声明更改为如下代码块:
构造函数的参数也更改为decimal?。这样一来,就可以将null作为参数值传递进来,或者在类中写Price=null;,这比其他任何解决方案都更有表现力。代码的其余部分和往常一样工作——价格未知的产品默认价格低于10美元因为可空值是通过“大于”操作符来处理比较的。为了检查一个价格是否已知,可以把它同null比较,或者使用HasValue属性。所以,为了在C#3中显示所有价格未知的产品,可以像代码清单1-14这样写:
代码清单1-14 显示价格未知的产品(C#3)
C#2代码与代码清单1-12的代码相似,但使用了returnp.Price==null;作为匿名方法的方法体。C#3在可空类型方面没有进行什么改进,而C#4则提供了一个与之相关的特性。
1.3.2?可选参数和默认值
有时你并不想给出方法所需的所有东西,比如对于某个特定参数,你可能总是会使用同样的值。传统的解决方案是对该方法进行重载,现在C#引入的可选参数(optionalparameters)可以简化这一操作。在Product类型的C#4版本中,构造函数接收产品的名称和价格。在C#2和C#3中,我们可以将价格设置为可空的decimal类型,但现在我们假设大多数产品都不包含价格。如果能像下面这样初始化产品就再好不过了:
在C#4之前,我们只能添加一个Product构造函数的重载来实现这一目的。而使用C#4可以为价格参数声明一个默认值(在本例中为null):
你需要为声明的可选参数指定一个常量值。这个值不一定为null,只不过在本例中默认值恰好为空而已。它可以应用于任何类型的参数,但是对于除字符串之外的引用类型来说,你只能使用null来作为可用的常量值。图1-4总结了C#不同版本的演变。
到现在为止所有的特性都很有用,但也许都不值得大书特书。下面我们来看一个让人更加兴奋的特性:LINQ。
图1-4处理“未知”数据的方法
1.4?LINQ简介
LINQ(LanguageIntegratedQuery,语言集成查询),是C#3的核心之所在。顾名思义,LINQ是关于查询的,其目的是使用一致的语法和特性,以一种易阅读、可组合的方式,使对多数据源的查询变得简单。
在很大程度上,C#2更像是对C#1的各种不足之处的修修补补,所以并没有一鸣惊人。而C#3中几乎所有特性都是为了构建LINQ,并且其结果也十分特别。我见过其他语言中的一些特性,可以解决与LINQ相同领域的问题,但没有一个可以如此全面和灵活。
1.4.1查询表达式和进程内查询
如果你以前见过LINQ,肯定会知道查询表达式可以以一种声明式风格对不同数据源创建查询。之所以前面的示例都没有使用查询表达式,是因为那些例子不使用查询表达式反而更简单。当然,这并不是说不能使用。例如,代码清单1-15与代码清单1-13是等价的。
代码清单1-15?使用查询表达式的前几步:筛选集合
我个人认为早先的代码清单(代码清单1-13)更易读——查询表达式唯一的好处就是where子句显得更简单。这里我还偷偷使用了另一个特性——隐式类型局部变量(implicitlytypedlocalvariables),它使用var上下文关键字声明。编译器可以根据该变量的初始值推断其类型。因此,filtered的类型为IEnumerable。在本章后面的示例中我将广泛使用var。它对于写书来说也是十分重要的,可以节省代码清单的空间。
那么,如果查询表达式不好,为什么每个人都对它(和LINQ)如此看重呢?第一个答案是,虽然查询表达式不是特别适合简单任务,但在一些较复杂的情况下,如果换成用方法调用来写(尤其是用C#1或2的方式),代码会变得难以阅读。在这些情况下,查询表达式就显得非常好用。为了稍微增大一点难度,让我们引入另一个类型——Supplier(供货商)。
每个供货商都有一个Name(string)和一个SupplierID(int)。我已经在Product类中将SupplierID作为一个属性添加,并对示例数据进行了适当的改编。无可否认,为每个产品都提供一个供货商,这不是一种纯面向对象的方式——但它更接近于数据在数据库中的表示方式。目前,它使这个特定的特性(查询表达式)更容易演示。但到第12章的时候,会看到LINQ也允许我们使用一个更自然的模型。
现在来看一下代码(代码清单1-16)。它将示例产品与示例供货商联接起来(明显要基于供货商的ID来联接),将和以前一样的价格筛选器(价格高于10美元)应用于产品,先按供货商名排序,再按产品名排序,最后打印每个匹配项的供货商名称和产品名称。这要放在以前版本的C#中,不知道需要输入多少代码进去,而且实现起来简直就是一场噩梦。相反,在LINQ中,要做到这些事情实在是太容易了。
代码清单1-16 联接(joining)、筛选(filtering)、排序(ordering)和投影(projecting)
(C#3)
明眼人一看便知,这跟SQL实在太像了。事实上,许多人初识LINQ时(但在仔细研究它之前),第一个反应就是抗拒,因为它似乎纯粹就是将SQL引入了语言,目的是为了加强与数据库交互的能力。幸好,LINQ只是借用了SQL的语法和一些思路。正如我们见到的那样,不需要数据库就能使用它——到现在为止,我所运行过的代码中,没有任何代码与数据库有关。事实上,可以从任意来源(如XML)获取数据。
1.4.2?查询XML
假定不是将供货商和产品硬编码进来,而是使用以下XML文件:
虽然这个文件非常简单,但从中提取数据的最佳方式是什么?怎样查询它?怎样基于它来进行联接操作?肯定会比代码清单1-16难吧?代码清单1-17展示了在LINQtoXML中要做多少工作。
代码清单1-17 用LINQtoXML对XML文件进行“复杂”的处理(C#3)
我得承认,现在的代码不像前面那么直观了,因为需要告诉系统如何理解数据(什么属性应该作为什么类型使用)。但是,两者的差别并不太大。尤其是在两个代码清单的每个部分之间,存在着明显的联系。假如不是因为行长的限制需要断行,在两个查询之间,应该是逐行对应的。
印象深刻吗?还没有完全信服吗?让我们将数据放到一个更有可能的地方——一个数据库中。
1.4.3?LINQtoSQL
此时要做一些工作(大多数都是自动进行的)让LINQtoSQL知道什么数据表里该有什么内容,但整个过程还是相当直观的。代码清单1-18所示,我们将直接跳到查询代码。如果你想看LingDemoDataContext的细节,在可下载的代码源中能找到。
代码清单1-18 对SQL数据库应用查询表达式(C#3)
这些代码看起来就应该非常熟悉了。join那一行下面的所有内容都是直接从代码清单1-16中复制并粘贴过来的,没有进行任何改动。这显然使人印象深刻,但思路清晰的人马上会想到一个问题:为什么要将所有数据都从数据库里“拽”回来,再应用这些.NET查询和排序呢?为什么不直接让数据库来做这些事情呢?那不正是它擅长的事情吗?事实上,这正是LINQtoSQL所做的事情。代码清单1-18中的代码发出了一个数据库请求,它基本上被转换为SQL查询。虽然查询是用C#代码来表示的,但却是作为SQL来执行的。
以后会知道,当模式(schema)和实体(entity)知道了供货商和产品之间的关系后,可以使用一种更面向关系(relation-oriented)的方式来进行联接。但结果是相同的。这里的例子只是展示了LINQtoObjects(对集合进行操作的“内存中的LINQ”)与LINQtoSQL是多么相似。
1.5COM和动态类型
我要介绍的最后一个特性是C#4特有的。LINQ是C#3的主要内容,而互操作性是C#4最重要的主题。这包括处理旧的COM技术,以及全新的执行在DLR(DynamicLanguageRuntime,动态语言运行时)上的动态语言。我们要将产品列表导出到一个Excel数据表中。
1.5.1简化COM互操作
要让数据出现在Excel中可以有很多种方式,但使用COM来控制是最强大最灵活的。不幸的是,以前的C#操作起COM来实在是太复杂,VB就要好得多。C#4扭转了这种情况。代码清单1-19展示了如何将数据保存到新的电子表格中。
代码清单1-19?使用COM将数据保存到Excel中(C#4)
尽管这可能并没有我们想象得那么完美,但已经比C#以前的版本好多了。事实上,你已经在此了解了一些C#4的特性,但这里有几个并不太明显的新特性,具体如下。
使用命名实参调用SaveAs。
调用函数时,许多可选参数都可以省略实参。特别是SaveAs,正常情况下它可能还会有10个额外的实参!
C#4可以将PIA(PrimaryInteropAssembly,主互操作程序集)的相关部分内嵌到调用代码中,因此不必再单独部署PIA。
在C#3中,由于ActiveSheet属性的类型为object,因此对worksheet赋值时如果不强制转换则会失败。在使用内嵌的PIA特性时,ActiveSheet的类型变为dynamic,从而使其他所有特性得以实现。
此外,在操作COM时,C#4还支持命名索引器,本例没有演示该特性。
我已经介绍了最终的特性:使用全新dynamic类型的动态类型。
1.5.2?与动态语言互操作
动态类型是一个非常大的话题,它会在本书最后单独成章,而且篇幅很长。这里我只介绍一个小小的示例,向你展示它能做什么。假设我们的产品没有存储在数据库、XML或内存中。我们可以通过Web服务来访问,但只能使用Python代码。Web服务中的代码使用了Python的动态特性来构建结果,没有声明你要访问的属性的类型。相反,它要求你来指定属性,并试图在执行时理解你的意图。对于Python这类语言来说,这些都是很平常的事情。但如何用C#来访问结果呢?
答案是使用dynamic——一个新的类型,C#编译器允许你动态地使用该类型。如果一个表达式为dynamic类型,你可以调用其方法、访问其属性、将其作为方法的参数进行传递,等等。并且大多数常见的绑定过程都发生在执行时,而不是编译时。你可以将dynamic类型的值隐式转换为其他类型(因此在代码清单1-19中可以对工作表进行那样的转换)或其他有趣的东西。
即使在纯粹的C#代码中,这种功能也是很有用的,虽然不需要COM互操作,但可能会需要与动态语言交互,而这时往往会更有用。代码清单1-20展示了如何从IronPython中获取产品列表并打印出来。它还包括一些设置代码,可以在同一进程中运行Python代码。
代码清单1-20?运行IronPython并动态获取属性(C#4)
products和product都声明为动态类型,因此编译器允许我们对产品列表进行迭代并打印其属性,尽管它并不知道这是否可以执行成功。如果不小心出现笔误,如将product.ProductName写成了product.Name,也只能在执行时才知道失败。
这与C#的其他部分截然相反,它们是静态类型的。动态类型只有在表达式为dynamic时才有效:大多数C#代码都会自始至终保持静态类型。
晕了吗?放松一些,本书其他部分会讲得慢得多。具体来说,我会解释一些个别情况并深入阐述引入不同特性的原因,另外还将就使用这些特性的时机给出一些指导。
现在,我已经向你展示了C#的特性。其中有一些同时是库的特性,有一些同时是运行时(runtime)的特性。我还会对此进行更详细的解释,现在先让我们弄清楚我所说的是什么。
1.6 剖析.NET平台
最开始引入时,.NET这个词涵义甚广,用来包罗微软公司的多种技术。例如,WindowsLiveID曾被叫做.NETPassport,虽然它和目前的.NET没有任何明显的联系。幸好,这个混乱的局面逐渐平息下来了。本节要探讨.NET的各个组成部分。
本书会提到3种不同的特性:C#语言本身的特性、运行时的特性(可以认为运行时提供了程序运行的一个“引擎”)以及.NET框架库的特性。本书重点是在C#语言上。通常只有在与C#本身的特性有关时,才会解释运行时和框架的特性。但是,只有在你清楚地理解了三者的差异之后,才能真正理解这些特性。特性经常会发生重叠,但重要的是理解其中的基本原理。
1.6.1?C#语言
C#语言是由它的规范来定义的。C#规范描述了C#源代码的格式,其中包括语法和行为。规范中并没有描述编译器输出要在什么平台上运行,只描述了两者进行交互的要点。例如,C#语言需要一个名为System.IDisposable的类型,其中包含一个名为Dispose的方法。它们是定义using语句所必需的。同样,平台需要(不管以什么样的形式)同时支持值类型和引用类型,另外还要支持垃圾回收。
理论上,任何平台只要支持要求的特性,C#编译器就可以以它为目标平台。例如,C#编译器除了可以以IL(IntermediateLanguage,中间语言,是本书写作期间最常见的一种输出形式)形式输出外,还可以以其他合法形式输出。运行时完全可以对C#编译器的输出进行解释,或将其直接完全转换为本机代码(nativecode),而不必非要对它进行JIT编译。尽管这些情况比较少见,但它们确实存在。例如,微框架使用了解释器,Mono也是如此。另一方面,NGen和MonoTouch(构建iPhone应用的平台,参见http:monotouch.net)都使用了前期编译(ahead-of-timecompilation)。
1.6.2运行时
.NET平台的运行时部分是数量相当少的一些代码,它们负责确保用IL写的程序以符合CLI(CommonLanguageInfrastructure,公共语言基础设施)规范PartitionsI~III的方式执行。CLI的运行时部分称为CLR(公共语言运行时)。本书以后提到CLR时,是指微软实现的CLR。
语言的一些元素永远不会在运行时的级别上出现,但也有一些元素越过了这个界线。例如,枚举器(enumerator)就不是在运行时的级别上定义的。相比之下,虽然数组和委托对IDisposable接口来说没有任何特别的含义,但它们对于“运行时”来说都是十分重要的。
1.6.3框架库
库提供了可供我们在程序中使用的代码。.NET中的库主要是以IL的形式构建的,只有在必要的时候才使用本机代码。这是运行时优势的一个体现:你自己写的代码并非天生就是“二等公民”——它完全能够提供与它利用的库一样强大的功能和性能。库中的代码量要比运行时的代码量多得多,这跟车和引擎的关系是一样的。
.NET库得到了部分标准化。CLI规范的PartitionIV提供了大量不同的概要(profile)协议和核心内容库。PartitionIV包含两个部分。第一部分是对库的常规文字描述,包括哪些配置文件中包括哪些库。第二部分则以XML格式描述了库本身的细节。这是在C#中使用XML注释时生成的相同形式的文档。
不过,.NET中有许多东西都不是基本库所定义的。如果写一个程序,在其中只使用来自规范定义的库,而且以正确的方式使用它们,那么代码应该能在任何实现(包括Mono、.NET等)上顺利地运行。但在实际应用中,几乎任意规模的任意程序都会使用非标准的库,如WindowsForms或ASP.NET。Mono项目也有它自己的、不是.NET一部分的库,如GTK#。另外,它也实现了许多非标准的库。
.NET一词是指微软公司提供的运行时和库的组合,其中也包含C#和VB.NET编译器。可以把它视为在Windows顶部构建的一个完整的开发平台。.NET各个部分的版本各不相同,这可能会造成混淆。附录C提供了一个概要,指明哪个部分的哪个版本在什么时候发布,以及包含哪些特性。
弄清楚这些之后,在开始深入研究C#之前,我还有一点需要强调。
1.7怎样写出超炫的代码
很抱歉起了这么一个容易引起误会的标题。本节就此而言并不会让你的代码更优美。它甚至不会让你感觉清爽。但它对你理解本书大部分内容都会有所帮助,因此你一定要阅读本节。本来这种问题应该写在前面(第1页以前),但我知道很多读者都会跳过那部分,直接进入正文。我对此表示理解,因此我会尽快介绍这些内容。
1.7.1 采用代码段形式的全能代码
写一本有关计算机语言(脚本语言除外)的书时,其中的一个挑战就是完整的程序(无须添加额外的代码,读者能直接编译和运行的程序),这些程序很快就会变得非常长。我想要解决这个问题,从而为你提供可以轻松键入和试验的代码:我认为实际键入代码的学习效果要比只读一读好得多。
只要有恰当的程序集引用和using指令,就能用极少量的C#代码做相当多的事情。但是,最麻烦的地方就是写那些using指令,然后声明一个类,然后声明一个Main方法。在所有这些“准备工作”都完成之后,才能动手写第一行真正有用的代码。我的例子基本上都是代码段的形式,忽略了在一个简单的程序中显得过于繁琐的那些“准备过程”,将精力集中在最重要的东西上面。我编写了一个小工具Snippy,可以直接运行这些代码段。
如果代码段不包含省略号(…),那么所有的代码都将被认为是程序Main方法的方法体。如果有一个省略号,那么省略号之前的代码为方法和内嵌类的声明,之后的代码为Main方法的内容。例如下面的代码段:
Snippy可以将其扩展为下面的形式:
实际上,Snippy所包含的using指令要多得多,但扩展后的版本已经很长了。注意,所生成的类永远都叫Snippet,声明在代码段之中的任何类型都是该类的内嵌类。
本书的网站上详细介绍了如何使用Snippy(http:mng.bzLh82),还包含所有示例的代码段和位于VisualStudio解决方案中的扩展版本。此外,它还支持LINQPad(http:www.linqpad.net),它是由JoeAlbahari开发的一个类似的工具,为研究LINQ提供了一些特别有用的功能。
接下来,我们来看一看之前的代码有没有什么不妥之处。
1.7.2?教学代码不是产品代码
如果你理解了本书所有的代码,不进一步思考就直接将其用到你的应用中,这没有什么问题,但我强烈建议你不要这么做。大多数示例都是为了演示某个特定的知识点,仅此而已。例如,大多数代码段都不包含参数验证、访问修饰符、单元测试和文档。当这些代码段在超出其预期的上下文使用时,就可能会失败。例如前面反转字符串的方法体,本书将多次使用这段代码。
先不管参数验证,这段代码可以反转字符串中以UTF-16编码的代码点序列,但在某些情况下,这还不够全面。比如一个由e和表示重音的组合字符组成的单个文字(即é),不能交换它们在序列中的位置,否则重音将用在错误的字符上。再比如某个字符串包含从代理对形成的基本多文种平面(basicmultilingualplane)之外的字符,重新排序可能会导致无效的UTF-16字符串。要修复这些问题需要更复杂的代码,反而容易让你忽略真正想表达的东西。
你可以随意使用本书中的代码,但请记住本节的忠告:从这些代码中获取灵感,这要比照抄照搬并认为它们能满足你特定的需求强多了。
最后,你还应该下载一本书,它涵盖了本书绝大部分内容。
1.7.3?你的新朋友:语言规范
我竭尽全力使本书正确无误,但是错误在所难免。你可以在本书的网站上查看已提交的错误列表(http:mng.bzm1Hh)。如果你发现了任何错误,可以给我发邮件(skeet@podox.com)或在作者论坛上发帖(http:mng.bzgi4q)。不过,你有可能在收到我的反馈之前就已经解决了问题,又或许你的问题不在本书讨论范畴之内。归根结底,对于C#行为最权威的资源是语言规范。
规范有两种重要的形式——ECMA国标标准规范和微软规范。在撰写本书时,ECMA规范尽管已是第4版,却只涵盖了C#2。谁也不知道它是否会更新,以及什么时候更新,但微软的版本是完整的,并且是免费的。本书的网站上包含这两种规范所有可用版本的链接(http:mng.bz8s38)。我在本书中所指的规范中的某一节,使用的是微软C#4规范的序号,即便谈及的是之前的语言版本时也是如此。我强烈建议你下载该版本,并在遇到某些怪异的情况时及时查阅。
我的目标之一是让程序员人手一本规范,它提供一个更易于面向开发者的方式,覆盖日常编码中的方方面面,而不用记住编译器作者所要求的全部细节。话虽如此,作为一种规范它其实极其易读,你不应被吓到。如果你对规范感兴趣,有一本书是针对C#3的注解版规范(http:mng.bz0y9c),包含C#团队和其他编著者一些令人着迷的注释。该书的C#4版本目前正在编撰中。
1.8 小结
本章展示了(但没有解释)本书要深入解析的一些特性。但还有许多没在这里展示,而且到现在为止见过的所有特性都有关联的“子特性”(subfeature)。希望本章的内容能激发你对本书剩余部分的兴趣。
本章大部分内容都是在介绍特性,不过我们也谈到了另外一些话题,比如如何阅读本书才能收获最大。我解释了语言、运行时、库以及本书的代码形式。
讲解C#2的特性之前,还有一个领域是必须涉及的,那就是C#1。显然,作为一个作者,我不知道你对C#1的了解程度如何。但是,我确实知道C#1的哪些主题很难理解,有的主题是真正掌握更高版本C#的关键,所以在下一章中,我将详细地讨论它们。
……
|
|