**《果壳中的C# C# 5.0 权威指南》

========== ========== ==========
[作者] (美) Joseph Albahari (美) Ben Albahari
[译者] (中) 陈昇 管学理 曾少宁 杨庆川
[出版] 中国水利水电出版社
[版次] 2013年08月 第1版
[印次] 2013年08月 第1次 印刷
[定价] 118.00元
========== ========== ==========

【前言】

C# 5.0 是微软旗舰编程语言的第4次重大升级。

C# 5.0 及相关 Framework 的新特性已经被标注清楚,因此也可以将本书作为 C# 4.0 参考书使用。

【第01章】

(P001)

C# 在面向对象方面的特性包括:

  1. 统一的类型系统 —— C# 中的基础构建块是一种被称为类型的数据与函数的封装单元。C# 有一个统一的类型系统,其中所有类型最终都共享一个公共的基类。这意味着所有的类型,不管它们是表示业务对象,或者像数字等基本类型,都共享相同的基本功能集;

  2. 类与接口 —— 在纯粹的的面向对象泛型中,唯一的类型就是类。但是 C# 中还有其他几种类型,其中一种是接口。接口与类相似,但它只是某种类型的定义,而不是实现。在需要用多继承时,它是非常有用的;

  3. 方法、属性与事件 —— 在纯粹的面向对象泛型中,所有函数都是方法。在 C# 中,方法只是一种函数成员,也包含一些属性和事件以及其他组成部分。属性是封装了一部分对象状态的函数成员。事件是简化对象状态变化处理的函数成员;

C# 首先是一种类型安全的语言,这意味着类型只能够通过它们定义的协议进行交互,从而保证每一种类型的内部一致性。

C# 支持静态类型化,这意味着这种语言会在编译时执行静态类型安全性检查。

(P002)

静态类型化能够在程序运行之前去除大量的错误。

C# 允许部分代码通过新的 dynamic 关键字来动态指定类型。然而,C# 在大多数情况下仍然是一种静态类型化的语言。

C# 之所以被称为一种强类型语言,是因为它的类型规则是非常严格的。

C# 依靠运行时环境来执行自动的内存管理。

C# 并没有去除指针 : 它只是使大多数编程任务不需要使用指针。对于性能至关重要的热点和互操作性方面,还是可以使用指针,但是只允许在显式标记为不安全的代码块中使用。

C# 依赖于一个运行时环境,它包括许多特性,如自动内存管理和异常处理。

(P003)

.NET Framework 由名为 Common Language Runtime (CLR) 的运行时环境和大量的程序库组成。这些程序库由核心库和应用库组成。

CLR 是执行托管代码的运行时环境。C# 是几种将源代码编译为托管语言之一。托管代码会被打包成程序集,它可以是可执行文件或程序库的形式,包括类型信息或元数据。

托管代码用 Intermediate Language 或 IL 表示。

Red Gate 的 .Net Reflector 是一个重要的分析程序集内容的工具 (可以将它作为反编译器使用) 。

CLR 是无数运行时服务的主机。这些服务包括内存管理、程序库加载和安全性服务。

CLR 是与语言无关的,它允许开发人员用多种语言开发应用程序。

(P004)

.NET Framework 由只支持基于所有 Windows 平台或 Web 的应用程序的程序库组成。

C# 5.0 还实现了 Windows Runtime (WinRT) 库的互操作。

WinRT 是一个扩展接口和运行时环境,它可以用面向对象和与语言无关的方式访问库。Windows 8 带有这个运行时库,属于 Microsoft 组件对象模型或 COM 的扩展版本。

Windows 8 带有一组非托管 WinRT 库,它是通过 Microsoft 应用商店交付的支持触摸屏的 Metro 风格应用程序框架。作为 WinRT ,这些程序库不仅可以通过 C# 和 VB 访问,也可以通过 C++ 和 JavaScript 访问。

WinRT 与普通 COM 的区别是,WinRT 的程序库支持多种语言,包括 C# 、 VB 、 C++ 和 JavaScript,所以每一种语言 (几乎) 都将 WinRT 类型视为自己的专属类型。

(P005)

C# 5.0 两个较大的新特性是通过两个关键字 (async 和 await) 支持异步功能 (asynchronous function)。

C# 4.0 增加的新特性有 : 动态绑定、可选参数和命名参数、用泛型接口和代理实现类型变化、改进 COM 互操作性。

C# 3.0 增加的这些特性主要集中在语言集成查询功能上 (Language Integrated Query,简称 LINQ) 。

C# 3.0 中用于支持 LINQ 的新特性还包括隐式类型化局部变量 (Var) 、匿名类型、对象构造器、 Lambda 表达式、扩展方法、查询表达式和表达式树。

(P006)

C# 3.0 也增加了自动化和局部方法。

【第02章】

(P007)

在 C# 中语句按顺序执行。每个语句都以分号 (;) 结尾。

C# 语句按顺序执行,以分号 (;) 结尾。

(P008)

方法是执行一系列语句的行为。这些语句叫做语句块。语句块由一对大括号中的 0 个或多个语句组成。

编写可调用低级函数的高级函数可以简化程序。

方法可以通过参数来接收调用者输入的数据,并通过返回类型给调用者返回输出数据。

C# 把 Main 方法作为程序的默认执行入口。 Main 方法也可以返回一个整数 (而不是 void) ,从而为程序执行的环境返回一个值。 Main 方法也可以接受一个字符串数组作为参数 (数组中包含可传递给可执行内容的任何参数) 。

数组代表某种特定类型,固定数量的元素的集合。数组由元素类型和它后面的方括号指定。

类由函数成员和数据成员组成,形成面向对象的构建块。

(P009)

在程序的最外层,类型被组织到命名空间中。

.NET Framework 的组织方式为嵌套的命名空间。

using 指令仅仅是为了方便,也可以用 “命名空间 + 类型名” 这种完全限定名称来引用某种类型。

C# 编译器把一系列 .cs 扩展名的源代码文件编译成程序集。

程序集是 .NET 中的最小打包和部署单元。

一个程序集可以是一个应用程序,或者是一个库。

一个普通的控制台程序或 Windows 应用程序是一个 .exe 文件,包含一个 Main 方法。

一个库是一个 .dll 文件,它相当于一个没有入口的 .exe 文件。

库是用来被应用程序或其他的库调用 (引用) 的。

.NET Framework 就是一组库。

C# 编译器名称是 csc.exe。可以使用像 Visual Studio 这样的 IDE 编译 C# 程序,也可以在命令行中手动调用 csc 命令编译 C# 程序。

(P010)

标识符是程序员为类、方法、变量等选择的名字。

标识符必须是一个完整的词、它是由字母和下划线开头的 Unicode 字符组成的。

C# 标识符是区分大小写的。

通常约定参数、局部变量和私有变量字段应该由小写字母开头,而其他类型的标识符则应该由大写字母开头。

关键字是编译器保留的名称,不能把它们用作标识符。

如果用关键字作为标识符,可以在关键字前面加上 @ 前缀。

@ 并不是标识符的一部分。

@ 前缀在调用其他有不同关键字的 .NET 语言编写的库时非常有用。

(P011)

点号 (.) 表示某个对象的成员 (或数字的小数点)。

括号在声明或调用方法时使用,空括号在方法没有参数时使用。

等号则用于赋值操作。

C# 提供了两种方式的注释 : 单行注释和多行注释。

单行注释由双斜线开始,到本行结束为止。

多行注释由 / 开始,由 / 结束。

变量代表它的值可以改变,而常量则表示它的值不可以更改。

(P012)

C# 中所有值都是一种类型的实例。一个值或一个变量所包含的一组可能值均由其类型决定。

预定义类型是指那些由编译器特别支持的类型。

预定义类型 bool 只有两种值 : true 和 false 。 bool 类型通常与 if 语句一起用于条件分支。

在 C# 中,预定义类型 (也称为内建类型) 被当做 C# 关键字。在 .NET Framework 中的 System 命名空间下包含了很多并不是预定义类型的重要类型。

正如我们能使用简单函数来构建复杂函数一样,也可以使用基本类型来构建复杂类型。

(P013)

类型包含数据成员和函数成员。

C# 的一个优点就是预定义类型和自定义类型只有很少的不同。

实例化某种类型即可创建数据。

预定义类型可以简单地通过字面值进行实例化。

new 运算符用于创建自定义类型的实例。

使用 new 运算符后会立刻实例化一个对象,对象的构造方法会在初始化时被调用。

构造方法像方法一样被定义,不同的是方法名和返回类型简化成它所属的类型名。

由类型的实例操作的数据成员和函数成员被称为实例成员。

在默认情况下,成员就是实例成员。

(P014)

那些不是由类型的实例操作而是由类型本身操作的数据成员和函数成员必须标记为 static 。

public 关键字将成员公开给其他类。

把成员标记为 public 就是在说 : “这就是我想让其他类型看到的,其他的都是我自己私有的” 。

用面向对象语言,我们称之为公有 (public) 成员封装了类中的私有 (private) 成员。

在 C# 中,兼容类型的实例可以相互转换。

转换始终会根据一个已经存在的值创建一个新的值。

转换可以是隐式或显式。

隐式转换自动发生,而显式转换需要 cast 关键字。

long 容量是 int 的两倍。

(P015)

隐式转换只有在下列条件都满足时才被允许 :

  1. 编译器能保证转换总是成功;

  2. 没有信息在转换过程中丢失;

只有在满足下列条件时才需要显式转换:

  1. 编译器不能保证转换总是能成功;

  2. 信息在转换过程中有可能丢失;

C# 还支持引用转换,装箱转换和自定义转换。

对于自定义转换,编译器并没有强制遵守上面的规则,所以设计不好的类型有可能在转换时出现预想不到的结果。

所有 C# 类型可以分成以下几类 : 值类型、引用类型、泛型类型、指针类型。

值类型包含大多数内建类型 (具体包括所有的数值类型、 char 类型和 bool 类型) 以及自定义 struct 类型和 enum 类型。

引用类型包括所有的类、数据、委托和接口类型。

值类型和引用类型最根本的不同是它们在内存中的处理方式。

值类型变量或常量的内容仅仅是一个值。

可以通过 struct 关键字定义一个自定义值类型。

对值类型实例的赋值操作总是会复制这些实例。

将一个非常大的 long 转换成 double 类型时,有可能造成精度丢失。

(P016)

引用类型比值类型复杂,它由两部分组成 : 对象和对象的引用。

引用类型变量或常量的内容是对一个包含值的对象的引用。

(P017)

一个引用可以赋值为字面值 null,这表示它不指向任何对象;

相对的,值类型通常不能有 null 值;

C# 中也有一种代表类型值为 null 的结构,叫做可空 (nullable) 类型。

(P018)

值类型实例正好占用需要存储其字段的内存。

从技术上说,CLR 用整数倍字段的大小来分配内存地址。

引用类型要求为引用和对象单独分配存储空间。

对象占用了和字段一样的字节数,再加上额外的管理开销。

每一个对象的引用都需要额外的 4 或 8 字节,这取决于 .NET 运行时是运行在 32 位平台还是 64 位平台上。

C# 中的预定义类型又称框架类型,它们都在 System 命名空间下。

在 CLR 中,除了 decimal 之外的一系列预定义值类型被认为是基本类型。之所以将其称为基本类型,是因为它们在编译过的代码中被指令直接支持。因此它们通常被翻译成底层处理器直接支持的指令。

(P019)

System.IntPtr 和 System.UIntPtr 类型也是基本类型。

在整数类型中,int 和 long 是最基本的类型, C# 和运行时都支持它们。其他的整数类型通常用于实现互操作性或存储空间使用效率非常重要的情况。

在实数类型中,float 和 double 被称为浮点类型,通常用于科学计算。

decimal 类型通常用于要求10位精度以上的数值计算和高精度的金融计算。

整型字面值可使用小数或十六进制小数标记,十六进制小数用 0x 前缀表示。

实数字面值可使用小数和指数标记。

从技术上说,decimal 也是一种浮点类型,但是在 C# 语言规范中通常不将其认为是浮点类型。

(P020)

默认情况下,编译器认为数值字面值或者是 double 类型或者是整数类型 :

  1. 如果这个字面值包含小数点或指数符号 (E),那么它被认为是 double ;

  2. 否则,这个字面值的类型就是下列能满足这个字面值的第一个类型 : int 、 uint 、 long 和 ulong ;

数值后缀显式地定义了一个字面值的类型。后缀可以是下列小写或大写字母 : F (float) 、 D (double) 、 M (decimal) 、 U (uint) 、 L (long) 、 UL (ulong) 。

后缀 U 、 L 和 UL 很少需要,因为 uint 、 long 和 ulong 总是可以表示 int 或从 int 隐式转换过来的类型。

从技术上讲,后缀 D 是多余的,因为所有带小数点的字面值都被认为是 double 类型。总是可以给一个数字类型加上小数点。

后缀 F 和 M 是最有用的,它在指定 float 或 decimal 字面值时使用。

double 是无法隐式转换成 float 的,同样的规则也适用于 decimal 字面值。

整型转换在目标类型能表示源类型所有可能的值时是隐式转换,否则需要显式转换。

(P021)

float 能隐式转换成 double ,因为 double 能表示所有可能的 float 的值。反过来则必须是显式转换。

所有的整数类型可以隐式转换成浮点数,反过来则必须是显式转换。

将浮点数转换成整数时,小数点后的数值将被截去,而不会四舍五入。

静态类 System.Convert 提供了在不同值类型之间转换的四舍五入方法。

把一个大的整数类型隐式转换成浮点类型会保留整数部分,但是有时会丢失精度。这是因为浮点类型总是有比整数类型更大的数值,但是可能只有更少的精度。

所有的整数类型都能隐式转换成 decimal 类型,因为小数类型能表示所有可能的整数值。其他所有的数值类型转换成小数类型或从小数类型转换到数值类型必须是显式转换。

算术运算符 (+ 、 - 、 * 、 / 、 %) 用于除了 8 位和 16 位的整数类型之外的所有数值类型。

自增和自减运算符 (++ 、 --) 给数值加 1 或减 1 。这两个运算符可以放在变量的前面或后面,这取决于你想让变量在计算表达式之前还是之后被更新。

(P022)

整数类型的除法运算总是会截断余数。用一个值为 0 的变量做除数将产生一个运行时错误 (DivisionByZeroException) 。

用字面值 0 做除数将产生一个编译时错误。

整数类型在运行算术运算时可能会溢出。默认情况下,溢出默默地发生而不会抛出任何异常。尽管 C# 规范不能预知溢出的结果,但是 CLR (通用语言运行时) 总是会造成溢出行为。

checked 运算符的作用是在运行时当整型表达式或语句达到这个类型的算术限制时,产生一个 OverflowException 异常而不是默默的失败。

checked 运算法在有 ++ 、 -- 、 + 、 - (一元运算符和二元运算符) 、 * 、 / 和整数类型间显式转换运算符的表达式中起作用。

checked 操作符对 double 和 float 数据类型没有作用,对 decimal 类型也没有作用 (这种类型总是受检的)。

checked 运算符能用于表达式或语句块的周围。

可以通过在编译时加上 /checked+ 命令行开关 (在 Visual Studio 中,可以在 Advanced Build Settings 中设置) 来默认使程序中所有表达式都进行算术溢出检查。如果你只想禁用指定表达式或语句的溢出检查,可以用 unchecked 运算符。

(P023)

无论是否使用了 /checked 编译器开关,编译时的表达式计算总会检测溢出,除非应用了 unchecked 运算符。

C# 支持如下的位运算符 : ~ (按位取反) 、 & (按位与) 、 | (按位或) 、 ^ (按位异或) 、 << (按位左移) 、 >> (按位右移) 。

8 位和 16 位整数类型指的是 byte 、 sbyte 、 short 和 ushort 。这些类型缺少它们自己的算术运算符,所以 C# 隐式把它们转换成所需的大一些类型。

不同于整数类型,浮点类型包含某些操作要特殊对待的值。这些特殊的值是 NaN (Not a Number) 、 +∞ 、 -∞ 和 -0 。

float 和 double 类型包含用于 NaN 、 +∞ 、 -∞ 值 (MaxValue 、 MinValue 和 Epsilon) 的常量。

(P024)

非零值除以零的结果是无穷大。

零除以零或无穷大减去无穷大的结果是 NaN。

使用比较运算符 (==) 时,一个 NaN 的值永远也不等于其他的值,甚至不等于其他的 NaN 值。

必须使用 float.IsNaN 或 double.IsNaN 方法来判断一个值是不是 NaN 。

无论何时使用 object.Equals 方法,两个 NaN 的值都是相等的。

NaN 在表示特殊值时很有用。

float 和 double 遵循 IEEE 754 格式类型规范,原生支持几乎所有的处理器。

double 类型在科学计算时很有用。

decimal 类型在金融计算和计算那些 “人为” 的而非真实世界的值时很有用。

(P025)

float 和 double 在内部是基于 2 来表示数值的。因此只有基于 2 表示的数值才能被精确的表示。事实上,这意味着大多数有小数的字面值 (它们基于10) 将无法精确的表示。

decimal 基于 10,它能够精确地表示基于10的数值 (也包括它的因子,基于2和基于5) 。因为实型字面值是基于 10 的,所以 decimal 能精确地表示像 0.1 这样的数。然而,double 和 decimal 都不能精确表示那些基于 10 的极小数。

C# 中的 bool (System.Boolean 类型的别名) 能表示 true 和 false 的逻辑值。

尽管布尔类型值仅需要 1 位存储空间,但是运行时却用 1 字节空间。这是因为字节是运行时和处理器能够有效使用的最小单位。为避免在使用数组时的空间浪费,.NET Framework 提供了 System.Collections 命名空间下的 BitArray 类,它被设置成每个布尔值使用 1 位。

bool 不能转换成数值类型,反之亦然。

== 和 != 运算符用于判断任何类型相等还是不相等,总是返回一个 bool 值。

(P026)

对于引用类型,默认情况的相同是基于引用的,而不是底层对象的实际值。

相等和比较运算符 == 、 != 、 < 、 > 、 >= 和 <= 用于所有的数值类型,但是用于实数时要特别注意。

比较运算符也用于枚举 (enum) 类型成员,它比较枚举的潜在整数值。

&& 和 || 运算符用于判断 “与” 和 “或” 条件。它们常常与代表 “非” 的 (!) 运算符一起使用。

&& 和 || 运算符会在可能的情况下执行短路计算。

短路计算在允许某些表达式时是必要的。

& 和 | 运算符也用于判断 “与” 和 “或” 条件。

不同之处是 & 和 | 运算符不支持短路计算。因此它们很少用于代替条件运算符。

不同于 C 和 C++ , & 和 | 运算符在用于布尔表达式时执行布尔比较 (非短路计算) 。& 和 | 运算符只在用于数值运算时才执行位操作。

三元条件运算符 (简称为条件运算符) 使用 q ? a : b 的形式,它在条件 q 为真时,计算 a,否则计算 b 。

(P027)

条件表达式在 LINQ 语句中特别有用。

C# 中的 char (System.Char 类型的别名) 表示一个 Unicode 字符,它占用 2 个字节。字符字面值在单引号 (') 中指定。

转义字符不能按照字面表示或解释。转义字符由反斜杠()和一个表示特殊意思的字符组成。

\' 单引号
\" 双引号
\ 斜线
\0 空
\a 警告
\b 退格
\f 走纸
\n 换行
\r 回车
\t 水平制表符
\v 垂直制表符

\u (或 \x ) 转义字符通过 4 位十六进制代码来指定任意 Unicode 字符。

从字符类型到数值类型的隐式转换只在这个数值类型可以容纳无符号 short 类型时有效。对于其他的数值类型,则需要显式转换。

(P028)

C# 中的字符串类型 (System.String 的别名) 表示一些不变的、按顺序的 Unicode 字符。字符串字面值在双引号 (") 中指定。

string 类型是引用类型而不是值类型,但是它的相等运算符却遵守值类型的语义。

对 char 字面值有效的转移字符在字符串中也有效。

C# 允许逐字字符串字面值,逐字字符串字面值要加前缀 @ ,它不支持转义字符。

逐字字符串字面值也可以贯穿多行。

可以通过在逐字字符串中写两次的方式包含双引号字符。

(+) 运算符连接两个字符串。

右面的操作对象可以是非字符串类型的值,在这种情况下这个值的 ToString 方法将被调用。

既然字符串是不变的,那么重复地用 (+) 运算符来组成字符串是低效率的 : 一个更好的解决方案是用 System.Text.StringBuilder 类型。

(P029)

字符串类型并不支持 < 和 > 的比较,必须使用字符串类型的 CompareTo 方法。

数组代表固定数量的特定类型元素,为了高效率地读取,数组中的元素总是存储在连续的内存块中。

数组用元素类型后加方括号表示。

方括号也可以检索数组,通过位置读取特定元素。

数组索引从 0 开始。

数组的 Length 属性返回数组中的元素数量。一旦数组被建立,它的长度将不能被更改。

System.Collection 命名空间和子命名空间提供了像可变数组等高级数据结构。

数组初始化语句定义了数组中的每个元素。

所有的数组都继承自 System.Array 类,它提供了所有数组的通用服务。这些成员包括与数组类型无关的获取和定义元素的方法。

建立数组时总是用默认值初始化数组中的元素,类型的默认值是值为 0 的项。

无论数组元素类型是值类型还是引用类型都有重要的性能影响,若元素类型是值类型,每个元素的值将作为数组的一部分进行分配。

(P030)

无论是任何元素类型,数组本身总是引用类型对象。

多维数组分为两种类型 : “矩形数组” 和 “锯齿形数组” 。 “矩形数组” 代表 n 维的内存块,而 “锯齿形数组” 则是数组的数组。

矩形数组声明时用逗号 (,) 分隔每个维度。

数组的 GetLength() 方法返回给定维度的长度 (从 0 开始) 。

锯齿形数组在声明时用两个方括号表示每个维度。

锯齿形数组内层维度在声明时可不指定。

不同于矩形数组,锯齿形数组的每个内层数组都可以是任意长度;每个内层数组隐式初始化成空 (null) 而不是一个空数组;每个内层数组必须手工创建。

有两种方式可以简化数组初始化表达式。第一种是省略 new 运算符和类型限制条件,第二种是使用 var 关键字,使编译器隐式确定局部变量类型。

(P032)

隐式类型转换能进一步用于一维数组的这种情况,能在 new 关键字之后忽略类型限制符,而由编译器推断数组类型。

为了使隐式确定数组类型正常工作,所有的元素都必须可以隐式转换成同一种类型。

运行时给所有的数组索引进行边界检查,如果使用了不合法的索引,就会抛出 IndexOutOfRangeException 异常。

和 Java 一样,数组边界检查对类型安全和简化调试是很有必要的。

通常来说,边界检查的性能消耗很小,即时编译器会进行优化。像在进入循环之前预先检查所有的索引是不安全的,以此来避免在每轮循环中都检查索引。

C# 提供 "unsafe" 关键字来显式绕过边界检查。

变量表示存储着可变值的存储空间,变量可以是局部变量、参数 (value 、 ref 或 out) 、 字段 (instance 或 static) 或数组元素。

“堆” 和 “栈” 是存储变量和常量的地方,它们每个都有不同的生存期语义。

“栈” 是存储局部变量和参数的内存块,栈在进入和离开一个函数时逻辑增加和减少。

(P033)

“堆” 是指对象残留的内存块,每当一个新的对象被创建时,它就被分配进堆,同时返回这个对象的引用。

当程序执行时,堆在新对象创建时开始填充。

.NET 运行时有垃圾回收器,它会定期从堆上释放对象。

只要对象没有被引用,他就会被选中释放。

无论变量在哪里声明,值类型实例以及对象引用一直存在。如果声明的实例作为对象中的字段或数组元素,那么实例存储于堆上。

在 C# 中你无法显式删除对象,但在 C++ 中可以。未引用的对象最终被垃圾回收器回收。

堆也存储静态字段和常量。不同于堆上被分配的对象 (可以被垃圾回收器回收),静态字段和常量将一直存在直到应用程序域结束。

C# 遵守明确赋值的规定。在实践中,这是指在没有 unsafe 上下文情况下是不能访问未初始化内存的。明确赋值有三种含义 :

  1. 局部变量在读取之前必须被赋值;

  2. 当调用方法时必须提供函数的参数;

  3. 其他的所有变量 (像字段和数组元素) 都自动在运行时被初始化;

(P034)

字段和数组元素都会用其类型的默认值自动初始化。

所有类型实例都有默认值。预定义类型的默认值是值为 0 的项 :

[类型] - [默认值]

所有引用类型 - null
所有数值和枚举类型 - 0
字符类型 - '\0'
布尔类型 - false

能够对任何类型使用 default 关键字来获得其默认值。

自定义值类型中的默认值与自定义类型定义的每个字段的默认值相同。

方法有一连串的参数,其中定义了一系列必须提供给方法的参数。

(P035)

能通过 ref 和 out 修饰符来改变参数传递的方式 :

[参数修饰符] - [传递类型] - [必须明确赋值的参数]

none - 值类型 - 传入
ref - 引用类型 - 传入
out - 引用类型 - 传出

通常,C# 中参数默认是按值传递的,这意味着在将参数值传给方法时创建参数值的副本。

值传递引用类型参数将赋值给引用而不是对象本身。

(P036)

如果按引用传递参数,C# 使用 ref 参数修饰符。

注意 ref 修饰符在声明和调用时都是必需的,这样就清楚地表明了将执行什么。

ref 修饰符对于转换方法是必要的。

无论参数是引用类型还是值类型,都可以实现值传递或引用传递。

out 参数和 ref 参数类似,除了 :

  1. 不需要在传入函数之前赋值;

  2. 必须在函数结束之前赋值;

(P037)

out 修饰符通常用于获得方法的多个返回值。

和 ref 参数一样, out 参数是引用传递。

当引用传递参数时,是为已存变量的存储空间起了个别名,而不是创建了新的存储空间。

params 参数修饰符在方法最后的参数中指定,它使方法接收任意数量的指定类型参数,参数类型必须声明为数组。

(P038)

也可以将通常的数组提供给 params 参数。

从 C# 4.0 开始,方法、构造方法和索引器都可以被声明成可选参数,只要在声明时提供默认值,这个参数就是可选参数。

可选参数在调用方法时可以被省略。

编译器在可选参数被用到的地方用了默认值代替了可选参数。

被其他程序集调用的 public 方法在添加可选参数时要求重新编译所有的程序集,因为参数是强制的。

可选参数的默认值必须由常量表达式或无参数的值类型构造方法指定,可选参数不能被标记为 ref 或 out 。

强制参数必须在可选参数方法声明和调用之前出现 (params 参数例外,它总是最后出现)。

相反的,必须将命名参数和可选参数联合使用。

命名参数可以按名称而不是按参数的位置确定参数。

(P039)

命名参数能按任意顺序出现。

不同的是参数表达式按调用端参数出现的顺序计算。通常,这只对相互作用的局部有效表达式有所不同。

命名参数和可选参数可以混合使用。

按位置的参数必须出现在命名参数之前。

命名参数在和可选参数混合使用时特别有用。

如果编译器能够从初始化表达式中推断出变量的类型,就能够使用 var 关键字 (C# 3.0 中引入) 来代替类型声明。

因为是直接等价,所以隐式类型变量是静态指定类型的。

(P040)

当无法直接从变量声明中推断出变量类型时,var 关键字将降低代码的可读性。

表达式本质上表示的是值。最简单的表达式是常量和变量。表达式能够用运算符进行转换和组合。运算符用一个或多个输入操作数来输出新的表达式。

C# 中的运算符分为一元运算符、二元运算符和三元运算符,这取决它们使用的操作数数量 (1 、 2 或 3) 。

二元运算符总是使用中缀标记法,运算符在两个操作数中间。

基础表达式由 C# 语言内置的基础运算符表达式组成。

(. 运算符) 执行成员查找;

(() 运算符) 执行方法调用;

空表达式是没有值的表达式。

因为空表达式没有值,所以不能作为操作数来创建更复杂的表达式。

赋值表达式用 = 运算符将一个表达式的值赋给一个变量。

(P041)

赋值表达式不是空表达式,实际上它包含了赋值操作的值,因此能再加上另一个表达式。

复合赋值运算符是由其他运算符组合而成的简化运算符。

当表达式包含多个运算符时,运算符的优先级和结合性决定了计算的顺序。

优先级高的运算符先于优先级低的运算符执行。

如果运算符的优先级相同,那么运算符的结合性决定计算的顺序。

二元运算符 (除了赋值运算符、 lambda 运算符 、 null 合并运算符) 是左结合运算符。换句话说,它们是从左往右计算。

赋值运算符、 lambda 运算符、 null 合并运算符和条件运算符是右结合运算符。换句话说,它们从右往左计算。右结合运算符允许多重赋值。

(P043)

函数包含按出现的字面顺序执行的语句。语句块是大括号 ({}) 中出现的一系列语句。

(P044)

声明语句可以声明新变量,也可以用表达式初始化变量。声明语句以分号结束。可以用逗号分隔的列表声明多个同类型的变量。

常量的声明和变量声明类似,除了不能在声明之后改变它的值和必须在声明时初始化。

局部变量和常量的作用范围是在当前的语句块中。不能在当前的或嵌套的语句块中声明另一个同名的局部变量。

变量的作用范围是它所在的整个代码段。

表达式语句是表达式也是合法的语句,表达式语句必须改变状态或调用某些改变的状态,改变的状态本质上是指改变一个变量。

可能的表达式语句是 :

  1. 赋值表达式 (包括自增和自减表达式) ;

  2. 方法调用表达式 (有返回值的和无返回值的) ;

  3. 对象实例化表达式;

(P045)

当调用有返回值的构造函数或方法时,并不一定要使用返回值。除非构造函数或方法改变了某些状态,否则这些语句完全没作用。

C# 有下面几种语句来有条件地控制程序的执行顺序 :

  1. 选择语句 (if, switch) ;

  2. 条件语句 (? :) ;

  3. 循环语句 (while 、 do-while 、 for 、 foreach) ;

if 语句是否执行代码体取决于布尔表达式是否为真。

如果代码体是一条语句,可以省略大括号。

if 语句之后可以紧跟 else 分句。

在 else 分句中,能嵌套另一个 if 语句。

(P046)

else 分句总是与其前语句块中紧邻的未配对的 if 语句结合。

可以通过改变大括号的位置来改变执行顺序。

大括号可以明确地表明结构,这能提高嵌套 if 语句的可读性 (即使编译器并不需要)。

从语义上讲,紧跟着每一个 if 语句的 else 语句从功能上都是嵌套在 else 语句之中的。

switch 语句可以根据变量可能值的选择来转移程序的执行。

switch 语句可以拥有比嵌套 if 语句更加简短的代码,因为 switch 语句只要求表达式计算一次。

(P047)

只能在支持静态计算的类型表达式中使用 switch 语句,因此限制了它只适用于整数类型、字符串类型和枚举类型。

在每个 case 分句的结尾,必须用某种跳转语句明确说明下一步要执行的代码。这里有选项 :

  1. break (跳转到 switch 语句结尾) ;

  2. goto case x (跳转到另一个 case 分句) ;

  3. goto default (跳转到 default 分句) ;

  4. 任何其他的跳转语句 —— return 、 throw 、 continue 或 goto 标签;

当多于一个值要执行相同代码时,可以按顺序列出共同的 case 条件。

switch 语句的这种特性对于写出比嵌套 if-else 语句更清晰的代码来说很重要。

C# 能够用 while 、 do-while 、 for 和 foreach 语句重复执行一系列语句。

while 循环在布尔表达式为真时重复执行一段代码,这个表达式在循环体被执行之前被检测。

(P048)

do-while 循环在功能上不同于 while 循环的是它在语句块执行之后检测表达式 (保证语句块至少被执行一次) 。

for 循环类似有特殊分句的 while 循环,这些特殊分句用于初始化和累积循环变量。

for 循环有下面的3个分句 :

for (initialization-clause; condition-clause; interation-clause) {statement-or-statement-block}

initialization-clause : 在循环之前执行,用于初始化一个或多个循环变量;
condition-clause : 是布尔表达式,当它为真时,将执行循环体;
interation-clause : 在每次循环语句体之后执行,通常用于更新循环变量;

for 语句的3个部分都可以被省略,可以通过下面的代码来实现一个无限循环 (也可以用 while(true) 代替) : for (;;)

(P049)

foreach 语句遍历可枚举对象的每一个元素,大多数 C# 和 .NET Framework 中表示集合或元素列表的类型都是可枚举的。

数组和字符串都是可枚举的。

C# 中的跳转语句有 break 、 continue 、 goto 、 return 和 throw 。

跳转语句违背了 try 语句的可靠性规则,这意味着 :

  1. 跳转到 try 语句块之外的跳转总是在到达目的地之前执行 try 语句的 finally 语句块;

  2. 跳转语句不能从 finally 语句块内跳到块外;

break 语句用来结束循环体或 switch 语句体的执行。

continue 语句放弃循环体中其后的语句,继续下一轮循环。

(P050)

goto 语句用于转移执行到语句块中的另一个标签处,或者用于 switch 语句内。

标签语句仅仅是语句块中的占位符,用冒号后缀表示。

goto case case-constant 用于转移执行到 switch 语句块中的另一个条件。

return 语句退出方法,如果这个方法有返回值,同时必须返回方法指定返回类型的表达式。

return 语句能出现在方法的任意位置。

throw 语句抛出异常来表示有错误发生。

using 语句用于调用在 finally 语句块中实现 IDisposable 接口的 Dispose 方法。

C# 重载了 using 关键字,使它在不同上下文中有不同的含义。

特别注意 using 指令不同于 using 语句。

(P051)

lock 语句是调用 Monitor 类 Enter() 方法和 Exit() 方法的简化操作。

命名空间是类型名称必须唯一的作用域,类型通常被组织到分层的命名空间里,这样既避免了命名冲突又使类型名更容易被找到。

命名空间组成了类型名的基本部分。

命名空间是独立于程序集的。

程序集是像 .exe 或 .dll 一样的部署单元。

命名空间不影响成员的可见性 —— public 、 internal 、 private 等。

namespace 关键字为其中的类型定义了命名空间。

命名空间中的点 (.) 表明嵌套命名空间的层次结构。

可以用包含从外到内的所有命名空间的完全限定名来指代一种类型。

如果类型没有在任何命名空间中被定义,则说明它存在于全局命名空间内。

全局命名空间也包含了顶级命名空间。

using 指令用于导入命名空间。这是不使用完全限定名来指代某种类型的便捷方法。

(P052)

在不同命名空间定义相同类型名称是合法的 (而且通常是需要的)。

外层命名空间中声明的名称能够直接在内层命名空间中使用。

如果想使用同一命名空间分层结构的不同分支中的类型,你就要使用部分限定名。

如果相同的类型名出现在内层和外层命名空间中,内层的类型优先。如果要使用外层命名空间中的类型,必须使用它的完全限定名。

(P053)

所有的类型名在编译时都被转换成完全限定名,中间语言 (IL) 代码不包含非限定名和部分限定名。

可以重复声明同一命名空间,只要它里面的类型名不冲突。

我们能在命名空间中使用嵌套 using 指令,可以在命名空间声明中指定 using 指令的范围。

(P054)

引入命名空间有可能引起类型名的冲突,因此可以只引入需要的类型而不是整个命名空间,为每个类型创建别名。

外部别名允许引用两个完全限定名相同的类型,这种特殊情况只发生在两种类型来自不同的程序集。

(P055)

内层命名空间中的名称隐藏了外层命名空间中的名称,但是,有时候即使使用类型的完全限定名也无法解决冲突。

(::) 用于限定命名空间别名。

【第03章】

(P057)

类是最常见的一种引用类型。

复杂的类可能包含一下内容 :

  1. 类属性 —— 类属性及类修饰符。非嵌套的类修饰符有 : public 、 internal 、 abstract 、 sealed 、 static 、 unsafe 、 partial ;

  2. 类名 —— 各种类型参数、唯一基类,多个接口;

  3. 花括号内 —— 类成员 (方法、成员属性、索引器、事件、字段、构造方法、运算符函数、嵌套类型和终止器) ;

字段是类或结构体中的变量。

以下修饰符可以用来修饰字段 :

[静态修饰符] —— static

[访问权限修饰符] —— public internal private protected

[继承修饰符] —— new

[不安全代码修饰符] —— unsafe

[只读修饰符] —— readonly

[跨线程访问修饰符] —— volatile

(P058)

“只读修饰符” 防止字段值在构造后被更改,只读字段只能在声明时或在其所属的类构造方法中被赋值。

字段不一定要初始化,没有被初始化的字段系统会赋一个默认值 ( 0 、 \0 、 null 、 false ) 。字段初始化语句在构造方法之前执行。

为了简便,可以用逗号分隔的列表声明一组同类型的字段,这是声明具有共同属性和修饰符的一组字段的简洁写法。

方法是用一组语句实现某个行为。方法能从调用语句的特定类型的传入参数中接收输入数据,并把输出数据以特定的返回值类型返回给调用语句。方法也可以返回 void 类型,表明这个方法不向调用方返回任何值。此外,方法还可以通过 ref / out 参数向调用方返回值。

方法签名在整个类中必须是唯一的,方法签名包括方法名、参数类型 (但不包括参数名及返回值类型) 。

方法可以用以下的修饰符 :

[静态修饰符] —— static

[访问权限修饰符] —— public internal private protected

[继承修饰符] —— new virtual abstract override sealed

[部分方法修饰符] —— partial

[非托管代码修饰符] —— unsafe extern

只要确保方法签名不同,可以在类中重载方法 (多个方法共用同一个方法名) 。

返回值类型和参数修饰符不属于方法签名的一部分。

参数是按值传递还是按引用传递,也是方法签名的一部分。

构造方法执行类或结构体的初始化代码,构造方法的定义和方法的定义类似,区别仅在于构造方法名和返回值只能和封装它的类相同。

(P059)

构造方法支持以下修饰符 :

[访问权限修饰符] —— public internal private protected

[非托管代码修饰符] —— unsafe extern

类或结构体可以重载构造方法,为了避免重复编码,一个构造方法可以用 this 关键字调用另一个构造方法。

(P060)

当一个构造方法调用另一个时,被调用的构造方法先执行。

C# 编译器自动为没有显式定义构造方法的类生成构造方法。但是,一旦显式定义了构造方法,系统将不再生成无参数构造方法。

对于结构体来说,无参数构造方法是结构体所固有的,因此,不能自己定义。结构体的隐式构造方法的作用是用默认值初始化每个字段。

字段初始化按声明的先后顺序,在构造方法之前执行。

构造方法不一定都是公有的。通常,定义非公有的构造方法的原因是为了在一个静态方法中控制类实例的创建。

静态方法可以用于从池中返回类对象,而不必创建一个新对象实例,或用来根据不同的输入属性返回不同的子类。

(P061)

为了简化类对象的初始化,可以在调用构造方法的语句中直接初始化对象的可访问字段或属性。

使用临时变量是为了确保在初始化过程中如果抛出异常,不会得到一个初始化未完成的对象。

对象初始化器是 C# 3.0 引入的新概念。

(P062)

如果想使程序在不同版本的程序集中保持二进制兼容,最好避免在公有方法中使用可选参数。

this 引用指的是引用类实例自身。

this 引用也用来避免类字段和局部变量或属性相混淆。

this 引用仅对类或结构体的非静态成员有效。

属性内部像方法一样包含逻辑。

属性和字段的声明很类似,但属性比字段多了一个 get / set 块。

(P063)

get 和 set 提供属性的访问器。

读取属性值时会运行 get 访问器,它必须返回属性类型的值。

给属性赋值时,运行 set 访问器,它有一个命名为 value 的隐含参数,类型和属性类型相同,值直接被指定给私有字段。

尽管访问属性和字段的方法相同,但不同之处在于,属性在获取和设置值时,给实现者提供了完全的控制能力。这种控制能力使得实现者可以选择所需的任何的内部通信机制,而无需将属性的内部细节暴露给用户。

在实际应用中,为了提高封装性,可能更多地在公有字段上应用公有属性。

属性可以用下面的修饰符 :

[静态修饰符] —— static

[访问权限修饰符] —— public internal private protected

[继承修饰符] —— new virtual abstract override sealed

[非托管代码修饰符] —— unsafe extern

如果只定义了 get 访问器,属性就是只读的;如果定义了 set 访问器,属性就是只写的,但很少用到只写属性。

通常属性会用一个简短的后台字段来存储其所代表的数据,但属性也可以从其他数据计算出来。

属性最常见的实现方法是 get 访问器和 set 访问器,对一个同类型的私有字段进行简单的读写操作。自动属性的声明表明由编译器提供上述实现方法。编译器会自动产生一个后台的私有字段,该字段名由编译器生成,且不能被引用。

如果希望属性对外暴露成只读属性, set 访问器可以标记为 private 的。

在 C# 3.0 中引入了自动属性。

get 和 set 访问器可以有不同的访问级别。

注意,属性本身被声明具有较高的访问权限,然后在需要较低级别的访问器上添加较低级别的访问权限修饰符。

C# 属性访问器在系统内部被编译成名为 get_XXX 和 set_XXX 的方法。

简单的非虚拟属性访问器被 JIT (即时) 编译器编译成内联的,消除了属性和字段访问方法的性能差别。内联是一种优化方法,它用方法的函数体替代方法调用。

通过 WinRT 的属性,编译器就可以假设是 put_XXX 命名转换,而不是 set_XXX 。

索引器为访问类或结构体中封装的列表或字典型数据元素提供了自然的访问接口。索引器和属性很相似,但索引器通过索引值而非属性名访问数据元素。

string 类具有索引器,可以通过 int 索引访问其中的每一个 char 值。

当索引是整型时,使用索引器的方法类似于使用数组。

索引器和属性具有相同的修饰符。

要编写一个索引器,首先定义一个名为 this 的属性,将参数定义放在一对方括号中。

(P065)

如果省略 set 访问器,索引器就变成只读的。

索引器在系统内部被编译成名为 get_Item 和 set_Item 的方法。

常量是值永远不会改变的字段。常量在编译时静态赋值,并且在使用时,编译器直接替换该值,类似于 C++ 中的宏。常量可以是内置的数据类型 : bool 、 char 、 string 或枚举类型。

常量用关键字 const 定义,并且必须以特定值初始化。

常量在使用时比静态只读字段有更多限制 : 不仅能使用的类型有限,而且初始化字段的语句含义也不同。常量和静态只读变量的不同之处还有,常量是在编译时赋值的。

(P066)

静态只读字段可以在每个应用中有不同的值。

静态只读字段的好处还有,当提供给其他程序集时,可以更新数值。

从另一角度看,将来可能发生变化的任意值都不受其定义约束,所以不应该表示为一个常量。

常量也可以在方法内声明。

常量可以使用以下修饰符 :

[访问权限修饰符] —— public internal private protected

[继承修饰符] —— new

静态构造方法是每个类执行一次,而不是每个类实例执行一次。一个类只能定义一个静态构造方法,并且必须没有参数,必须和类同名。

运行时在使用类之前自动调用静态构造方法,下面两种行为可以触发静态构造函数 :

  1. 实例化类;

  2. 访问类的静态成员;

静态构造方法只有两个修饰符 : unsafe 和 extern 。

如果静态构造方法抛出一个未处理异常,类在整个应用程序的生命周期内都是不可用的。

(P067)

静态字段在调用静态构造方法之前执行初始化。如果一个类没有静态构造方法,字段在类被使用前初始化或在运行时随机选一个更早的时间执行初始化 (这说明静态构造方法的存在可能使字段初始化比正常时间晚执行)。

静态字段按字段声明的先后顺序初始化。

类可以标记为 static ,表明它必须仅由静态成员组成,并且不能产生子类。

System.Console 和 System.Math 类就是静态类的最好示例。

终止器是只能在类中使用的方法,它在垃圾收集器回收没有被应用的对象前执行。

终止器的语法是类名加前缀 (~) 。

实际上,这是重载对象的 Finalize() 方法的 C# 语法。

(P068)

终止器允许使用以下修饰符 :

[非托管代码修饰符] —— unsafe

局部类允许一个类分开定义,典型的用法是分开在多个文件中。从其他源文件自动生成的类需要和自定义的方法交互时,通常使用 partial 类。

每个类必须由 partial 声明。

局部类的各组成部分不能有冲突的成员。

局部类完全由编译器处理,也就是说,各组成部分在编译时必须可用,并必须编译在同一个程序集中。

有两个方法为 partial 类定义基类 : 在每个部分定义同一个基类、仅在其中一部分定义基类。

每个部分都可以独立定义并实现接口。

局部类可以包含局部方法,这些方法使自动生成的局部类可以为自定义方法提供自定义钩子 (hook) 。

(P069)

局部方法由两部分组成 : 定义和实现。定义一般由代码生成器产生,而实现多为手工编写。

如果没有提供方法的实现,方法的定义会被编译器清除。这使得自动代码生成可以自由提供钩子 (hook) ,而不用担心代码过于臃肿。

局部方法必须是 void 型,并且默认是 private 的。

局部方法在 C# 3.0 中引入。

为了扩展或自定义原类,类可以继承另一个类。继承类让你可以重用另一个类的方法,而无需重新构建。

一个类只能继承自唯一的类,但可以被多个类继承,从而形成类的层次。

子类也被称为派生类;基类也被称为超类。

(P070)

引用是多态的,意味着 X 类型的变量可以指向 X 子类的对象。

多态性之所以能实现,是因为子类具有基类的全部特征。反过来,则不正确。

对象引用可以被 :

  1. 隐式向上转换成基类的引用;

  2. 显式向下转换为子类的引用;

在可兼容的类型引用之间向上类型转换或向下类型转换即为引用转换 : 生成一个新的引用指向同一个对象。向上转换总是能成功,而向下转换只有在对象的类型符合要求时才能成功。

向上类型转换创建一个基类指向子类的引用。

向上转换以后,被引用的对象本身不会被替换或改变。

(P071)

向下类型转换创建一个子类指向基类的引用。

对于向上转换,只影响了引用,被引用的对象没有变化。

向下转换必须是显式转换,因为它可能导致运行时错误。

如果向下转换出错,会抛出 InvalidCastException 。

as 运算符在向下类型转换出错时为变量赋值 null (而不是抛出异常) 。

这个操作相当有用,接下来只需判断结果是否为 null 。

如果不用判断结果是否为 null ,使用 cast 更好,因为如果发生错误,cast 会抛出描述更清楚的异常。

as 运算符不能用来实现自定义转换,也不能用于数值型转换。

as 和 cast 运算符也可以用来实现向上类型转换,但不常用,因为隐式转换就可以实现。

is 运算符用于检查引用的转换能否成功,换句话说,它是检查一个对象是否是从某个特定类派生 (或是实现某个接口),经常在向下类型转换前使用。

(P072)

is 运算符不能用于自定义类型转换和数值型类型转换,但它可以用于拆箱机制的类型转换。

标识为 virtual 的函数可以被提供特定实现的子类重载。

方法、属性、索引器和事件都可以被声明为 virtual 。

子类通过 override 修饰符重载虚方法。

虚方法和重载方法的标识、返回值以及访问权限必须完全一致。

重载方法可以通过 base 关键字调用其基类的实现。

从构造方法调用虚方法可能很危险,因为编写子类的人在重写方法时不可能知道正在操作一个未完全实例化的对象。换而言之,重写方法最终会访问到一些依赖于未被构造方法初始化的域的方法或属性。

被声明为 abstract 的抽象类不能被实例化,只有抽象类的具体实现子类才能被实例化。

抽象类中可以定义抽象成员,抽象成员和虚成员相似,但抽象成员不提供默认的实现。实现必须由子类提供,除非子类也被声明为抽象类。

(P073)

基类和子类可能定义相同的成员。

有时需要故意隐藏一个成员,这种情况下,可以在子类中使用 new 修饰符。

new 修饰符的作用仅为防止编译器发出警告。

修饰符 new 把你的意图传达给编译器以及其他编程人员,即重复的成员不是无意的。

C# 在不同的上下文环境中使用 new 关键字表达完全不同的含义,特别要注意 new 运算符和 new 成员修饰符的不同。

(P074)

重载的方法成员可用 sealed 关键字密封它的实现,以防止该方法被它的更深层次的子类再次重载。

可以在类中使用 sealed 修饰符来密封整个类,含义是密封类中所有的虚方法。

密封类比密封方法成员更常见。

关键字 base 和关键字 this 很类似,它有两个重要目的 :

  1. 从子类访问重载的基类方法成员;

  2. 调用基类的构造方法;

(P075)

子类必须声明自己的构造方法。

子类必须重新定义它想对外公开的任何构造方法。不过,定义子类的构造方法,也可以通过使用关键字 base 调用基类的某个构造方法实现。

关键字 base 和 this 用法类似,但 base 关键字调用的是基类中的构造方法。

基类的构造方法总是先执行,这保证了 base 的初始化发生在作为子类的特例初始化之前。

如果子类中的构造方法省略 base 关键字,那么基类的无参构造方法将被隐式调用。

如果基类没有无参数的构造方法,子类的构造方法中就必须使用 base 关键字。

当对象被实例化时,初始化按以下顺序进行 :

(1) 从子类到基类 : a. 初始化字段 b. 指定被调用基类的构造方法中的变量;

(2) 从基类到子类 : a. 构造方法体执行;

(P076)

继承对方法的重载有特殊的影响。

当重载被调用时,类型最明确的优先匹配。

具体调用哪个重载是静态决定的 (编译时) 而不是在运行时决定。

object 类 (System.Object) 是所有类型的最终基类。

任何类型都可以向上转换成 object 类型。

(P077)

栈是一种遵循 LIFO (Last-In First-Out,后进先出法) 的数据结构。

栈有两种操作 : push 表示一个元素进栈和 pop 表示一个元素出栈。

承载了类的优点,object 是引用类型。

当数值类型和 object 类型之间相互转换时,公共语言运行时 (CLR) 必须作一些特定的工作,实现数值类型和引用类型的转换这个过程被称为装箱和拆箱。

装箱是将数值类型实例转换成引用类型实例的行为。

引用类型可以是 object 类或接口。

拆箱需要显式进行。

运行时检查提供的值类型是否与真正的对象类型相匹配,并在检查出错误时,抛出 InvalidCastException 。

(P078)

装箱是把数值类型的实例复制到新对象中,而拆箱是把对象的内容复制回数值类型的实例中。

C# 在静态 (编译时) 和运行时都会进行类型检查。

静态类型检查使编译器能在程序没有运行的情况下检查正确性。

在引用或拆箱操作的向下类型转换时,由 CLR 执行运行时类型检查。

可以进行运行时类型检查,是因为堆栈中的每个对象都在内部存储了类型标识,这个标识可以通过调用 object 类的 GetType() 方法读取。

所有 C# 的类型在运行时都会维护 System.Type 类的实例。有两个基本方法可以获得 System.Type 对象 :

  1. 在类实例上调用 GetType 方法;

  2. 在类名上使用 typeof 运算符;

GetType 在运行时赋值;typeof 在编译时静态赋值 (如果使用泛型类型,那么它将由即使编译器解析)。

(P079)

System.Type 有针对类型名、程序集、基类等的属性。

同时 System.Type 还有作为运行时反射模式的访问器。

ToString 方法返回类实例的默认文本表述。这个方法被所有内置类型重载。

如果不重写 ToString ,那么这个方法会返回类型名称。

当直接在数值型对象上调用像 ToString 这样的重载的 object 成员时,不会发生装箱。只有进行类型转换时,才会执行装箱操作。

(P080)

结构体和类相似,不同之处在于 :

  1. 结构体是值类型,而类是引用类型;

  2. 结构体不支持继承 (除了隐式派生自 object 类的,更精确些说,是派生自 System.ValueType) 。

除了以下三项内容,结构体可以包含类的所有成员 :

  1. 无参数的构造方法;

  2. 终止器;

  3. 虚成员;

当表示值类型时使用结构体更理想而不用类。

结构体是值类型,每个实例不需要在堆栈上实例化。

结构体的构造语义如下 :

  1. 隐含存在一个无法重载的无参数构造方法,将字段按位置零;

  2. 定义结构体的构造方法时,必须显式指定每个字段;

  3. 不能在结构体内初始化字段;

(P081)

为了提高封装性,类或类成员会在声明中添加五个访问权限修饰符之一,来限制其他类和其他程序集对它的访问权限 :

[public] —— 完全访问权限;“枚举类型成员” 或 “接口” 隐含的访问权限;

[internal] —— 仅可访问程序集和友元程序集;“非嵌套类型” 的默认访问权限;

[private] —— 仅在包含类型可见;类和结构体 “成员” 的默认访问权限;

[protected] —— 仅在包含类型和子类中可见;

[protected internal] —— protected 和 internal 的访问权限并集 Eric Lippert 是这样解释的 : 默认情况下尽可能将所有成员定义为私有,然后每一个修饰符都会提高其访问级别。所以用 protected internal 修饰的成员在两个方面的访问级别都提高了。

CLR 有对 protected 和 internal 访问权限交集的定义,但 C# 并不支持。

(P082)

在高级语义应用中,加上 System.Runtime.CompilerServices.InternalsVisibleTo 属性,就可以把 internal 成员提供给其他的友元程序集。

类权限是它内部声明的成员访问权限的封顶,关于权限封顶最常用的示例是 internal 类中的 public 成员。

当重载基类的函数时,重载函数的访问权限必须一致。

(P083)

编译器会阻止使用任何不一致的访问权限修饰符。

子类可以比基类访问权限低,但不能比基类访问权限高。

接口和类相似,但接口只为成员提供定义而不提供实现。

接口和类的不同之处有 :

  1. 接口的成员都是隐含抽象的。相反,类可以包含抽象成员和有具体实现的成员;

  2. 一个类 (或结构体) 可以实现多个接口。相反,类只能继承一个类,而结构体完全不支持继承 (只能从 System.ValueType 派生)。

接口声明和类声明很类似,但接口不提供其成员的实现,因为它的所有成员都是隐式定义为抽象的,这些成员将由实现接口的类或结构体实现。

接口只能包含方法、属性、事件、索引器,这些正是类中可以定义为抽象的成员。

接口成员总是隐式地定义成 public 的,并且不能用访问修饰符声明。

实现接口意味着为其所有成员提供 public 的实现。

可以把对象隐式转换为它实现的任意一个接口。

(P084)

接口可以从其他接口派生。

当实现多个接口时,有时成员标识符会有冲突。显式实现接口成员可以解决冲突。

调用显式实现成员的唯一方法是先转换为相应的接口。

(P085)

另一个使用显式实现接口成员的原因是,隐藏那些和类的正常用法差异很大或有严重干扰的成员。

默认情况下,接口成员的实现是隐式定义为 sealed 。为了能重载,必须在基类中标识为 virtual 或者 abstract 。

显式实现的接口成员不能标识为 virtual 的,也不能实现通常意义的重载。但是它可以被重新实现。

子类可以重新实现基类中已经被实现的任意一个接口。不管基类中该成员是不是 virtual 的,当通过接口调用时,重新实现都能够屏蔽成员的实现。它不管接口成员是隐式还是显式实现都有效,但后者效果更好。

(P086)

重新实现屏蔽仅当通过接口调用成员时有效,从基类调用时无效。

(P087)

将结构体转换成接口会引发装箱机制。调用结构体的隐式实现接口成员不会引发装箱。

枚举类型是一种特殊的数值类型,可以在枚举类型中定义一组命名的数值常量。

(P088)

每个枚举成员都对应一个整数型,默认情况下 :

  1. 对应的数值是 int 型的;

  2. 按枚举成员的声明顺序,自动指定的常量为 0 、 1 、 2 ······ ;

可以指定其他的整数类型代替默认类型。

也可以显式指定每个枚举成员对应的值。

编译器还支持显式指定部分枚举成员,没有指定的枚举成员,在最后一个显式指定的值的基础上递增。

枚举类型的实例可以和它对应的整型值互相显式转换。

也可以显式地将一个枚举类型转换成另一个。

两个枚举类型之间的转换通过对应的数值进行。

在枚举表达式中,编译器对数值 0 进行特别处理,不需要显式转换。

(P089)

对 0 进行特别管理原因有两个 :

  1. 第一个枚举成员经常被用作 “默认” 值;

  2. 在合并枚举类型中,0 表示不标识类型;

枚举类型成员可以合并。为了避免混淆,合并枚举类型的成员要显式指定值,典型的增量为 2 。

使用位运算符操作合并枚举类型的值,例如 | 和 & ,它们作用在对应的整型数值上。

依照惯例,当枚举类型元素被合并时,一定要应用 Flags 属性。

如果声明了一个没有标注 Flags 属性的枚举类型,枚举类型的成员仍然可以合并,但是当在该枚举实例上调用 ToString 方法时,输出一个数值而非一组名字。

一般来说,合并枚举类型通常用复数名而不用单数名。

位运算符、算数运算符和比较运算符都返回对应整型值的运算结果。

枚举类型和整型之间可以做加法,但两个枚举类型之间不能做加法。

因为枚举类型可以和它对应的整型值相互转换,枚举的真实值可能超出枚举类型成员的数值范围。

位操作和算数操作也会产生非法值。

(P090)

检查枚举值的合法性,静态方法 Enum.IsDefined 有此功能。

Enum.IsDefined 对标识枚举类型不起作用。

(P091)

嵌套类型是声明在另一个类型内部的类型。

嵌套类型有如下特征 :

  1. 可以访问包含它的外层类中的私有成员、以及外层类所能访问的所有内容;

  2. 可以使用所有的访问权限修饰符,而不仅限于 public 和 internal ;

  3. 嵌套类型的默认访问权限是 private 而不是 internal ;

  4. 从外层类以外访问嵌套类型,需要用外层类名称限定 (就像访问静态成员一样);

所有类型都可以被嵌套,但只有类和结构体才能嵌套其他类型。

(P092)

嵌套类型在编译器中的应用也很普遍,如编译器用于生成捕获迭代和匿名方法结构状态的私有类。

如果使用嵌套类型的主要原因是避免一个命名空间中类型定义杂乱无章,那么可以考虑使用嵌套命名空间。使用嵌套类型的原因,应该是利用它较强的访问控制能力,或者是因为嵌套类型必须访问其外层类的私有成员。

C# 对书写能跨类型复用的代码,有两个不同的支持机制 : 继承和泛化。但继承的复用性来自基类,而泛化的复用性是通过带有 “占位符” 类的 “模板” 。和继承相比,泛化能提高类型的安全性以及减少类型的转换和装箱。

C# 的泛化和 C++ 的模板是相似概念,但它们的工作方法不同。

泛型中声明类型参数 —— 占位符类型,由泛型的使用者填充,它支持类型变量。

(P093)

在运行时,所有泛型的实例都是关闭的 —— 占位符类型填充。

只有在类或方法内部,T 才可以被定义为类型参数。

泛化是为了代码能跨类型复用而设计的。

泛化方法指在方法的标识符内声明类参数。

(P094)

通常不需要提供参数的类型给泛化方法,因为编译器可以在后台推断出类型。

在泛型中,只有新引入类型参数的方法才被归为泛化方法 (用尖括号标出) 。

唯有方法和类可以引入类型参数。属性、索引器、事件、字段、构造方法、运算符都不能声明类型参数,虽然它们可以参与使用所在的类中已经声明的类型参数。

构造方法可以参与使用已存在的类型参数,但不能引入新的类型参数。

可以在声明类、结构体、接口、委托和方法时引入类型参数。其他的结构 (如属性) 不能引入类型参数,但可以使用类型参数。

泛型类或泛型方法可以有多个参数。

(P095)

泛型类名和泛型方法名可以被重载,只要类型参数的数量不同即可。

习惯上,泛型类和泛型方法如果只有一个类型参数,只要参数的含义明确,一般把这个类型参数命名为 T 。当使用多个类型参数时,每个类型参数都使用 T 作为前缀,后面跟一个更具描述性的名称。

在运行时不存在开放的泛型 : 开放泛型被汇编成程序的一部分而关闭。但运行时可能存在无绑定 (unbound) 泛型,只用作类对象。C# 中唯一指定无绑定泛型的方法是使用 typeof 运算符。

开放泛型类型一般与反射 API 一起使用。

可以用 default 关键字获取赋给泛型类参数的默认值。引用类型的默认值是 null ,数值类型的默认值是将类的所有字段位置 0 。

默认情况下,类型参数可以被任何类型替换。在类型参数上应用约束,可以定义类型参数为指定类型。

where T : base-class // 基类约束
where T : interface // 接口约束
where T : class // 引用类型约束
where T : struct // 数值类型约束 (排除可空类型)
where T : new() // 无参数构造方法约束
where U : T // 裸类型约束

(P096)

约束可以应用在方法和类的任何类型参数的定义中。

“基类约束” 或 “接口约束” 规定类型参数必须是某个类的子类或实现特定类或接口。这允许参数类可以被隐式转换成特定类或接口。

“类约束” 和 “结构体约束” 规定 T 必须是引用类型或数值类型 (不能为空)。

“无参数构造方法约束” 要求 T 有一个公有的无参数构造方法。如果定义了这个约束,就可以在 T 中调用 new() 。

“裸类型约束” 要求一个类型参数从另一个类型参数派生。

(P097)

泛型类和非泛型的类一样,都可以作为子类。子类可以让基类中的类型参数保持开放。

子类也可以用具体类型关闭泛型参数。

子类还可以引入新的类型变量。

技术上,子类型中所有类型参数都是新的 : 可以说子类型关闭后又重新开放了基类的基类参数。这表明子类可以为其重新打开的类型参数使用更有意义的新名称。

当关闭类型参数时,类可以用自己作为实体类。

对每个封装的类来说,静态数据是全局唯一的。

(P098)

C# 的类型转换运算符可以进行多种转换,包括 :

  1. 数值型转换;

  2. 引用型转换;

  3. 装箱 / 拆箱 转换;

  4. 自定义转换 (通过运算符重载) ;

根据原数据的类型,在编译时决定转换成何种类型,并实现转换。因为编译时还不知道原数据的确切类型,使得泛型参数具有有趣的语义。

(P099)

假定 S 是 B 的子类,如果 X 允许引用转换成 X ,那么称 X 为协变类。

由于 C# 符号的共变性 (和逆变性) ,所以 “可改变” 表示可以通过隐式引用转换进行改变 —— 如 A 是 B 的子类,或者 A 实现 B。数字转换、装箱转换和自定义转换都不包含在内。

C# 4.0 中,泛化接口支持协变 (泛化委托也支持) ,但泛化类不支持。数组也支持协变 (如 S 是 B 的子类,S[] 可以转换成 B[]) 。

为了保证静态类的安全性,泛化类不是协变的。

(P100)

由于历史原因,数组 array 类型具有协变性。

在 C# 4.0 中,泛化接口对用 out 修饰符标注的类型参数支持协变。和数组不同,out 修饰符保证了协变性的接口是完全类型安全的。

T 前的 out 修饰符是 C# 4.0 的新特性,表明 T 只用在输出的位置。

接口中的协变和逆变的典型应用是使用接口 : 很少需要向协变性接口写入。确切地说,由于 CLR 的限制,为了协变性将方法参数标注为 out 是不合法的。

(P101)

不管泛型还是数组,协变 (逆变) 仅对引用转换的元素有效而对装箱转换无效。

泛化接口支持逆变当泛型参数只出现在输入的位置,且被指定了 in 修饰符时。

【第04章】

(P103)

委托将方法调用者和目标方法动态关联起来。

代理类型定义了代理实例可调用的方法。

(P104)

委托实例实际上是调用者的代表 : 调用者先调用委托,然后委托调用目标方法。这种间接调用方式可以将调用者和目标方法分开。

调用委托和调用方法类似 (因为委托的目的仅仅是提供一定程序的间接性) 。

委托和回调相似,是捕获 C 函数指针等结构体的一般方法。

委托变量动态指定调用的方法。这个特性对于编写插入式方法非常有用。

(P105)

所有的委托实例都有多播能力。意思是一个委托实例不仅可以引用一个目标方法,而且可以引用一组目标方法。用运算符 + 和 += 联合多个委托实例。

委托按照添加的顺序依次被触发。

运算符 - 和 -= 从左边的委托操作数中移除右边的委托操作数。

可以在委托变量上 + 或 += null 值,等价于为变量指定一个新值。

同样,在只有唯一目标方法的委托上调用 -= 等价于为该变量指定 null 值。

委托是不可变的,因此调用 += 或 -= 的实质是创建一个新的委托实例,并把它赋值给已有变量。

如果多播委托有非 void 的返回类型,调用者从最后一个触发的方法接收返回值。前面的方法仍然被调用,但返回值都被丢弃了。大部分情况下调用的多播委托都返回 void 类型,所以这个细小的差别就没有了。

所有委托类型都是从 System.MulticastDelegate 派生的,System.MulticastDelegate 继承自 System.Delegate。C# 将委托中使用的 + 、 - 、 += 和 -= 都编译成 System.Delegate 的静态 Combine 和 Remove 方法。

(P106)

当委托对象指向一个实例方法时,委托对象不仅需维护到方法的引用,而且需维护到方法所属类实例的引用。 System.Delegate 类的 Target 属性表示这个类实例 (当委托引用静态方法时为 null) 。

(P107)

委托类可以包含泛型参数。

public delegate T Transformer(T arg);

有了泛化委托,我们就可以写非常泛化的小型委托类,它们可以为具有任意返回类型和任意多参数的方法服务。

(P108)

在 Framework 2.0 之前,并不存在 Func 和 Action 代理 (因为那时还不存在泛型)。由于有这个历史问题,所以 Framework 的许多代码都使用自定义代理类型,而不使用 Func 和 Action 。

能用委托解决的问题,都可以用接口解决。

在下面的情形中,委托可能是比接口更好的选择 :

  1. 接口内只定义一个方法;

  2. 需要多播能力;

  3. 订阅者需要多次实现接口;

(P109)

即使签名相似,委托类也互不兼容。

如果委托实例指向相同的目标方法,则认为它们是等价的。

如果多播委托按照相同的顺序引用相同的方法,则认为它们是等价的。

当调用一个方法时,可以给方法的参数提供大于其指定类型的变量,这是正常的多态行为。基于同样的原因,委托也可以有大于它目标方法参数类型的参数,这称为逆变。

(P110)

标准事件模式的设计宗旨是在其使用公共基类 EventArgs 时应用逆变。

如果调用一个方法,得到的返回值类型可能大于请求的类型,这是正常的多态性行为。基于同样的原因,委托的返回类型可以小于它的目标方法的返回值类型,这被称为协变。

如果要定义一个泛化委托类型,最好按照如下准则 :

  1. 将只用在返回值的类型参数标注为协变 (out) ;

  2. 将只用在参数的类型参数标注为逆变 (in) ;

(P111)

当使用委托时,一般会出现两种角色 : 广播者和订阅者。

广播者是包含委托字段的类,它决定何时调用委托广播。

订阅者是方法目标的接收者,通过在广播者的委托上调用 += 和 -= ,决定何时开始和结束监听。一个订阅者不知道也不干涉其他的订阅者。

事件是使这一模式正式化的语言形态。事件是只显示委托中 广播 / 订阅 需要的子特性的结构。使用事件的主要目的在于 : 保护订阅互不影响。

声明事件最简单的方法是,在委托成员的前面加上 event 关键字。

(P113)

.NET 框架为事件定义了一个标准模式,它的目的是保持框架和用户代码之间的一致性。

标准事件模式的核心是 System.EventArgs —— 预定义的没有成员的框架类 (不同于静态 Empty 属性) 。

EventArgs 是用于为事件传递信息的基类。

考虑到复用性,EventArgs 子类根据它包含的内容命名 (而非根据将被使用的事件命名),它一般以属性或只读字段将数据。

定义了 EventArgs 的子类,下一步是选择或定义事件的委托,需遵循三条原则 :

  1. 委托必须以 void 作为返回值;

  2. 委托必须接受两个参数 : 第一个是 object 类,第二个是 EventArgs 的子类。第一个参数表明事件的广播者,第二个参数包含需要传递的额外信息。

  3. 委托的名称必须以 EventHandler 结尾。

框架定义一个名为 System.EventHandler<>的泛化委托,该委托满足如下条件 :

public delegate void EventHandler (object source, TEventArgs e) where TEventArgs : EventArgs

(P114)

最后,该模式要求写一个受保护的 (protected) 虚方法引发事件。方法名必须和事件名一致,以 On 作前缀,并接受唯一的 EventArgs 参数。

(P115)

如果事件不传递额外的信息,可以使用预定义的非泛化委托 EventHandler 。

(P116)

事件访问器是对 += 和 -= 功能的实现。默认情况下,访问器由编译器隐式实现。

编译器把它转换为 :

  1. 一个私有的委托字段;

  2. 一对公有的事件访问器函数,它们实现私有委托字段的 += 、 -= 运算;

通过自定义事件访问器,指示 C# 不要产生默认的字段和访问器逻辑。

显式定义的事件访问器,可以在委托的存储和访问上进行更复杂的操作。有以下三种常用情形 :

  1. 当事件访问器仅为广播该事件的另一个类作交接;

  2. 当类定义了大量事件,而大部分时间有很少订阅者。这种情况下,最好在字典中存储订阅者的委托实例,因为字典比大量的空委托字段的引用需要更少的存储开销;

  3. 当显式实现声明事件的接口时;

事件的 add 和 remove 部分被编译成 add_XXX 和 remove_XXX 方法。

和方法相似,事件可以是虚拟的 (virtual) 、重载的 (overriden) 、抽象的 (abstract) 或密封的 (sealed) 。事件还可以是静态的 (static)。

(P117)

Lambda 表达式是写在委托实例上的匿名方法。

编译器立即将 Lambda 表达式转换成下面两种情形其中的一种 :

  1. 委托实例;

  2. Expression 类型的表达式树,该表达式树将 Lambda 表达式内的代码显示为可遍历的对象模式,这使得对 Lambda 表达式的解释可以延迟到运行时。

编译器在内部将这种 Lambda 表达式编译成一个私有方法,并把表达式代码移到该方法中。

Lambda 表达式有以下形式 : (参数) => 表达式或语句块。

为了方便,在只有一个可推测类型的参数时,可以省略小括号。

Lambda 表达式使每个参数和委托的参数一致,表达式的参数 (可以为 void) 和委托的返回值类型一致。

Lambda 表达式代码除了可以是表达式还可以是语句块。

Lambda 表达式通常和 Func 或 Action 委托一起使用,因此可以将前面的表达式写成下面的形式。

(P118)

Lambda 表达式是 C# 3.0 中引入的概念。

编译器通常可以根据上下文推断出 Lambda 参数的类型,但当不能推断时,必须明确指定每个参数的类型。

Lambda 表达式可以引用方法内的内部变量和参数 (外部变量) 。

Lambda 表达式引用的外部变量称为捕获变量。捕获变量的表达式称为一个闭包。

捕获的变量在真正调用委托时被赋值,而不是在捕获时赋值。

Lambda 表达式可以自动更新捕获变量。

捕获变量的生命周期可以延伸到和委托的生命周期相同。

(P119)

在 Lambda 表达式内实例化的局部变量,在每次调用委托实例期间是唯一的。

在内部捕获是通过把被捕获的变量 “提升” 到私有类的字段实现的。当方法被调用时,实例化该类,并将其生命周期绑定在委托的实例上。

当捕获 for 或 foreach 语句中的循环变量时,C# 把这些循环变量看做是声明在循环外部的。这表明每个循环捕获的是相同的变量。

(P120)

匿名方法是 C# 2.0 引入的特性,并通过 C# 3.0 的 Lambda 表达式得到大大扩展。

匿名方法类似于 Lambda 表达式,但没有下面的特性 :

  1. 确定类型的参数;

  2. 表达式语法 (匿名方法必须是语句块) ;

  3. 在指定到 Expression 时,编译成表达式树的功能;

写匿名方法的方法是 : delegate 关键字后面跟参数声明 (可选) ,然后是方法体。

(P121)

完全省略参数声明是匿名方法独有的特性 —— 即使委托需要这些参数声明。

匿名方法和 Lambda 表达式使用同样的方法捕获外部变量。

try 语句是为了处理错误或清理代码而定义的语句块。try 块后面必须跟有 catch 块或 finally 块或两个块都有。

当 try 块执行发生错误时,执行 catch 块;当结束 try 块时 (如果当前是 catch 块,则当结束 catch 块时),不管有没有发生错误,都执行 finally 块来清理代码。

catch 块可以访问 Exception 对象,该对象包含错误信息。catch 中可以弥补错误也可以再次抛出异常。当仅仅是记录错误或要抛出更高层次的错误时,我们选择再次抛出异常。

finally 块在程序中起决定作用,因为任何情况下它都被执行,通常用于清除任务。

(P122)

异常处理需要几百个时钟周期,代价相对较高。

当抛出异常时,公共语言运行时 CLR 询问 : 当前是否在能捕获异常的 try 语句块中运行 ?

  1. 如果是,执行转到相应的 catch 块,如果 catch 块成功地运行结束,执行转到 try 下面的语句 (如果存在,finally 块优先执行) ;

  2. 如果否,执行跳转到调用函数,重复上述询问 (在执行 finally 块之后) ;

如果没有用于处理异常的函数,用户将看到一个错误提示框,并且程序终止。

catch 子句定义捕获哪些类型的异常,这些异常应该是 System.Exception 或 System.Exception 的子类。

捕获 System.Exception 表示捕获所有可能的异常,用于以下情况 :

  1. 不管哪种特定类型的异常,程序都可以修复;

  2. 希望重新抛出该异常 (可以在记入日志后);

  3. 程序终止前的最后一个错误处理;

(P123)

更常见的做法是,为了避免处理程序没有被定义的情况,只捕获特定类型的异常。

可以在多个 catch 子句中处理各种异常类型。

对于每一种给定的异常,只有一个 catch 子句执行。如果想要建立捕获更普遍的异常的安全网,必须把处理特定异常的语句放在前面。

如果不需要使用变量值,不指定变量也可以捕获异常。

甚至,变量和类型可以都省略,表示指捕获所有异常。

除 C# 外的其他语言中,可以抛出不是派生自 Exception 类的对象 (但不推荐) 。 CLR 自动把此对象封装在 RuntimeWrappedException 类中 (该类派生自 Exception) 。

无论是否抛出异常,也不管 try 程序块是否完全执行,finally 程序块总是被执行。通常用 finally 程序块来清除代码。

在以下情况下执行 finally 程序块 :

  1. catch 块执行完成;

  2. 由于跳转语句 (如 return 或 goto) 离开 try 块;

  3. try 块结束;

(P124)

finally 块为程序添加了决定性内容,在下面实例中,无论是否符合以下条件,打开的文件总能被关闭 :

  1. try 块正常结束;

  2. 因为是空文件,提前返回 EndOfStream ;

  3. 读取文件时抛出 IOException 异常;

在 finally 块中调用对象的 Dispose 方法是贯穿 .NET 框架的标准约定,且在 C# 的 using 语句中也明确支持。

许多类内部封装了非托管资源,例如文件管理、图像管理、数据库连接等。这些类实现 System.IDisposable 接口,这个接口定义了一个名为 Dispose 的无参数方法,用于清除这些非托管资源。

using 语句提供了一种在 finally 块中调用 IDisposable 接口对象的 Dispose 方法的优雅方法。

(P125)

可以在运行时或用户代码中抛出异常。

可以捕获异常后再重新抛出。

如果将 throw 替换为 throw ex,那么这个例子仍然有效,但是新产生异常的 StackTrace 属性不再反映原始的错误。

(P126)

重新抛出异常不会影响异常的 StackTrace 属性,当重新抛出一个不同类型的异常时,可以设置 InnerException 属性为原始的异常,这样有利于调试。几乎所有类型的异常都可以实现这一目的。

System.Exception 类的最重要的属性有下面几个 :

  1. StackTrace —— 表示从异常的起源到 catch 块的所有方法的字符串;

  2. Message —— 描述异常的字符串;

  3. InnerException —— 导致外部异常的内部异常 (如果有的话) ,它本身还可能有另一个 InnerException ;

所有的 C# 异常都是运行时异常,没有和 Java 对等的编译时检查异常。

下面的异常类型在 CLR 和 .NET 框架中广泛使用,可以在程序中自主抛出这些异常或者将它们作为基类来派生自定义异常类 :

  1. System.ArgumentException —— 当使用不恰当的参数调用函数时抛出,这通常表明程序有 bug ;

  2. System.ArgumentNullException —— ArgumentException 的子类,当函数参数为 null (意料外的) 时抛出;

  3. System.ArgumentOutOfRangeException —— ArgumentException 的子类,当属性值太大或太小时抛出 (通常是数值型) ;

  4. System.InvalidOperationException —— 不管是哪种特定的属性值,当对象的状态不符合方法正确执行的要求时抛出;

  5. System.NotSupportedException —— 该异常抛出表示不支持特定功能;

  6. System.NotImplementedException —— 该异常抛出表明某个方法还没有具体实现;

  7. System.ObjectDisposedException —— 当函数调用的对象已被释放时抛出;

另一个常见的异常类型是 NullReferenceException 。当一个对象的值为 null 并访问它的成员时,CLR 就会抛出这个异常 (表示代码有 bug) 。

当方法出错时,可以选择返回某种类型的错误代码或抛出异常。一般情况下,如果错误发生在正常的工作流之外或者希望方法的直接调用者不进行错误处理时,抛出异常。但有些情况下最好给调用者提供两种选择。

如果类型解析失败,Parse 方法抛出异常,TryParse 方法返回 false 。

(P128)

Enumerator 是只读的,且游标只能在顺序值上向前移,实现下面对象之一 :

  1. System.Collections.IEnumerator ;

  2. System.Collections.Generic.IEnumerator

从技术上讲,任何具有 MoveNext 方法和 Current 属性的对象,都被看作是 enumerator 类型的。

foreach 语句用来在可枚举的对象上执行迭代操作。可枚举对象是顺序表的逻辑表示,它本身不是一个游标,但对象自身产生游标。

可枚举对象可以是 :

  1. IEnumerable 或 IEnumerable 的实现;

  2. 具有名为 GetEnumerator 的方法返回一个 enumerator ;

IEnumerator 和 IEnumerable 在 System.Collections 命名空间中定义。

IEnumerator 和 IEnumerable 在 System.Collection.Generic 命名空间中定义。

如果 enumerator 实现了 IDisposable ,那么 foreach 语句也起到 using 语句的作用。

(P129)

可以通过一个简单的步骤实例化和填充可枚举的对象,它要求可枚举对象实现 System.Collections.IEnumerable 接口,并且有可调用的带适当个数参数的 Add 方法。

和 foreach 语句是枚举对象的使用者相对,迭代器是枚举对象的生产者。

(P130)

return 语句表示该方法返回的值,而 yield return 语句表示从本枚举器产生的下一个元素。

迭代器是包含一个或多个 yield 语句的方法、属性或索引器,迭代器必须返回以下四个接口之一 (否则,编译器会报错) :

// Enumerable 接口
System.Collections.IEnumerable
System.Collections.Generic.IEnumerable

// Enumerator 接口
System.Collections.IEnumerator
System.Collections.Generic.IEnumerator

返回 enumerable 接口和返回 enumerator 接口的迭代器具有不同的语义。

yield break 语句表明迭代器不返回后面的元素而是提前结束。

(P131)

迭代器块中使用 return 语句是不合法的,必须使用 yield break 语句来代替。

yield return 语句不能出现在带 catch 子句的 try 语句块中。

yield return 语句也不能出现在 catch 或 finally 语句块中。出现这些限制的原因是编译器必须将迭代器转换为带有 MoveNext 、 Current 和 Dispose 成员的普通类,而且转换异常处理语句块可能会大大增加代码复杂性。

但是,可以在只带 finally 语句块的 try 块中使用 yield 语句。

迭代器具有高度可组合性。

(P132)

迭代器模式的组合性在 LINQ 中是非常有用的。

引用类型可以表示一个不存在的值,即空引用。

(P133)

若要在数值类型中表示空值,必须使用特殊的结构即可空类型 (Nullable)。可空类型是由数据类型后加一个 “?” 表示的。

T? 转换成 System.Nullable 。而 Nullable 是一个轻量的不变结构,它只有两个域,分别是 Value 和 HasValue 。System.Nullable 实质上是很简单的。

public struct Nullable where T : struct
{
public T Value {get;}
public bool HasValue {get;}
public T GetValueOrDefault();
public T GetValueOrDefault(T defaultValue);
}

当 HasValue 为假时尝试获取 Value,程序会抛出一个 InvalidOperationException 异常。

当 HasValue 为真时,GetValueOrDefault() 会返回 Value ,否则返回 new T() 或者一个特定的自定义默认值。

T? 的默认值是 null 。

从 T 到 T? 的转换是隐式的,而从 T? 到 T 的转换则必须是显式的。

显式强制转换与直接调用可空对象的 Value 属性实际上是等价的。因此,当 HasValue 为假时,程序会抛出一个 InvalidOperationException 异常。

如果 T? 是装箱的,那么堆中的装箱值包含的是 T ,而不是 T? 。这种优化方式是可以实现的,因为装箱值是一个可能已经赋值为空的引用类型。

(P134)

C# 允许通过 as 运算符对一个可空类型进行拆箱。如果强制转换出错,那么结果为 null 。

Nullable 结构体并没有定义诸如 < 、 > 或者 == 的运算符。尽管如此,下面的代码仍然能够正常编译和执行。

运算符提升表示可以隐式地使用 T 的运算符来处理 T? 。

编译器会基于运算符类型来执行空值逻辑。

提升 “等于运算符” 处理空值的方式与引用类型相似,这意味着两个空值是相等的。而且 :

  1. 如果只有一个操作数为空,那么结果不相等;

  2. 如果两个操作数都不为空,那么比较它们的 Value ;

(P135)

关系运算符的运算原则表明空值操作数的比较是无意义的,这意味着比较两个空值或比较一个空值与一个非空值的结果都是 false 。

可以混合使用可空和不可空类型,这是因为 T 与 T? 之间存在隐式转换机制。

如果操作数的类型是 bool? ,那么 & 和 | 运算符会将 null 作为一个未知值看待。所以,null | true 的结果为真,因为 :

  1. 如果未知值为假,那么结果为真;

  2. 如果未知值为真,那么结果为真;

(P136)

?? 运算符是空值合并运算符,它既可用来计算可空值类型,也可用来计算引用类型。也就是说,如果操作数不为空,直接计算;否则,计算器默认值。

?? 运算符的结果等同于使用一个显式默认值调用 GetValueOrDefault ,除非当变量不为空时传递给 GetValueOrDefault 的表达式从未求值。

可空类型在将 SQL 映射到 CLR 时是非常有用的。

可空类型还可用于表示所谓环境属性的后备字段,如果环境属性为空,那么返回其父类的值。

(P137)

运算符可以经过重载实现更自然的自定义类型语法,运算符重载非常适合用来表示最普通的基本数据类型的自定义结构体。

下面的运算符也可以重载 :

  1. 隐式和显式转换 (使用 implicit 和 explicit 关键字实现) ;

  2. 常量 true 和 false;

下面的运算符可以间接进行重载 :

  1. 复合赋值运算符 (例如 += 、 /=) 可以通过重载非复合运算符 (例如 + 、 /) 进行隐式重载;

  2. 条件运算符 && 和 || 可以通过重载位运算符 & 和 | 进行隐式重载;

(P138)

运算符是通过声明一个运算符函数进行重载的。运算符函数具有以下规则 :

  1. 函数名是通过 operator 关键字及其后的运算符指定的;

  2. 运算符函数必须标记为 static 和 public ;

  3. 运算符函数的参数表示的是操作数;

  4. 运算符函数的返回类型表示的是表达式的结果;

  5. 运算符函数所声明的类型至少有一个操作数;

重载一个赋值运算符会自动支持相应的复合赋值运算符。

成对重载 : C# 编译器要求逻辑上成对的运算符必须同时定义。这些运算符包括 (== 、 !=) 、 (< 、 >) 和 (<= 、 >=) 。

Equals 和 GetHashCode : 在大多数情况中,如果重载了 (==) 和 (!=) ,那么通常也需要重载对象中定义的 Equals 和 GetHashCode 方法,使之具有合理的行为。如果没有按要求重载,那么 C# 编译器将会发出警告。

IComparable 和 IComparable : 如果重载了 (< 、 >) 和 (<= 、 >=),那么还应该实现 IComparable 和 IComparable

(P139)

隐式和显式转换也是可重载的运算符,这些转换经过重载后一般能使强关联类型之间的转换变得更加简明和自然。

如果要在弱关联类型之间进行转换,那么更适合采用以下方式 :

  1. 编写一个具有该转换类型的参数的构造函数;

  2. 编写 ToXXX 和 (静态) FromXXX 方法进行类型转换;

(P140)

扩展方法允许一个现有类型扩展新的方法而不需要修改原始类型的定义。

扩展方法是静态类的静态方法,其中第一个参数需要使用 this 修饰符,类型就是扩展的类型。

(P141)

扩展方法是 C# 3.0 后增加的特性。

扩展方法类似于实例方法,也支持一种链接函数的方法。

只有命名空间在定义域内,我们才能够访问扩展方法。

任何兼容的实例方法总是优先于扩展方法。

如果两个扩展方法名称相同,那么扩展方法必须作为一个普通的静态方法调用,才能够区分所调用的方法。然而,如果其中一个扩展方法具有更具体的参数,那么有更具体参数的方法优先级更高。

(P143)

匿名类型是一个由编译器临时创建来存储一组值的简单类。如果要创建一个匿名类型,我们可以使用 new 关键字,后面加上对象初始化语句,在其中指定该类型包含的属性和值。

必须使用 var 关键字来引用一个匿名类型,因为类型的名称是编译器产生的。

匿名类型的属性名可以从本身是一个标识符或以标识符结尾的表达式得到。

如果这两个匿名类型实例的元素是相同类型的,并且它们在相同的程序集中声明,那么它们在内部是相同的类型。

匿名类型的 Equals 方法也被重载了,从而能够执行正确的等于比较运算。

(P144)

匿名类型主要是在编写 LINQ 查询时使用,并且是 C# 3.0 后才出现的特性。

动态绑定是将绑定 (解析类型、成员和操作的过程) 从编译时延迟到运行时。

在编译时,如果程序员知道某个特定函数、成员或操作的存在,而编译器不知道,那么动态绑定是很有用的。

这种情况通常出现在操作动态语言 (如 IronPython) 和 COM 时,而且如果不使用动态绑定,就只能使用反射机制。

动态类型是通过上下文关键字 dynamic 声明的。

动态绑定类型会告诉编译器 “不要紧张” 。

无论绑定的是什么样的方法,其底线是已知绑定是由编译器实现的,而且绑定是完全依赖于之前已经知道的操作数类型,这就是所谓的静态绑定。

(P145)

动态类型类似于 object ,同样不表现为一种类型。其区别是能够在编译时在不知道它存在的情况下使用它。

动态对象是基于其运行时类型进行绑定的,而不是基于编译时类型。

当编译器遇到一个动态绑定表达式时 (通常是一个包含任意动态类型值的表达式) ,它仅仅对表达式进行打包,而绑定则在后面的运行时执行。

在运行时,如果一个动态对象实现了 IDynamicMetaObjectProvider ,那么这个接口将用来执行绑定。否则,绑定的发生方式就几乎像是编译器已经事先知道动态对象的运行时类型一样。我们将这两种方式称为自定义绑定和语言绑定。

COM 可认为是第三种绑定方式。

自定义绑定是通过实现了 IDynamicMetaObjectProvider (IDMOP) 而实现的。

(P146)

动态绑定会损坏静态类型安全性,但不会影响运行时类型安全性。与反射机制不同,不能通过动态绑定绕过成员访问规则。

静态和动态绑定之间最显著的差异在于扩展方法。

动态绑定也会对性能产生影响。然而,由于 DLR 的缓存机制对同一个动态表达式的重复调用进行了优化,允许在一个循环中高效地调用动态表达式。这个优化机制能够使一个简单的动态表达式的处理负载对硬件的性能影响控制在 100ms 以内。

如果一个成员绑定失败,那么程序会抛出 RuntimeBinderException 异常,可以将它看作是一个运行时的编译错误。

dynamic 和 object 类型之间可以执行一个深度等值比较。在运行时,下面这个表达式的结果为 true :

typeof(dynamic) = typeof (object)

(P147)

与对象引用相似,动态引用可以指向除指针类型以外的任意类型的对象。

在结构上,对象引用和动态引用之间没有任何区别。

动态引用可以直接在它所指的对象上执行动态操作。

动态类型会对其他所有类型进行隐式转换。

如果要成功进行转换,动态对象的运行时类型必须能够隐式转换到目标的静态类型上。

(P148)

var 和 dynamic 类型表面上是相似的,但是它们实际上是有区别的 :

var 由编译器确定类型。

dynamic 由运行时确定类型。

一个由 var 声明的变量的静态类型可以是 dynamic 。

域、属性、方法、事件、构造函数、索引器、运算符和转换都是可以动态调用的。

dynamic 的标准用例是包含一个动态接受者。

然而,还可以使用动态参数调用已知的静态函数。这种调用受到动态重载解析的影响,并且可能包括 :

  1. 静态方法;

  2. 实例构造函数;

  3. 已知静态类型的接收者的实例方法;

(P149)

动态类型用在动态绑定中。但是,静态类型在可能的情况下也用在动态绑定中。

(P150)

有一些函数是不能够动态调用的,如下 :

  1. 扩展方法 (通过扩展方法语法) ;

  2. 接口的所有成员;

  3. 子类隐藏的基类成员;

扩展方法成为只适用于编译时的概念。

using 指令在编译后会消失 (当它们在绑定过程中完成了将简单的名称映射到完整命名空间的任务之后) 。

(P151)

特性是添加自定义信息到代码元素 (程序集、类型、成员、返回值和参数) 的扩展机制;

特性的一个常见例子是序列化,就是将任意对象转换为一个特定格式或从特定格式生成一个对象的过程。在这情况中,某个字段的属性可以指定该字段的 C# 表示方式和该字段的表示方式之间的转换。

特性是通过直接或间接地继承抽象类 System.Attribte 的方式定义的。

如果要将一个特性附加到一个代码元素中,那么就需要在该代码元素之前用方括号指定特性的类型名称。

编译器能够识别这个特性,如果某个标记为弃用的类型或成员被引用时,编译器会发出警告。

按照惯例,所有特性类型都以 Attribute 结尾,C# 能够识别这个后缀,也可以在附加一个属性时省略这个后缀。

C# 语言和 .NET Framework 包含了大量的预定义特性。

特性可能具有一些参数。

特性参数分为两类 : 位置和命名。

位置参数对应于特性类型的公开构造函数的参数;命令参数则对应于该特性类型的公开字段或公开属性。

当指定一个特性时,必须包含对应于其中一个特性构造函数的位置参数。命名参数则是可选的。

(P152)

特性目标不需要显式指定,特性目标就是它后面紧跟的代码元素而且一般是一个类型或类型成员。然而,也可以给程序集附加一些特性,这要求显式地指定特性的目标。

一个代码元素可以指定多个特性,每一个特性可以列在同一对方括号中 (用逗号分割) 或者在多对方括号中或者结合两种方式。

从 C# 5 开始,可以给可选参数添加 3 个调用者信息属性中的一个,它们可以让编译器从调用者代码获取参数的默认值 :

  1. [CallerMemberName] —— 表示调用者的成员名称;

  2. [CallerFilePath] —— 表示调用者的源代码文件路径;

  3. [CallerLineNumber] —— 表示调用者源代码文件的行号;

(P153)

调用者信息特性很适合用于记录日志以及实现一些模式,如当一个对象的某个属性发生变化时,触发一个变化通知事件。事实上,.NET 框架有一个专门实现这个效果的标准接口 INotifyPropertyChanged (位于 System.ComponentModel) 。

(P154)

C# 支持通过标记为不安全和使用 /unsafe 编译器选项编译的代码块中的指针直接进行内存操作。指针类型主要用来与 C 语言 API 进行互操作,但是也可用来访问托管堆以外的内存,或者分析严重影响性能的热点。

使用 unsafe 关键字标记一个类型、类型成员或语句块,就可以在该范围内使用指针类型和对内存执行 C++ 中的指针操作。

不安全代码与对应的安全实现相比运行速度更快。

fixed 语句是用来锁定托管对象的。

由于这可能对运行时效率产生一定的影响,所以 fixed 代码块只能短暂使用,而且堆分配应该避免出现在 fixed 代码块中。

(P155)

除了 & 和 * 运算符,C# 还支持 C++ 中的 -> 运算符,可以在结构体中使用。

我们可以在代码中显式地通过 stackalloc 关键字分配栈中的内存,由于这部分内存是从栈上分配的,所以其生命周期仅限于方法的执行时间,这点与其他的局部变量相同,这个代码块可以使用 [] 运算符实现内存索引。

我们也可以使用 fixed 关键字在一个结构体代码块中分配内存。

fixed 表示两个不同的方面 : 大小固定和位置固定。

(P156)

空指针 (void) 不给出假定底层数据的具体类型,它对于处理原始内存的函数是非常有用的。任意指针类型都可以隐式地转换为 void 。 void* 不可以被解除引用,算术运算符不能通过 void 指针执行。

指针也很适于访问位于托管堆之外的数据 (如与 C DLL 或 COM 交互时) ,以及处理不在主存中的数据 (如图形化内存或嵌入式设备的存储介质) 。

(P157)

预处理指令向编译器提供关于代码范围的额外信息。最常用的预处理指令是条件指令,它提供了一种将某些代码加入或排除出编译范围的方法。

通过 #if 和 #elif 指令,可以使用 || 、 && 和 ! 运算符在多个符号上执行或、与、非操作。

#error 和 #warning 符号会要求编译器在遇到一些不符合要求的编译符号时产生一条警告信息或错误信息,从而防止出现条件指令的偶然误用。

(P158)

使用 Conditional 修饰的特性只有在出现指定的预处理符号时才编译。

(P159)

文档注释是一种嵌入的、记录类型或成员的 XML 。文档注释位于类型或成员声明之前,以三个斜线开头。

也可以采用以下方法 (注意开头有两个星号) 。/ */

如果使用 /doc 指令进行编译,那么编译器会将文档注释存储到一个 XML 文件中,并进行校对,这个特性主要有两种作用 :

  1. 如果与编译的程序集位于同一个文件夹,那么 Visual Studio 会自动读取这个 XML 文件,使用这些信息向同名程序集的使用者提供 IntelliSense 成员清单;

  2. 第三方工具 (如 Sandcastle 和 NDoc) 可以将 XML 文件转换成 HTML 帮助文件;

【第05章】

(P163)

.NET Framework 中几乎所有的功能都是通过大量的托管类型提供的,这些类型被组织成有层次的命名空间,并且被打包成一套程序集,与 CLR 一起构成 .NET 平台。

有些 .NET 类型是由 CLR 直接使用的,并且对于托管的宿主环境而言是必不可少的。这些类型位于一个名为 mscorlib.dll 的程序集中,包括 C# 的内置类型,以及基本的集合类、流处理类型、序列化、反射、多线程和原生互操作性。

除此之外是一些附加类型,它们充实了 CLR 层面的功能,提供了其他一些特性,如 XML 、网络和 LINQ 等 。这些类型位于 System.dll 、 System.Xml.dll 和 System.Core.dll 中,并且与 mscorlib 一起提供丰富的编程环境供 .NET Framework 的其他部分使用。

.NET Framework 的其余部分是由一些实用 API 组成的,主要包括以下三个方面的功能 :

  1. 用户接口技术;

  2. 后台技术;

  3. 分布式系统技术;

C# 5.0 对应 CLR 4.5,这个版本比较特殊,因为它属于 CLR 4.0 的补丁版本。

这意味着安装 CLR 4.5 之后,目标平台是 CLR 4.0 的应用实际上运行在 CLR 4.5 上。

(P164)

程序集和命名空间在 .NET Framework 中是相互交叉的。

(P164)

[.NET Framework 4.5 新特性]

Framework 4.5 新特性包括 :

  1. 通过返回 Task 的方法广泛支持异步编程;

  2. 支持 zip 压缩协议;

  3. 通过新增 HttpClient 类改进 HTTP 支持;

  4. 改进垃圾收集器和程序集资源回收的性能;

  5. 支持 WinRT 互操作性和开发 Metro 风格平板应用的 API ;

此外,还有一个新的 TypeInfo 类,以及可以指定与正则表达工作超过时间匹配的超时时间。

在并行计算领域,还有一个全新库 Dataflow,可用于开发 生产者 / 消费者 风格的网格。

此外,WPF 、 WCF 和 WF (工作流基础) 库也有一些改进。

许多核心类型定义在以下程序集中 : mscorlib.dll 、 System.dll 和 System.Core.dll 。第一个程序集 mscorlib.dll 包括运行时环境本身所需要的类型;System.dll 和 System.Core.dll 包含程序员所需要的其他核心类型。

[.NET Framework 4.0 新特性]

Framework 4.0 增加了以下新特性 :

  1. 新的核心类型 : BigInteger (大数字) 、 Complex (复数) 和元组;

  2. 新的 SortedSet 集合;

  3. 代码协定,使方法能够通过共同的义务和责任实现更可靠的交互;

  4. 直接支持内存映射文件;

  5. 延迟的文件和目录 I / O 方法,它们返回 IEnumerable 而不是数组;

  6. 动态语言运行时 (DLR) 成为 .NET Framework 的一部分;

  7. 安全透明,简化了保证部分可信环境中程序库安全性的方法;

  8. 新的多线程结构,包括更强大的 Monitor.Enter 重载、新的信号发送类 (Barrier 和 CountdownEvent) 和延迟初始化原语;

  9. 支持多核处理的并行计算 API ,包括 Parallel LINQ (PLINQ) 、命令式数据与任务并行性结构、支持并发的集合和低延迟同步机制与 spinning 原语;

  10. 用于监控应用程序域资源的方法;

Framework 4.0 还包含了一些 ASP.NET 的改进,包括 MVC 框架和 Dynamic Data,以及 Entity Framework 、 WPF 、 WCF 和 Workflow 等方面的改进。此外,它还包含了新的 Managed Extensibility Framework 库,以帮助运行时环境实现组合、发现和依赖注入。

(P165)

大多数的基础类型都直接位于 System 命名空间。其中包括 C# 的内置类型、 Exception 基类、 Enum 、 Array 和 Delegate 基类、以及 Nullable 、 Type 、 DateTime 、 TimeSpan 和 Guid 。System 命名空间也包含执行数字计算功能 (Math) 、生成随机数 (Random) 和各种数据类型转换 (Convert 和 BitConvert) 的类型。

System 命名空间还定义了 IDisposable 接口和与垃圾回收器交互的 GC 类。

在 System.Text 命名空间中有一个 StringBuilder 类,以及处理文本编码的类型。

在 System.Text.RegularExpressions 命名空间中有一些执行基于模式的搜索和替换操作的高级类型。

.NET Framework 提供了各种处理集合项目的类,其中包括基于链表和基于字典的结构,以及一组统一它们常用特性的标准接口。

System.Collections //非泛型类型
System.Collections.Generic //泛型框架
System.Collections.Specialized //强类型框架
System.Collections.ObjectModel //自定义框架基类
System.Collections.ConCurrent //线程安全框架

(P166)

Framework 3.5 增加了语言集成查询 (Language Integrated Query,LINQ) 。LINQ 允许对本地和远程集合 (例如 SQL Server 表) 执行类型安全查询。

LINQ 的最大优势是提供了一种跨多个域的统一查询 API 。

Metro 模板不包含整个 System.Data.* 命名空间。

LINQ to SQL 和 Entity Framework API 使用了 System.Data 命名空间的 ADO.NET 底层类型。

XML 在 .NET Framework 中被广泛使用,同时也得到广泛支持。

操作线程和异步操作的类型位于 System.Threading 和 System.Threading.Tasks 命名空间。

(P167)

Framework 提供了基于流的模型进行底层 输入 / 输出 操作。流一般用于文件和网络连接的直接读写操作,它们可以被链接和封装到装饰流中,从而实现压缩或加密功能。

Stream 和 I / O 类型是在 System.IO 命名空间中定义的。

可以通过 System.Net 中的类型直接访问标准的网络协议,如 HTTP 、 FTP 、 TCP / IP 和 SMTP 。

Framework 提供了几个可以将对象保存为二进制或文本方式的系统,这些系统是分布式应用程序技术所必需的,如 WCF 、 Web Services 和 Remoting ,它们也可用于将对象保存到文件和从文件恢复对象。

Metro 模板不包含二进制序列化引擎。

C# 程序编译产生的程序集包含可执行指令 (存储为中间语言或 IL) 和元数据,它描述了程序的类型、成员和属性。通过反射机制,可以在运行时检查元数据或者执行某些操作,如动态调用方法。

通过 Reflection.Emit 可以随时创建新代码。

(P168)

动态编程的类型位于 System.Dynamic 中。

.NET Framework 具有自己的安全层,从而能够将程序集装入沙箱,甚至将自己装入沙箱。

Metro 模板只包含 System.Security ;加密操作则在 WinRT 中处理。

C# 5 的异步函数可以显著简化并发编程,因为它们减少了底层技术的使用。然而,开发者有时候仍然需要使用信号发送结构、线程内存储、读 / 写 锁等。

线程类型位于 System.Threading 命名空间。

CLR 支持在一个进程中增加额外的隔离级别,即应用程序域。

AppDomain 类型定义在 System 命名空间中。

原生互操作性使您能够调用未托管 DLL 中的函数、注册回调函数、映射数据结构和操作原生数据类型。COM 互操作性使您能够调用 COM 类型和将 .NET 类型传递给 COM 。

.NET Framework 提供了 4 种支持基于用户界面的应用程序的 API 。

  1. ASP.NET (System.Web.UI) 编写运行在标准网页浏览器上的瘦客户端应用程序;

  2. Silverlight 在网页浏览器上实现富用户界面;

  3. Windows Presentation Foundation (System.Windows) 编写富客户端应用程序;

  4. Windows Forms (System.Windows.Forms) 支持遗留富客户端应用程序;

(P169)

一般而言,瘦客户端应用程序指的是网站;而富客户端应用程序则是最终用户必须下载或安装在客户端计算机上的程序。

富客户端的方法是在客户端和数据库之间插入一个中间层,中间层运行在一台远程应用程序服务器上 (通常与数据库服务器一起) ,并通过 WCF 、 Web Services 或 Remoting 与富客户端通信。

在编写网页时,可以选择传统的 Web Forms 或者新的 MVC (模型 - 视图 - 控制器) API 。这两种方法都基于 ASP.NET 基础框架。从一开始,Framework 就支持 Web Forms ;MVC 则是在后来 Ruby on Rails 和 MonoRail 流行之后才出现的。

Web Forms 仍然适合用来编写主要包含静态内容的网页。

AJAX 的使用可以通过注入 jQuery 等库进行简化。

编写 ASP.NET 应用程序的类型位于 System.Web.UI 命名空间及其子命名空间中,并且属于 System.Web.dll 程序集。

Silverlight 在技术上并不属于 .NET Framework 的主框架 : 它是一个独立的框架,包含了一部分的 Framework 核心特性,增加了作为网页浏览器插件运行的功能。

(P170)

Silverlight 主要用于一些边缘场景。

Windows Metro 库同样不属于 .NET 框架,它只用于在 Windows 8 中开发平板电脑界面。

Metro API 源于 WPF 的启发,并且使用 XAML 实现布局。其命名空间包括 Windows.UI 和 Windows.UI.Xaml 。

WPF 是在 Framework 3.0 时引入的,用来编写富客户端应用程序。

WPF 的规模和复杂性使学习周期比较长。

编写 WPF 应用程序的类型位于 System.Windows 命名空间以及除 System.Windows.Forms 之外的所有子命名空间中。

与 WPF 相比,Windows Forms 相对简单,它支持编写一般 Windows 应用程序时所需要使用的大多数特性,也能够良好地兼容遗留应用程序。

Windows Forms 的学习过程相对简单,并有丰富的第三方控件支持。

(P171)

Windows Forms 类型位于命名空间 System.Windows.Forms (在 System.Windows.Forms.dll 中) 和 System.Drawing (在 System.Drawing.dll) 中。其中后者包含了绘制自定义控件的 GDI+ 类型。

ADO.NET 是托管的数据访问 API 。虽然它的名称源于 20 世纪 90 年代的 ADO (ActiveX Data Objects) ,但是这两种技术是完全不同的。

ADO.NET 包含两个主要的底层组件 :

  1. 提供者层 —— 提供者模型定义了数据库提供者底层访问的通用类和接口。这些接口包括连接、命令,适配器和读取器 (数据库的只向前的只读游标) 。 Framework 包含对 Microsoft SQL Server 和 Oracle 的原生支持,具有 OLE-DB 和 ODBC 提供者。

  2. DataSet 模型 —— 一个 DataSet 是一个数据的结构化缓存。它类似于一个常驻内存的原始数据库,其中定义了 SQL 结构,如表、记录行、字段、关系、约束和视图。通过对数据缓存的编程,可以减少数据库的交互数量、增加服务器可扩展性以及加快富客户端用户界面的响应速度。 DataSet 是可序列化的,它支持通过客户端和服务器应用程序之间的线路传输。

提供者层只有两个 API ,它们提供了通过 LINQ 查询数据库的功能 :

  1. LINQ to SQL (从 Framework 3.5 开始引入) ;

  2. Entity Framework (从 Framework 3.5 SP1 开始引入) ;

这两种技术都包含 对象 / 关系 映射器 (ORM) ,意味着它们会自动将对象 (基于定义的类) 映射到数据库的记录行。这允许用户通过 LINQ 查询这些对象,而不需要编写 SQL 语句查询并且不需要手动编写 SQL 语句进行对象更新。

DataSet 仍然是唯一能够存储和序列化状态变化的技术 (这在多层应用程序中是非常有用的) 。

现在还没有现成的便捷方法可以使用 Microsoft 的 ORM 来编写 N 层应用程序。

LINQ to SQL 比 Entity Framework 更简单、更快速,并且一般会产生更好的 SQL 。Entity Framework 则更具灵活性,可以在数据库和查询的类之间创建复杂的映射。除了 SQL Server ,Entity Framework 还支持一些第三方数据库。

Windows Workflow 是一个对可能长期运行的业务过程进行建模和管理的框架。Workflow 目标是成为一个标准的提供一致性和互操作性的运行时库。Workflow 有助于减少动态控制的决策树的编码量。

Windows Workflow 严格意义上并不是一种后台技术,可以在任何地方使用它。

Workflow 是从 .NET Framework 3.0 开始出现的,它的类型定义在 System.Workflow 命名空间中。实际上 Workflow 在 Framework 4.0 中进行了修改,增加的新类型位于 System.Activities 命名空间。

(P172)

Framework 允许通过 System.EnterpriseServices 命名空间中的类型与 COM+ 进行互操作,以实现诸如分布式事物等服务。它也支持通过 System.Messaging 中的类型使用 MSMQ (Microsoft Message Queuing) ,微软消息队列实现异步的单向消息传递。

WCF 是 Framework 3.0 引入的一个复杂的通信基础架构。WCF 非常灵活且可配置,这使它的两个预处理器 —— Remoting 和 (.ASMX) Web Services ,大多是冗余的。

WCF 、 Remoting 和 Web Services 很相似的方面就是它们都实现以下允许客户端和服务器应用程序进行通信的基本模型 :

  1. 在服务器端,可以指定希望远程客户端应用程序能够调用的方法;

  2. 在客户端,可以指定或推断将要调用的服务器方法的签名;

  3. 在服务器端和客户端,都可以选择一种传输和通信协议 (在 WCF 中,这是通过一个绑定完成的) ;

  4. 客户端建立一个服务器连接;

  5. 客户端调用远程方法,并在服务器上透明地执行;

WCF 会通过服务协定和数据协定进一步对客户端和服务器进行解耦。概念上,客户端会发送一条 (XML 或二进制) 消息给远程服务的终端,而非直接调用一个远程方法。这种解耦方式的好处是客户端不会依赖于 .NET 平台或任意私有的通信协议。

WCF 是高度可配置的,它支持广泛的标准化消息协议,包括 WS-* 。

WCF 的另一个好处是可以直接修改协议,而不需要修改客户端或服务器应用程序的其他内容。

与 WCF 通信的类型位于 System.ServiceModel 命名空间中。

Remoting 和 .ASMX Web Services 是 WCF 的预处理器,虽然 Remoting 仍然适合在相同进程中的应用程序域之间进行通信,但是它们在 WCF 中几乎是冗余的。

Remoting 的功能针对一些紧密耦合的应用程序。

Web Services 针对一些低耦合或 SOA 类型应用程序。

Web Services 只能使用 HTTP 或 SOAP 作为传输和格式化协议,而应用程序一般是运行在 IIS 上。

互操作性的好处在于性能成本方面 —— Web Services 应用程序一般在执行和开发时间上的速度都比精心设计的 Remoting 应用程序慢。

Remoting 的类型位于 System.Runtime.Remoting 命名空间中;而 Web Services 的类型则位于 System.Web.Services 中。

(P173)

通过一个安全的 HTTP 通道进行连接时,WCF 允许通过 System.IdentityModel.Claims 和 System.IdentityModel.Policy 命名空间中的类型指定一个 CardSpace 身份。

【第06章】

(P174)

编程所需要的许多核心工具都不是由 C# 语言提供的,而是由 .NET Framework 中的类型提供的。

一个 C# 的 char 表示一个 Unicode 字符,它是 System.Char 结构体的别名。

System.Char 定义了许多处理字符的静态方法,如 ToUpper 、 ToLower 和 IsWhiteSpace 。可以通过 System.Char 类型或它的别名 char 调用这些方法。

ToUpper 和 ToLower 会受到最终用户的语言环境的影响,这可能会导致出现细微的缺陷。

(P175)

System.Char 、 System.String 还提供了针对语言变化的 ToUpper 和 ToLower ,它们加上后缀 Invariant 。

char 保留的大多数静态方法都与字符分类有关。

(P176)

对于更细的分类,char 提供了一个名为 GetUnicodeCategory 的静态方法,它返回一个 UnicodeCategory 枚举值。

通过显式转换一个整数,可以产生一个位于 Unicode 集之外的 char 。要检测字符的有效性,我们可以调用 char.GetUnicodeCategory : 如果结果是 UnicodeCategory.OtherNotAssigned ,那么这个字符就是无效的。

一个 char 占用 16 个二进制位。

C# 的 string (== System.String) 是一个不可变的 (不可修改的) 字符序列。

创建字符串的最简单的方法就是给变量定义一个字面值。

要创建一个重复的字符序列,可以使用 string 的构造函数。

还可以从 char 数组创建字符串,而 ToCharArray 方法则是执行相反操作。

我们还可以重载 string 的构造方法来接受各种 (不安全的) 指针类型,以便创建其他类型字符串。

空字符串是长度为 0 的字符串。如果要创建空字符串,可以使用一个字母值或静态的 string.Empty 字段;如果要测试空字符串,可以执行一个等值比较或测试它的 Length 属性。

由于字符串是引用类型,它们也可能是 null 。

(P177)

静态的 string.IsNullOrEmpty 方法是测试一个给定字符串是 null 还是空白的快捷方法。

字符串的索引器可以返回一个指定索引位置的字符。与所有操作字符串的方法相似,它是从 0 开始计数的索引。

string 还实现了 IEnumerable ,所以可以用 foreach 遍历它的字符。

在字符串内搜索的最简单方法是 Contains 、 StartsWith 和 EndsWith 。所有这些方法都返回 true 或 false 。

Contains 方法并没有提供这种重载的便利方法,但是可以使用 IndexOf 方法实现相同的效果。

IndexOf 方法更强大 : 它会返回指定字符或子字符串的首次出现位置 (-1 表示该子字符串不存在) 。

StartsWith 、 EndsWith 和 IndexOf 都有重载方法,我们可以指定一个 StringComparison 枚举变量或 CultureInfo 对象,控制大小写和文字顺序。默认为使用当前文化规则执行区分大小写的匹配。

LastIndexOf 与 IndexOf 类似,但是它是从后向前开始搜索的。

IndexOfAny 则返回任意一系列字符的首次匹配位置。

LastIndexOfAny 则在相反方向执行相同的操作。

由于 String 是不可变的,所有 “处理” 字符串的方法都会返回一个新的字符串,而原始字符串则不受影响 (其效果与重新赋值一个字符变量一样) 。

Substring 是取字符串的一部分。

(P178)

如果省略长度,那么会得到剩余的字符串。

Insert 和 Remove 会从一个指定位置插入或删除一些字符。

PadLeft 和 PadRight 会用特定字符将字符串 (如果未指定,则使用空格) 填充成指定的长度。

如果输入字符串长度大于填充长度,那么返回不发生变化的原始字符串。

TrimStart 和 TrimEnd 会从字符串的开始或结尾删除指定的字符;Trim 则用两个方法执行删除操作。默认情况下,这些函数会删除空白字符 (包括空格、制表符、换行符和这些字符的 Unicode 变体) 。

Replace 会替换字符串中出现的特定字符或子字符串。

ToUpper 和 ToLower 会返回输入字符串相应的大写和小写字符。默认情况下,它们会受用户的当前语言设置的影响;ToUpperInvariant 和 ToLowerInvariant 总是采用英语字母表规则。

Split 接受一个句子,返回一个单词数组。

默认情况下,Split 使用空白字符作为分隔符;经过重载后也可以接受包含 char 或 string 分隔符的 params 数组。

Split 还可以选择接受一个 StringSplitoptions 枚举值,它支持删除一些空项 : 这在一行单词由多种分隔符分隔时很有用。

静态的 Join 方法执行与 Split 相反的操作,它需要一个分隔符和字符串数组。

静态的 Concat 方法与 Join 类似,但是它只接受字符串数组参数,并且没有分隔符。

Concat 与 + 操作符效果完全相同 (实际上,编译器会将 + 转换成 Concat) 。

(P179)

静态的 Format 方法提供了创建嵌入变量字符串的便利方法。嵌入的变量可以是任意类型;而 Format 会直接调用它们的 ToString 。

包含嵌入变量的主字符串称为 “组合格式字符串” 。调用 String.Format 时,需要提供一个组合格式字符串,后面紧跟每一个嵌入式变量。

花括号里面的每一个数字称为格式项。这些数字对应参数位置,后面可以跟 :

  1. 逗号与应用的最小宽度;

  2. 冒号与格式字符串;

最小宽度用于对齐各个列,如果这个值为复数,那么数据就是左对齐;否则,数据就是右对齐的。

信用额度是通过 “C” 格式字符串格式化为货币值。

组合格式字符串的缺点是它很容易出现一些只有在运行时才能发现的错误。

进行两个值比较时,.NET Framework 有两个不同的概念 : 等值比较和顺序比较。等值比较会判断两个实例在语义上是否是相同的;而顺序比较则将两个 (如果有) 实例按照升序或降序排列,然后判断哪一个首先出现。

(P180)

等值比较并不是顺序比较的一个子集,这两种方法有各自不同的用途。

对于字符串等值比较,可以使用 == 操作符或者其中一个字符串的 Equals 方法。后者功能更强一些,因为它们允许指定一些选项,如区分大小写。

另一个不同点是,如果变量被转换成 object 类型,那么 == 就不一定是按字符串处理。

对于字符串顺序比较,可以使用 CompareTo 实例方法或静态的 Compare 和 CompareOrdinal 方法 : 这些方法会返回一个正数、负数或 0 ,这取决于第一个值是在第二个值之后、之前还是同时出现。

字符串比较有两种基本的算法 : 按顺序的和区分文化的。顺序比较会直接将字符解析为数字 (根据它们的 Unicode 数值);文化比较则参照特定的字母表来解析字符。特殊的文化有两种 : “当前文化” ,这是基于计算机控制面板的设置;“不变文化” ,这在任何计算机上都是相同的。

对于等值比较,顺序和特定文化的算法都是很有用的。然而,在排序时,人们通常选择词义相关的比较 : 对字符串按字母表排序时,需要一个字母顺序表。顺序比较则使用 Unicode 数字位置值,这可能会使英语字符按字母顺序排序 —— 但是即使这样也可能不满足你的期望。

不变文化封装了一个字母表,它认为大写字符与其对应的小写字符是相邻的。

顺序算法将所有大写字母排列在前面,然后才是全部小写字符。

尽管顺序比较有一些局限性,但是字符串的 == 操作符总是执行区分大小写的顺序比较。当不带参数调用时,string.Equals 的实例版本也是一样的;这定义了 string 类型的 “默认” 等值比较行为。

字符串的 == 和 Equals 函数选择顺序算法的原因是它既高效又具有确定性。字符串等值比较被认为是基础操作,并且远比顺序比较的使用更频繁。

等式的 “严格” 概念也与常见的 == 操作符用途保持一致。

(P181)

静态方法会更适合一些,因为即使其中一个或两个字符串为 null 它也一样有效。

String 的 CompareTo 实例方法执行区分文化和区分大小写的顺序比较。与 == 操作符不同,CompareTo 不使用顺序比较 : 对于顺序比较,区分文化的算法更有效。

Compare 实例方法实现了 IComparable 泛型接口,这是在整个 .NET Framework 中使用的标准比较协议。这意味着字符串的 CompareTo 定义了默认的顺序行为字符串。

所有顺序比较的方法都会返回正数、负数 或 0 ,这取决于第一个值是在第二个值之后、之前还是相同位置。

(P182)

StringBuilder 类 (System.Text 命名空间) 表示一个可变 (可编辑) 的字符串。使用 StringBuilder ,可以 Append 、 Insert 、 Remove 和 Replace 子字符串,而不需要替换整个 StringBuilder 。

StringBuilder 的构建函数可以选择接受一个初始字符串值,以及其内部容量的初始值 (默认是 16 个字符) 。如果需要更大的容量,那么 StringBuilder 会自动调整它的内部结构,以容纳 (会有一些性能开销) 最大的容量 (默认为 int.MaxValue) 。

StringBuilder 的一个普通使用方法是通过重复调用 Append 来创建一个长字符串。这个方法比复杂连接普通字符串类型要高效得多。

AppendLine 执行新添加一行字符串 (在 Windows 中是 "\r\n") 的 Append 操作。

AppendFormat 接受一个组合格式字符串,与 String.Format 类似。

除了 Insert 、 Remove 和 Replace 方法 (Replace 函数类似于字符串的 Replace),StringBuilder 定义了一个 Length 属性和一个可写的索引器,可用来 获取 / 设置 每个字符串。

如果要清除 StringBuilder 的内容,我们可以创建一个新的 StringBuilder 或者将它的 Length 设为 0 。

(P183)

将 StringBuilder 的 Length 设置为 0 不会减少它的内部容量。

Unicode 具有约一百万个字符的地址空间,目前已分配的大约有十万个。

.NET 类型系统的设计使用的是 Unicode 字符集。但是,ASCII 是隐含支持的,因为它是 Unicode 的子集。

UTF-8 对于大多数文本而言是最具空间效率的 : 它使用 1~4 个字节来表示每个字符。

UTF-8 是最普遍的文本文件和流的编码方式 (特别是在互联网上) ,它是 .NET 中默认的流 I / O 编码方式 (事实上,它几乎是所有语言隐含的默认编码方式) 。

UTF-16 使用一个或两个 16 位字来表示一个字符,它是 .NET 内部用来表示字符和字符串的方式。有一些程序也使用 UTF-16 写文件。

UTF-32 是空间效率最低的 : 每一个代码点直接对应一个 32 位数,所以每个字符都会占用 4 个字节。因此,UTF-32 很少使用。然而,它可以简化随机访问,因为每个字符都对应相同的字节数。

System.Text 中的 Encoding 类是封装文本编码类的通用基本类型。它有一些子类,它们的作用是封装各种编码方式的相似特性。初始化一个正确配置类的最简单方法是用一个标准的 IANA 名称调用 Encoding.GetEncoding 。

最常用的编码也可以通过专用的 Encoding 静态属性获取。

(P184)

静态的 GetEncodings 方法会返回所有支持的编码方式清单以及它们的标准 IANA 名称。

Encoding 对象最常见的应用是控制文件或流的文本读写操作。

UTF-8 是所有文件和流 I / O 的默认文本编码方式。

Encoding 对象和字节数组之间也可以进行互相转换。GetBytes 方法将使用指定的编码方式将 string 转换为 byte[];而 GetString 则将 byte[] 转换为 string 。

(P185)

.NET 将字符和字符串存储为 UTF-16 格式。

在 System 命名空间中有三个不可变结构可用来表示日期和时间 : DateTime 、 DateTimeOffset 和 TimeSpan 。而 C# 没有定义与这些类型相对应的关键字。

TimeSpan 表示一段时间间隔或者是一天内的时间。对于后者,他就是一个 “时钟” 时间 (不包括日期) ,它等同于从半夜 12 点开始到现在的时间 (假设没有夏时制) 。TimeSpan 的最小单位为 100 纳秒,最大值为 1 千万天,可以为正数或负数。

创建 TimeSpan 的方法有三种 :

  1. 通过其中一个构造方法;

  2. 通过调用其中一个静态的 From... 方法;

  3. 通过两个 DateTime 相减得到;

(P186)

如果希望指定一个单位的时间间隔,如分钟、小时等,那么静态的 From.. 方法更方便。

TimeSpan 重载了 < 、 > 、 + 和 - 操作符。

Total... 属性则返回表示整个时间跨度的 double 类型值。

静态的 Parse 方法则执行与 ToString 相反的操作,它能将一个字符串转换为一个 TimeSpan 。

TryParse 执行与 ToString 相同的操作,但是当转换失败时,它会返回 false ,而不是抛出异常。

XmlConvert 类也提供了符合标准 XML 格式化协议的 TimeSpan 字符串转换方法。

TimeSpan 的默认值是 TimeSpan.Zero 。

TimeSpan 也可用于表示一天内时间 (从半夜 12 点开始经过的时间) 。要获得当前的时间,我们可以调用 DateTime.Now.TimeOfDay 。

(P187)

DateTime 和 DateTimeOffset 表示日期或者时间的不可变结构。它们的最小单位为 100 纳秒,值的范围从 0001 到 9999 年。

DateTimeOffset 是从 Framework 3.5 开始引入的,在功能上类似于 DateTime 。它的主要特性是能够存储 UTC 偏移值,这允许我们比较不同时区的时间值时得到更有意义的结果。

DateTime 和 DateTimeOffset 在处理时区方式上是不同的。DateTime 具有三个状态标记,可表示 DateTime 是否与下列因素相关 :

  1. 当前计算机的本地时间;

  2. UTC (相当于现代的格林威治时间) ;

  3. 不确定;

DateTimeOffset 更加特殊 —— 它将 UTC 的偏移量存储为一个 TimeSpan 。

这会影响等值比较结果,而且是在 DateTime 和 DateTimeOffset 之间进行选择的主要依据 :

  1. DateTime 会忽略三个比较状态标记,并且当两个值的年、月、日、时、分等相等时就认为它们是相等的;

  2. 如果两个值引用相同的时间点,那么 DateTimeOffset 就认为它们是相等的;

夏时制会使这个结果差别很大,即使应用程序不需要处理多个地理时区。

在大多数情况中,DateTimeOffset 的等值比较逻辑会更好一些。

(P188)

如果在运行时指定与本地计算机相关的值,使用 DateTime 会更好。

DateTime 定义了能够接受年、月和日以及可选的时、分、秒和毫秒的构造方法。

如果只指定日期,那么时间会被隐含地设置为半夜时间 (00:00:00) 。

DateTime 构造方法也允许指定一个 DateTimeKind —— 这是一个具有以下值的枚举值 : Unspecified 、 Local 、 Utc 。

这三个值与前一节所介绍的三个状态标记相对应。

Unspecified 是默认值,它表示 DateTime 是未指定时区的。

Local 表示与当前计算机的本地时区相关。

本地 DateTime 不包含它引用了哪一个特定的时区,而且与 DateTimeOffset 不同的是,它也不包含 UTC 偏移值。

DateTime 的 Kind 属性返回它的 DateTimeKind 。

DateTime 的构造方法也经过重载从而可以接受 Calendar 对象 —— 允许使用 System.Globalization 中所定义的日历子类指定一个时间。

DateTime 总是使用默认的公历。

如果要使用另一个日历进行计算,那么必须使用 Calendar 子类自己的方法。

也可以使用 long 类型的计数值 (ticks) 来创建 DateTime,其中计数值是从午夜开始算起的 100 纳秒数。

在互操作性上,DateTime 提供了静态的 FromFileTime 和 FromFileTimeUtc 方法来转换一个 Windows 文件时间 (由 long 指定),并且提供了 FromOADate 来转换一个 OLE 自动日期 / 日期 (由 double 指定) 。

要从字符串创建 DateTime,我们必须调用静态的 Parse 或 ParseExact 方法。

这两个方法都接受可选标记和格式提供者;ParseExact 还接受格式字符串。

(P189)

DateTimeOffset 具有类似的构造方法,其区别是还需要指定一个 TimeSpan 类型的 UTC 偏移值。

TimeSpan 必须刚好是整数分钟,否则函数会抛出一个异常。

DateTimeOffset 也有一些接受 Calendar 对象、 long 计数值的构造方法,以及接受字符串的静态的 Parse 和 ParseExact 方法。

还可以通过构造方法从现有的 DateTime 创建 DateTimeOffset 。

也可以通过隐式转换创建。 从 DateTime 隐式转换到 DateTimeOffset 是很简单的,因为大多数的 .NET Framework 类型都支持 DateTime —— 而不是 DateTimeOffset 。

如果没有指定偏移量,那么可以使用以下规则从 DateTime 值推断出偏移值 :

  1. 如果 DateTime 具有一个 UTC 的 DateTimeKind ,那么其偏移量为 0 ;

  2. 如果 DateTime 具有一个 Local 或 Unspecified (默认) 的 DateTimeKind ,那么偏移量从当前的本地时区计算得到;

为了在其他方法中进行转换,DateTimeOffset 提供了三个属性,它们返回 DateTime 类型的值 :

  1. UtcDateTime 属性会返回一个 UTC 时间表示的 DateTime ;

  2. LocalDateTime 属性返回一个以当前本地时区 (在需要时进行转换) 表示的 DateTime ;

  3. DateTime 属性返回一个以任意指定的时区表示的 DateTime ,以及一个 Unspecified 的 Kind ;

DateTime 和 DateTimeOffset 都具有一个静态的 Now 属性,它会返回当前的日期和时间;

DateTime 也具有 Today 属性,它返回日期部分;

(P190)

静态的 UtcNow 属性会返回以 UTC 表示的当前日期和时间。

所有这些方法的精度取决于操作系统,并且一般是在 10 ~ 20 毫秒内。

DateTime 和 DateTimeOffset 提供了返回各种 日期 / 时间 的类似实例属性。

DateTimeOffset 也有一个类型为 TimeSpan 的 Offset 属性。

调用 DateTime 的 ToString 会将结果格式化为一个短日期 (全部是数字) ,后跟一个长时间 (包括秒) 。

(P191)

默认情况下,操作系统的控制面板决定日、月或年是否在前、是否使用前导零,以及是使用 12 小时还是 24 小时时间格式。

调用 DateTimeOffset 的 ToString 效果是一样的,只是它同时返回偏移值。

ToShortDateString 和 ToLongDateString 方法只返回日期部分。

ToShortTimeString 和 ToLongTimeString 方法只返回时间部分。

刚刚介绍的这四个方法实际上是四个不同的格式字符串的快捷方式。ToString 重载后可以接受一个格式字符串和提供者,这允许指定大量的选项,并且控制区域设置的应用方式。

静态的 Parse 和 ParseExact 方法执行与 ToString 相反的操作,它们将一个字符串转换成一个 DateTime 或 DateTimeOffset 。Parse 方法重载后也可以接受格式提供者。

因为 DateTime 和 DateTimeOffset 是结构体,它们是不可为空的。当需要将它们设置为空时,可以使用以下两种方法 :

  1. 使用一个 Nullable 类型值;

  2. 使用静态域 DateTime.MinValue 或 DateTimeOffset.MinValue (这些类型的默认值) ;

使用一个可空值通常是最佳方法,因为编译器会防止出现错误。DateTime.MinValue 对于兼容 C# 2.0 (引入了可空类型) 之前编写的代码是很有用的。

(P192)

当比较两个 DateTime 实例时,只有它们的计数值是可以比较的,它们的 DateTimeKinds 是被忽略的。

TimeZone 和 TimeZoneInfo 类提供了关于时区名称、 UTC 偏移量和夏令时规则等信息。

TimeZoneInfo 在两者中较为强大,并且是 Framework 3.5 的新增特性。

这两种类型的最大区别是 TimeZone 只能访问当前的本地时区,而 TimeZoneInfo 则能够访问全世界的时区。而且,TimeZoneInfo 具有更丰富的 (虽然有时不宜使用) 基于规则的夏令时描述模型。

(P193)

静态的 TimeZone.CurrentTimeZone 方法会基于当前的本地设置返回一个 TimeZone 对象。

TimeZoneInfo 类采用类似的处理方式。TimeZoneInfo.Local 返回当前的本地时区。

静态的 GetSystemTimeZones 方法则返回全世界所有的时区。

(P197)

格式化表示将对象转换为一个字符串;而解析表示将一个字符串转换为某种对象。

最简单的格式化机制是 ToString 方法,它能够为所有简单的值类型产生有意义的输出。对于反向转换,这些类型都定义了静态的 Parse 方法。

如果解析失败,它会抛出一个 FormatException 。许多类型还定义了 TryParse 方法,如果转换失败,它会返回 false ,而不是抛出一个异常。

(P198)

如果遇到错误,在异常处理代码块中调用 TryParse 是更快速且更好的处理方式。

使用格式提供者的方法是 IFormattable 。所有数字类型和 DateTime(Offset) 都实现了这个接口。

格式字符串提供一些指令;而格式提供者则决定了这些指令是如何转换的。

大多数类型都重载了 ToString 方法,可以省略 null 提供者。

(P199)

.NET Framework 定义了以下三种格式提供者 (它们都实现了 IFormatProvider) : NumberFormatInfo 、 DateTimeFormatInfo 、 CultureInfo 。

所有 enum 类型都可以格式化,但是它们没有具体的 IFormatProvider 类。

在格式提供者的上下文中,CultureInfo 作为其他两个格式提供者的间接机制,返回一个适合文化区域设置的 NumberFormatInfo 或 DateTimeFormatInfo 。

(P200)

组合格式字符串可以包含组合变量替代符和格式字符串。

Console 类本身重载了它的 Write 和 WriteLine 方法,以接受一个组合格式字符串。

所有格式提供者都实现了 IFormatProvider 接口 。

(P202)

标准格式字符串决定数字类型或 DateTime / DateTimeOffset 集是如何转换为字符串的。格式字符串有两种 :

  1. 标准格式字符串 —— 可以使用标准格式字符串是实现基本的控制。标准格式字符串是由一个字母及其后面一个可选的数字 (它的作用由前面的字母决定) 组成;

  2. 自定义格式字符串 —— 可以使用自定义格式字符串作为模板对每一个字符进行精细控制;

自定义格式字符串与自定义格式提供者无关。

(P203)

如果不提供数字格式字符串或者使用 null 或空字符串,那么相当于使用不带数字的 “G” 标准格式化字符串。

每一种数字类型都定义了一个静态的 Parse 方法,它接受 NumberStyles 参数。NumberStyles 是一个标记枚举值,可以判断如何读取转换为数字类型的字符串。

(P208)

.NET Framework 将以下类型称为基本类型 :

  1. bool 、 char 、 string 、 System.DateTime 和 System.DateTimeOffset ;

  2. 所有 C# 数值类型;

静态 Convert 类定义了将每一个基本类型转换成其他基本类型的方法。可是,这些方法大多数都是无用的 : 它们或者抛出异常,或者是隐式转换的冗余方法。

(P209)

所有基本类型都 (显式地) 实现了 IConvertible ,它定义了转换到其他基本类型的方法。在大多数情况中,每一种方法的实现都直接调用 Convert 类中的方法。在少数情况中,编写一个接受 IConvertible 类型的参数是很有用的。

允许在数字类型之间执行的隐式和显式转换,概括为 :

  1. 隐式转换只支持无值丢失的转换;

  2. 只有会出现值丢失的转换才需要使用显式转换;

转换是经过效率优化的,,因此它们将截断不符合要求的数据。

Convert 的数值转换方法采用圆整的方式。

Convert 采用银行的圆整方式,将中间值转换为偶整数 (这样可以避免正负偏差) 。

To (整数类型) 方法隐含了一些重载方法,它们可以将数字转换为其他进制。第二个参数指定了进制数,它可以是任何一种进制 (二、八、十或十六进制) 。

ChangeType 的缺点是无法指定一个格式字符串或解析标记。

Convert 的 ToBase64String 方法能够将一个字节数组转换为 Base 64 ;FromBase64String 则执行相反操作。

(P211)

大多数基本类型都可以通过调用 BitConverter.GetBytes 转换为字节数组。

应用程序的国际化包括两个方面 : 全球化和本地化。

全球化注重于三个任务 (重要性由大到小) :

  1. 保证程序在其他文化环境中运行时不会出错;

  2. 采用一种本地文化的格式化规则;

  3. 设计程序,使之能够从将来可能编写和部署的附属程序集读取与文化相关的数据和字符串;

本地化表示为特定文化编写附属程序集以结束最终任务。

(P213)

Round 方法能够指定圆整的小数位数以及如何处理中间值 (远离 0 ,或者使用银行的圆整方式) 。

Floor 和 Ceiling 会圆整到最接近的整数 : Floor 总是向下圆整,而 Ceiling 则总是向上圆整 —— 即使是负数 。

(P214)

BigInteger 结构体是 .NET Framework 新增的特殊数值类型。它位于 System.Numerics.dll 中新的 System.Numerics 命名空间,可以用于表示一个任意大的整数而不会丢失精度。

C# 并不提供 BigInteger 的原生支持,所以无法表示 BigInteger 值。然而,可以从任意整数类型隐式地转换到 BigInteger 。

可以将一个 BigInteger 隐式地转换为标准数值类型,也可以显式地进行反向转换。

BigInteger 重载了所有的算术运算符,以及比较、等式、求模 (%) 和负值运算符。

将一个数字存储到一个 BigInteger 中而不是字节数组的优点是可以获得值类型的语义,调用 ToByteArray 可以将一个 BigInteger 转换回字节数组。

Complex 结构体是 Framework 4.0 新增的另一个特殊数值类型,用来表示用 double 类型的实数和虚数构成的复数。

要使用 Complex ,我们需要实例化这个结构体,指定实数和虚数值。

(P215)

Complex 结构体具有实数和虚数值的属性,以及阶和量级。

还可以通过指定量级和阶来创建复数。

复数也重载了标准的算术操作符。

Complex 结构体具有一些支持更高级功能的静态方法,其中包括 :

  1. 三角函数;

  2. 取对数与求幂;

  3. 共轭;

Random 类能够生成一个随机 byte 、 integer 或 double 类型的伪随机数序列。

要使用 Random ,首先要实例化,可选择提供一个种子来实例化随机数序列。使用相同的种子一定会产生相同序列的数字,当希望有可再现性时,是非常有用的。

如果不希望可再现性,那么可以不使用种子来创建 Random 而是使用当前系统时间来创建。

因为系统时钟只有有限的粒度,创建时间间隔很小 (一般是 10ms 内) 的两个 Random 将会产生相同序列的值。常用的方法是每次需要一个随机数时才实例化一个新的 Random 对象,而不是重用同一个对象。

调用 Next(n) 可以生成一个 0 至 n-1 之间的随机整数。NextDouble 可以生成一个 0 至 1 之间的随机 double 数值。NextBytes 会用随机数填充一个字节数组。

(P216)

System.Enum 的静态实用方法主要是与转换和获取成员清单相关。

(P217)

每一种整型 (包括 ulong) 都可以转换为十进制数而不会丢失值。

Enum.ToObject 能够将一个整型值转换为一个指定类型的 enum 实例。

(P218)

ToObject 已经重载,可以接受所有的整数类型和对象 (后者支持任何装箱的整数类型) 。

Enum.Parse 可以将一个字符串转换为一个 enum 。它接受 enum 类型和一个包含多个成员的字符串。

Enum.GetValues 返回一个包含某特定 enum 类型的所有成员。

Enum.GetNames 执行相同的操作,但是返回的是一个字符串数组。

在内部,CLR 通过反射 enum 类型的字段实现 GetValues 和 GetNames ,其结果会被缓存以提高效率。

枚举类型的语义很大程序上是由编译器决定的。在 CLR 中,enum 实例 (未拆箱) 与它实际的整型值在运行时是没有任何区别的。而且,在 CLR 中定义的 enum 仅仅是 System.Enum 的子类型,它的每个成员都是静态的整型域。

(P219)

C# 会在调用 enum 实例的虚方法之前对它进行显式装箱。而且,当 enum 实例被装箱后,它会获得一个引用其 enum 类型的封装。

Framework 4.0 提供了一组新的泛型类来保存不同类型的元素集,称为元组。

每种元组都有名为 Item1 、 Item2 等的只读属性,分别对应一种类型参数。

可以通过它的构造方法实例化一个元组,或者通过静态帮助方法 Tuple.Create 。后者使用的是泛型推断方法,可以将这种方法与隐式类型转换结合使用。

元组可以很方便地用来实现从一个方法返回多个值或者创建值对集合。

元组的替代方法是使用对象数组。然而,这种方法会影响静态类型安全性,增加了值类型的 装箱 / 开箱 开销,并且需要作一些编译器无法验证的复杂转换。

(P220)

元组是一些类 (也就是引用类型) 。

Guid 结构体表示一个全局唯一标识符 : 一个随机生成的 16 位值,几乎可以肯定具有唯一性。Guid 在应用程序和数据库中通常用作各种排序的键。

我们可以调用静态的 Guid.NewGuid 方法创建一个新的随机 Guid 。

ToByteArray 方法可以将一个 Guid 转换为一个字节数组。

静态的 Guid.Empty 属性会返回一个空的 Guid (全为零) ,通常用来替换 null 。

(P221)

相等有两种类型 :

  1. 值相等 —— 两个值在某种意义上是相等的;

  2. 引用相等 —— 两个引用指向完全相同的对象;

默认情况下 :

  1. 值类型采用的是值相等;

  2. 引用类型采用的是引用相等;

事实上,值类型只能使用值相等形式进行比较 (除非已装箱) 。

引用类型默认是采用引用相等的比较形式。

(P222)

有三种标准方法可以实现等值比较 :

  1. == 和 != 运算符;

  2. 对象的虚方法 Equals ;

  3. IEquatable 接口;

Equals 在 System.Object 中定义,所以所有类型都支持这个方法。

Equals 是在运行时根据对象的实际类型解析的。

对于结构体,Equals 会调用每个字段的 Equals 执行结构比较。

(P223)

Equals 很适合用来比较两个未知类型的对象。

object 类提供了一个静态的帮助方法,它的名称是 Equals ,与虚方法相同,但是不会有冲突,因为它接受两个参数。

如果在处理编译时未知类型对象,这是一种能够避免 null 值异常的等值比较算法。

(P224)

静态方法 object.ReferenceEquals 可以实现引用等值比较。

另一种采用引用等值比较的方法是将值转换为 object ,然后再使用 == 运算符。

调用 object.Equals 的结果是强制对值类型执行装箱。这在对性能高度敏感的情况下是不太适合的,因为装箱操作相对于实际比较操作的开销还要高。C# 2.0 引入了一个解决办法,那就是使用 IEquatable 接口。

关键在于实现 IEquatable 所返回的结果与调用 object 的虚方法 Equals 是一样的,但是执行速度会更快。大多数 .NET 基本类型都实现了 IEquatable 。可以在泛型中使用 IEquatable 作为一个约束。

(P225)

默认的等值比较操作有 :

  1. 值类型采用的是值相等;

  2. 引用类型采用的是引用相等;

此外 :

结构体的 Equals 方法默认采用的是结构值相等。

有时创建一个类型时重载这个行为是很有用的,有以下两种情况我们需要这样做 :

  1. 修改相等的语义 —— 当 == 和 Equals 默认行为不符合要求的类型,并且这种行为一般人难以想象时,修改相等的语义是很有用的。

  2. 提高结构体的等值比较的执行速度 —— 结构体的默认结构等值比较算法相对较慢。通过重载 Equals 来实现这个过程可以将性能提高 20% 。重载 == 运算符和实现 IEquatable 接口可以实现等值比较的拆箱,并且同样能够将比较速度提高 20% 。

(P226)

重载引用类型的等值语义并不能提高性能。引用等值比较的默认算法已经非常快速,因为它只比较两个 32 位或 64 位引用。

重载等值语义操作步骤总结 :

  1. 重载 GetHashCode() 和 Equals() ;

  2. (可选) 重载 != 和 == ;

  3. (可选) 实现 IEquatable

在 System.Object 中定义的 GetHashCode 对于散列表而言非常重要,所以每一种类型都具有一个散列码。

引用类型和值类型都只有默认的 GetHashCode 实现,这意味着不需要重载这个方法 —— 除非重载了 Equals 。 (反之亦然,如果重载了 GetHashCode ,那么也必须重载 Equals) 。

下面是重载 object.GetHashCode 的其他规则 :

  1. 它必须为 Equals 方法都返回 true 的两个对象返回相同的值,因此, GetHashCode 和 Equals 必须同时重载;

  2. 它不能抛出异常;

  3. 如果重复调用相同对象,必须返回相同的值 (除非对象改变) ;

(P227)

结构体的默认散列方法只是在每个字段上执行按位异或操作,通常会比编写的算法产生更多的重复码。

类的默认 GetHashCode 实现基于一个内部对象标识,它在 CLR 当前实现中的每一个实例上都是唯一的。

object.Equals 的执行逻辑如下 :

  1. 对象不能是 null (除非它是可空类型) ;

  2. 相等是自反性的 (对象与其本身相等) ;

  3. 相等是可交换的 (如果 a.Equals(b) ,那么 b.Equals(a)) ;

  4. 相等时可传递的 (如果 a.Equals(b) 且 b.Equals(c) ,那么 a.Equals(c)) ;

  5. 等值操作是可重复且可靠的 (它们不会抛出异常) ;

除了重载 Equals ,还可以选择重载相等和不等运算符。这种重载几乎都发生在结构体上,否则 == 和 != 运算符无法正确判断类型。

对于类,与两种方法可以处理 :

  1. 保留 == 和 != ,这样它们会应用引用相等;

  2. 重载 Equals 同时重载 == 和 != ;

(P228)

为了保持完整性,在重载 Equals 时,最好也要实现 IEquatable ,其结果应该总是与被重载对象 Equals 方法保持一致,如果自己编写 Equals 方法实现,那么实现 IEquatable 并没有任何的程序开销。

(P229)

除了标准等值协议,C# 和 .NET 还定义了用于确定对象之间相对顺序的协议。基本的协议包括 :

  1. IComparable 接口 (IComparable 和 IComparable) ;

  2. > 和 < 运算符;

IComparable 接口可用于普通的排序算法。

< 和 > 操作符比较特殊,它们大多数情况用于比较数字类型。因为它们是静态解析的,所以可以转换为高效的字节码,适用于一些密集型算法。

.NET Framework 也通过 IComparer 接口实现了可插入的排序协议。

(P230)

CompareTo 方法按如下方式执行 :

  1. 如果 a 在 b 之后,那么 a.CompareTo(b) 返回一个正数;

  2. 如果 a 与 b 位置相同,那么 a.CompareTo(b) 返回 0 ;

  3. 如果 a 在 b 之前,那么 a.CompareTo(b) 返回一个负数;

(P231)

在重载 < 和 > 后,同时实现 IComparable 接口,这也是一种标准方法,但是反之不成立。事实上,大多数实现了 IComparable 的 .NET 类型都没用重载 < 和 > 。与等值的处理方法不同的是,在等值中如果重载了 Equals ,一般也会重载 == 。

字符串不支持 < 和 > 运算符。

【第07章】

(P234)

System.Diagnostics 中的 Process 类可以用于启动一个新的进程。

Process 类也允许查询计算机上运行的其他进程,并与之交互。

(P235)

.Net Framework 提供了标准的存储和管理对象集合的类型集。其中包括可变大小列表、链表和排序或不排序字典以及数组。在这些类型中,只有数组属于 C# 语言;其余的集合只是一些类,可以像使用其他类一样进行实例化。

Framework 中的集合类型可以分成以下三类 :

  1. 定义标准集合协议的接口;

  2. 随时可用的集合类 (列表、字典等) ;

  3. 编写应用程序特有集合的基类;

集合命名空间有以下几种 :

System.Collections —— 非泛型集合类和接口;
System.Collections.Specialized —— 强类型非泛型集合类;
System.Collections.Generic —— 泛型集合类和接口;
System.Collections.ObjectModel —— 自定义集合的委托和基类;
System.Collections.Concurrent —— 线程安全的集合;

(P236)

IEnumerator 接口定义了以向前方式遍历或枚举集合元素的基本底层协议。

MoveNext 将当前元素或 “游标” 向前移动到下一个位置,如果集合没有更多的元素,那么它会返回 false 。Current 返回当前位置的元素 (通常需要从 object 转换为更具体的类型) 。在取出第一个元素之前,我们必须先调用 MoveNext —— 即使是空集合也支持这个操作。如果 Reset 方法实现了,那么它的作用就是将位置移回到起点,允许再一次遍历集合。 (通常是不需要调用 Reset 的,因为并非所有枚举器都支持这个方法) 。

IEnumerable 可以看作是 “IEnumerator 的提供者” ,它是集合类需要实现的最基础接口。

(P237)

IEnumerable 实现了 IDisposable 。它允许枚举器保存资源引用,并保证这些资源在枚举结束或者中途停止时能够被释放。foreach 语句能够识别这个细节。

(P238)

using 语句保证清理操作的执行。

有时由于下面一个或多个原因而希望实现 IEnumerable 或 IEnumerable :

  1. 为了支持 foreach 语句;

  2. 为了与任何使用标准集合的组件交互;

  3. 作为一个更复杂集合接口实现的一部分;

  4. 为了支持集合初始化器;

为了实现 IEnumerable / IEnumerable ,必须提供一个枚举器。可以采用以下三个方法来实现 :

  1. 如果这个类 “包装” 了任何一个集合,那么就返回所包装集合的枚举器;

  2. 使用 yield return 的迭代器;

  3. 实例化 IEnumerator / IEnumerator

还可以创建一个现有集合类的子类,Collection 正是基于此目的而设计的。

返回另一个集合的枚举器就是调用内部集合的 GetEnumerator 。然而,这种方法仅仅适合一些最简单的情况,那就是内部集合的元素正好是所需要的类型。

更好的方法是使用 C# 的 yield return 语句编写迭代器。

迭代器是 C# 语言的一个特性,它能够协助完成集合编写,与 foreach 语句协助完成集合遍历的方式是一样的。

迭代器会自动处理 IEnumerable 和 IEnumerator 或者它们的泛型类的实现。

注意, GetEnumerator 实际上不返回一个枚举器,通过解析 yield return 语句,编译器编写一个隐藏的枚举器类,然后重构 GetEnumerator 来实例化和返回这个类。

迭代器很强大,也很简单,并且是 LINQ 实现的基础。

(P240)

因为 IEnumerable 实现了 IEnumerable ,所以必须同时实现泛型和非泛型的 GetEnumerator 。

最后一种编写 GetEnumerator 的方法是编写一个直接实现 IEnumerator 的类。

(P241)

实现 Reset 方法不是必需的,相反,可以抛出一个 NotSupportedException 。

注意,第一次调用 MoveNext 会将位置移到列表的第一个 (而非第二个) 元素。

(P242)

IEnumerable (和 IEnumerable ) —— 支持最少的功能 (只支持枚举) 。

ICollection (和 ICollection ) —— 支持一般的功能 。

IList / IDictionary 及其非泛型版本 —— 支持最多的功能 。

大多数情况下不需要实现这些接口,几乎在需要编写一个集合类的任何时候,都可以使用子类 Collection 替代。

泛型和非泛型版本的差别很大,特别是对于 ICollection 。

因为泛型出现在后,而泛型接口是为了后面出现的泛型而开发的。

ICollection 并没有继承 ICollection ;

IList 也没有继承 IList ;

而且 IDictionary 也同样不继承 IDictionary 。

当然,在有利的情况下,集合类本身通常是可以实现某个接口的两个版本的。

.NET Framework 中并没有一种统一使用集合 (collection) 和 列表 (list) 这两个词的方法。我们通常将 集合 (collection) 和 列表 (list) 这两个术语看作在很多方面是同义的,只有在使用具体类型时例外。

ICollection 是对象的可计数集合的标准接口。它提供了很多功能,包括确定集合大小 (Count) 、确定集合中是否存在某个元素 (Contains) 、将集合复制到一个数组 (ToArray) 以及确定集合是否为只读 (IsReadOnly) 。对于可写集合,可能还需要对集合元素执行 Add 、 Remove 和 Clear 操作。而且,由于它继承了 IEnumerable ,所以也支持通过 foreach 语句进行遍历。

(P243)

非泛型的 ICollection 具有与可计数集合类似的功能,但是它不支持修改列表或检查元素成员的功能。

IList 是标准的可按位置索引的接口,除了从 ICollection 和 IEnumerable 继承的功能,它还提供了按位置 (通过一个索引器) 读写元素和按位置 插入 / 删除 元素的功能。

IndexOf 方法可以对列表执行线性搜索,如果未找到指定项,那么返回 -1 。

IList 非泛型版本具有更多的成员方法,因为它继承了少量的 ICollection 成员方法。

(P244)

非泛型 IList 接口的 Add 方法返回一个整数,这是最新添加元素的索引。相反,ICollection 的 Add 方法的返回类型为 void 。

通用的 List 类是 IList 和 IList 的典型表现。C# 数组也同时实现了泛型和非泛型的 IList 。

为了与只读的 Windows Runtime 集合实现互操作,Framework 4.5 引入了一个新的集合接口 IReadOnlyList 。这个接口本身很有用,并且可以看作为 IList 的缩减版本,它只包含列表只读操作所需要的成员。

因为它的类型参数只用在输出位置,所以它被标记为协变式 (covariant) 。

IReadOnlyList 表示一个链表的只读版本,它并不意味着底层实现也是只读的。

IReadOnlyList 与 Windows 运行时类型 IVectorView 相对应。

(P245)

Array 类是所有一维和多维数组的隐式基类,它是实现标准集合接口的最基本类型之一。

Array 类提供了类型统一性,所以常见的方法都适用于所有的数组,而与它们声明或实际的元素类型无关。

由于数组是基本类型,所以 C# 提供了明确的声明和初始化语法。

当使用 C# 语法声明一个数组时,CLR 会在内部将它转化为 Array 的子类 —— 合成一个对应数组维数和元素类型的伪类型。

CLR 也会特别处理数组类型的创建,将它们分配到一块连续的内存空间。因此数组的索引非常高效,但是不允许在创建后修改数组大小。

Array 实现了 IList 的泛型与非泛型的集合接口。

Array 类实例也提供了一个静态的 Resize 方法,但是它实际上是创建一个新数组,然后将每一个元素复制到新数组中。Resize 方法是很低效的,而且程序的数组引用无法修改为新位置。

实现可变大小集合的最好方法是使用 List 类。

(P246)

因为 Array 是一个类,所以无论数组的元素是什么类型,数组 (本身) 总是引用类型。

两个不同的数组在等值比较中总是不相等的 —— 除非使用自定义的等值比较。

Framework 4.0 提供了一种用于比较数组或元组元素的比较方式,可以通过 StructuralComparisons 类型进行访问。

数组可以通过 Clone 方法进行复制。然而,这是一个浅克隆,表示只有数组本身表示的内存会被复制。如果数组包含的是值类型的对象,那么这些值会被复制类;如果数组包含的是引用类型的对象,那么只有引用被复制。

如果要进行深度复制即复制引用类型子对象,必须遍历整个数组,然后手动克隆每个元素。相同的规则也适用于其他 .NET 集合类型。

CLR 不允许任何对象 (包括数组) 在大小上超过 2GB (无论是运行在 32 位或是 64 位环境上) 。

(P247)

你可能会以为 Array 类的许多方法是实例方法,但是实际上它们是静态方法。这是一个奇怪的设计方法,意味着在寻找 Array 方法时,应该同时查看静态方法和实例方法。

最简单的创建和索引数组的方法是使用 C# 的语言构造。

此外,可以通过调用 Arrray.CreateInstance 动态实例化一个数组,可以在运行时指定元素类型和维数以及为非零开始索引的数组指定下界。非零开始索引的数组不符合 CLS (Common Language Specification ,公共语言规范) 。

静态的 GetValue 和 SetValue 方法访问动态创建的数组的元素 (它们也支持普通数组的元素访问) 。

动态创建的从零开始索引的数组可以转换为一种类型匹配或兼容 (兼容标准数组变化规则) 的 C# 数组。

为什么不使用 object[] 作为统一的数组类型,而要使用 Array 类呢?原因就是 object[] 既不兼容多维数组,也不兼容值类型以及非零开始索引的数组。

GetValue 和 SetValue 也支持编译器创建的数组,并且它们对于编写能够处理任意类型和任意维数数组的方法是很有用的。

(P248)

如果元素与数组类型不一致,SetValue 方法会抛出一个异常。

当实例化数组时,无论是通过语言语法还是 Array.CreateInstance ,数组元素都会自动初始化。对于引用类型元素的数组,这意味着写入 null 值;对于值类型元素的数组,这意味着调用值类型的默认构造函数 (实际上是成员的 “归零” 操作)。

数组可以通过 foreach 语句进行枚举。

也可以使用静态的 Array.ForEach 方法进行枚举。

(P249)

GetLength 和 GetLongLength 会返回一个指定维度的长度 (0 表示一维数组),而 Length 和 LongLength 返回数组的元素总数 (包括所有维数) 。

GetLowerBound 和 GetUpperBound 在处理非零开始索引的数组时是很有用的。GetUpperBound 返回的结果与任意维度的 GetLowerBound 和 GetLength 相加的结果是相同的。

(P250)

Array.Sort 要求数组中的元素实现 IComparable ,这意味着 C# 的最基本类型都可以进行排序。

如果元素是不可比较的,或者希望重写默认的顺序比较,那么必须给 Sort 提供一个自定义的比较提供者,用来判断两个元素的相对位置。可以采用以下方法 :

  1. 通过一个实现 IComparer / IComparer 的帮助对象;

  2. 通过一个 Comparison 委托 : public delegate int Comparison (T x, T y) ;

Comparison 委托采用与 IComparer.CompareTo 相同的语义。

(P251)

作为 Sort 的替代方法,可以使用 LINQ 的 OrderBy 和 ThenBy 运算符。与 Array.Sort 不同的是,LINQ 运算符不会修改原始数组,而是将排序结果保存在一个新的 IEnumerable 序列中。

Array 有 4 个方法可以执行浅拷贝操作 : Clone 、 CopyTo 、 Copy 和 ConstrainedCopy 。前两个方法都是实例方法;后两个方法是静态方法。

Clone —— 方法返回一个全新 (浅拷贝) 的数组;

CopyTo 和 Copy —— 方法复制数组的若干连续元素;

ConstrainedCopy —— 执行一个原子操作 : 如果所有请求的元素都无法成功复制,那么操作会回滚;

Array 还有一个 AsReadOnly 方法,它会返回一个包装器,可以防止元素被重新赋值。

(P252)

System.Linq 命名空间包含另外一些适合用于执行数组转换的扩展方法。这些方法会返回一个 IEnumerable ,它可以通过 Enumerable 的 ToArray 方法转换回一个数组。

在灵活性和性能方面,泛型类更具优势,而它们的非泛型冗余实现则是为了实现向后兼容。

泛型 List 和非泛型 ArrayList 类提供了一种动态调整大小的对象数组实现,它们是集合类中使用最广泛的类。 ArrayList 实现了 IList ,而 List 同时实现了 IList 和 IList 。与数组不同,所有接口都是公开实现的。

在内部,List 和 ArrayList 都维护了一个对象数组,并在超出容量时替换为一个更大的数组。添加元素是很高效的 (因为数组末尾通常还有空闲存储位置) ,但是插入元素的速度会慢一些 (因为插入位置之后的所有元素都必须向后移动才能留出插入空间) 。与数组一样,如果对已排序列表执行 BinarySearch 方法,那么查找是很高效的,但是其他情况效率就不高,因为查找时必须检查每一个元素。

如果 T 是一种值类型,那么 List 的速度会比 ArrayList 快好几倍,因为 List 不需要元素执行装箱和开箱操作。

List 和 ArrayList 具有可以接受已有元素集合的构造函数,它们会将已有集合的每一个元素复制到新的 List 或 ArrayList 中。

(P254)

非泛型 ArrayList 类主要用于向后兼容 Framework 1.x 代码。

ArrayList 的功能与 List 类型相似。当需要一个包含不共享任何相同基类的混合类型元素时,这两种类型是很有用的。在这种情况下,如果需要使用反射机制处理列表,那么选择使用 ArrayList 更具优势。相比于 List ,反射机制更容易处理非泛型的 ArrayList 。

如果定义 System.Linq 命名空间,那么可以通过先调用 Cast 再调用 ToList 的方式将一个 ArrayList 转换为一个泛型 List 。

Cast 和 ToList 是 System.Linq.Enumerable 的扩展方法,是从 .NET Framework 3.5 开始支持的。

LinkedList 是一个泛型的双向链表。双向链表是一系列互相引用的节点,其中每个节点都引用前一个节点、后一个节点及实际存储数据的元素。它的主要优点是元素总是能够高效地插入到链表的任意位置,因为插入节点只需要创建一个新节点,然后修改引用值。然而,查找插入节点的位置可能减慢执行速度,因为链表本身没有直接索引的内在机制;我们必须遍历每一个节点,并且无法执行二叉查找。

(P255)

LinkedList 实现了 IEnumerable 和 ICollection 及其非泛型版本,但是没有实现 IList ,因为它不支持根据索引进行访问。

(P256)

Queue 和 Queue 是一种先进先出 (FIFO) 的数据结构,它们提供了 Enqueue (将一个元素添加到队列末尾) 和 Dequeue (取出并删除队列的第一个元素) 方法。它们还包括一个只返回而不删除队列第一个元素的 Peek 方法,以及一个 Count 属性 (可用来检查出列前的元素个数) 。

虽然队列是可枚举的,但是它们都没有实现 IList / IList ,因为不能够直接通过索引访问它的成员。

队列内部是使用一个可根据需要调整大小的数组来操作的,这与一般的 List 类很类似。队列具有一个直接指向头和尾元素的索引,因此,入列和出列操作是及其快速的 (除非内部的大小需要调整) 。

(P257)

Stack 和 Stack 是后进先出 (LIFO) 的数据结构,它们提供了 Push (添加一个元素到堆栈的顶部) 和 Pop (从堆栈顶部取出并删除一个元素) 方法。它们还提供了一个只读而不删除元素的 Peek 方法,以及 Count 属性和用于导出数据以实现随机访问的 ToArray 方法。

堆栈内部也是使用一个可根据需要调整大小的数组来操作,这一点和 Queue 与 List 类似。

BitArray 是一个保存压缩 bool 值的可动态调整大小的集合。它具有比简单的 bool 数组和 bool 泛型 List 更高的内存使用效率,因为它的每个值只占用一位,而 bool 类型的每个值占用一个字节。

(P258)

HashSet 和 SortedSet 分别是 Framework 3.5 和 4.0 新增加的泛型集合。这两个类都具有以下特点 :

  1. 它们的 Contains 方法都使用基于散列的查找而实现快速执行;

  2. 它们都不保存重复元素,并且都忽略添加重复值的请求;

  3. 无法根据位置访问元素;

SortedSet 按一定顺序保存元素,而 HashSet 则不是。

这些类型的共同点是由接口 ISet 提供的。

HashSet 是通过使用只存储键的散列表实现的;而 SortedSet 则是通过一个 红 / 黑 树实现的。

两个集合都实现了 ICollection 接口。

因为 HashSet 和 SortedSet 实现了 IEnumerable 接口,所以可以将另一种集合作为任意集合操作方法的参数。

SortedSet 的构造函数还接受一个可选的 IComparer 参数 (而非一个等值比较器) 。

(P259)

字典是一种所包含元素均为 键 / 值 对的集合。字典通常都用来执行列表查找和排序。

Framework 通过接口 IDictionary 和 IDictionary 及一组通用的字典类定义了一个标准字典协议。这些类在以下方面有区别 :

  1. 元素是否按有序序列存储;

  2. 元素是否按位置 (索引) 或按键访问;

  3. 类是泛型还是非泛型的;

  4. 集合变大时的性能;

(P260)

IDictionary 定义了所有基于 键 / 值 的集合的标准协议。它扩展了 ICollection ,增加了一些基于任意类型的键访问元素的方法和属性。

(P261)

从 Framework 4.5 开始,还出现了一个接口 IReadOnlyDictionary ,它定义了字典成员的只读子集。它与 Windows Runtime 类型 IMapView 相对应,当时也是因为相同原因而引入的。

重复的键在所有字典实现中都是禁止的,所以用相同的键调用两次 Add 会抛出一个异常。

直接通过一个 IDictionary 进行枚举会返回一个 KeyValuePair 结构体序列。

非泛型的 IDictionary 接口在原理上与 IDictionary 相同,但是存在以下两个重要的功能区别 :

  1. 通过索引器查找一个不存在的键会返回 null (而不是抛出一个异常) ;

  2. 使用 Contains 而非 ContainsKey 来检测成员是否存在 ;

枚举一个非泛型 IDictionary 会返回一个 DictionaryEntry 结构体序列。

泛型 Dictionary (和 List 集合一样) 是使用最广泛的集合之一。它使用一个散列表结构来存储键和值,而且快速、高效。

Dictionary 的非泛型版本是 Hashtable ;Framework 中不存在名为 Dictionary 的非泛型类。当我们提到 Dictionary 时,指的是泛型的 Dictionary 类。

Dictionary 同时实现了泛型和非泛型的 IDictionary 接口,而泛型 IDictionary 是公开的接口。

事实上, Dictionary 是泛型 IDictionary 的一个标准实现。

(P262)

Dictionary 和 Hashtable 的缺点是元素是无序的。而且,添加元素时不保存原始顺序。此外,所有字典类型都不允许出现重复值。

(P263)

OrderedDictionary 是一种非泛型字典,它能够保存添加元素的原始顺序。通过使用 OrderedDictionary ,既可以根据索引访问元素,也可以根据键进行访问。

OrderedDictionary 并不是一个有序的字典。

OrderedDictionary 是 Hashtable 和 ArrayList 的组合。

这个类是在 .NET 2.0 中引入的,特殊的是,它没有泛型版本。

ListDictionary 和 HybridDictionary 这两个类都只有非泛型版本。

Framework 只支持两种在内部结构中将内容根据键进行排序的字典 :

  1. SortedDictionary

  2. SortedList (SortedList 是具有相同功能的非泛型版本) ;

(P265)

Collection 类是一个可定制的 List 包装类。

(P267)

CollectionBase 是 Framework 1.0 引入的 Collection 的非泛型版本。它提供了大多数与 Collection 相似的特性,但是使用方式不太灵活。

KeyedCollection 是 Collection 的子类。它增加也删去了一些功能。它增加的功能是按键访问元素,这与字典很相似,删去的功能是委托自己的内部列表。

KeyedCollection 通常看作是实现了按键进行快速查找的 Collection

(P269)

KeyedCollection 的非泛型版本称为 DictionaryBase 。

DictionaryBase 存在的目的就是为了向后兼容。

ReadOnlyCollection 是一个包装器,或者称为委托,它提供了集合的一种只读视图。它的用途是允许一个类公开地显示集合的只读访问,但是同时这个类仍然可以在内部进行修改。

【第08章】

(P277)

LINQ 是 Language Integrated Query 的简写,它可以被视为一组语言和框架特性的集合,我们可以使用 LINQ 对本地对象和远程数据源进行结构化的类型安全的查询操作。

在 C# 3.0 和 Framework 3.5 中引入了 LINQ 。

LINQ 可用于查询任何实现了 IEnumerable 接口的集合类型。

LINQ 具有编译时的类型检查及动态查询组合这两大优点。

LINQ 中所有核心类型都包含在 System.Linq 和 System.Linq.Expressions 这两个命名空间中。

LINQ 数据源的基本组成部分是序列和元素。在这里,序列是指任何实现了 IEnumerable 接口的对象,其中的每一项则称为一个元素。

查询运算符是 LINQ 中用于转换序列的方法。通常,查询运算符可接收一个输入序列,并将其转换为一个输出序列。在 System.Linq 命名空间的 Enumerable 类中定义了约 40 种查询运算符,这些运算符都是以静态扩展方法的形式来实现的,称为标准查询运算符。

我们把对本地序列进行的查询操作称为本地查询或者是 LINQ 到对象查询。

LINQ 还支持对那些从远程数据源中动态获取的序列进行查询,这些序列需要实现 IQueryable 接口,而在 Queryable 类中则有一组相应的标准查询运算符对其进行支持。

(P278)

一个查询可以理解为一个使用查询运算符对所操作的序列进行转换的表达式。

由于标准查询运算符都是以静态扩展方法的方式来实现的,因此我们可以像使用对象的实例方法那样直接使用。

大多数查询运算符都接受一个 Lambda 表达式作为参数。

Lambda 表达式用于对查询进行格式化。

(P279)

运算符流语法和查询表达式语法是两种互补的 LINQ 表达方法。

运算符流是最基本同时也是最灵活的书写 LINQ 表达式的方式。

如果想创建更复杂的查询表达式,只需在前面的表达式后面添加新的查询运算符。

(P280)

查询运算符绝不会修改输入序列,相反,它会返回一个新序列。这种设计是符合函数式编程规范的, LINQ 的思想实际上就起源于函数式编程。

(P281)

每个查询运算符对应着一个扩展方法。

(P282)

返回一个 bool 值的表达式我们称之为 “断言” 。

查询运算符的 Lambda 表达式针对的是集合中的每个元素,而不是集合整体。

标准的查询运算符使用了一个泛型 Func 委托。 Func 是 System.Linq 命名空间中一组通用的泛型委托,它的作用是保证 Func 中的参数顺序和 Lambda 表达式中的参数顺序一致。

(P283)

标准的查询运算符使用下面这些泛型 :

  1. TSource —— 输入集合的元素类型;

  2. TResult —— 输出集合的元素类型 (不同于 TSource) ;

  3. TKey —— 在排序、分组或者连接操作中所用的键 ;

这里的 TSource 由输入集合的元素类型决定。而 TResult 和 TKey 则由我们给出的 Lambda 表达式指定。

Lambda 表达式可以指定输出序列的类型,也就是说 Select 运算符可以根据 Lambda 表达式中的定义将输入类型转化成输出类型。

Where 查询运算符的内部操作比 Select 查询运算符要简单一些,因为它只筛选集合,不对集合中的元素进行类型转换,因此不需要进行类型推断。

Func 将每个输入元素关联到一个排序键 TKey ,TKey 的类型也是由 Lambda 表达式中推测出来的,但它的类与同输入类型、输出类型是没有关系的,三者是独立的,类型可以相同也可以不同。

(P284)

实际上我们可以使用传统的方式直接调用 Enumerable 中的各种方法来实现查询运算符的功能,此时在查询过程可以不使用 Lambda 表达式。这种直接调用的方式在对本地集合进行查询时非常好用,尤其是在 LINQ to XML 这种操作中应用最为方便。

传统调用方式并不适合对 IQueryable 类型集合的查询,最典型的就是对数据库的查询,因为在对 IQueryable 类型数据进行查询时,Queryable 类中的运算符需要 Lambda 表达式来生成完整的查询表达式树,没有 Lambda 表达式,这个表达式树将不能生成。

LINQ 中集成了对集合的排序功能,这种内置的排序对整个 LINQ 体系来说有重要意义。因为一些查询操作直接依赖于这种排序。

Take 运算符 —— 会输出集合中前 x 个元素,这个 x 以参数的形式指定;

Skip 运算符 —— 会跳过集合中的前 x 个元素,输出其余元素;

Reverse 运算符 —— 则会将集合中的所有元素反转,也就是按照元素当前顺序的逆序排列;

Where 和 Select 这两个查询运算符在执行时,会将集合中元素按照原有的顺序进行输出。实际上,在 LINQ 中,除非有必要,否则各个查询运算符都不会改变集合中元素的排序方式。

(P285)

Union 运算符会将结果集合中相同的元素去掉;

(P286)

查询表达式一般以 from 子句开始,最后以 select 或者 group 子句结束。

(P287)

查询表达式中的所有逻辑都可以用运算符流语法来书写。

紧跟在 from 关键字之后的标识符实际上是一个范围变量,范围变量指向当前序列中将要进行操作的元素。

在每个子查询的 Lambda 表达式中,范围变量都会被重新定义。

要定义存储中间结果的变量,需要使用下面几个子句 : let 、 into 、一个新的 from 子句、 join 。

(P288)

查询表达式语法和运算符流语法各有优势。

在包含以下运算符的查询操作中,使用查询表达式语法更加方便 :

  1. 在查询中使用 let 子句导入新的查询变量;

  2. 在查询中用到 SelectMany 、 Join 或者 GroupJoin 这些运算符;

对于只包含 Where 、 OrderBy 或者 Select 的查询语句,这两种查询方式都可以。

一般来说,查询表达式语法由单个的运算符组成,结构比较清晰;而运算符流语法写出的代码相对简洁。

在不含以下运算符的查询中,选用运算符流语法进行查询会更加方便 : Where 、 Select 、 SelectMany 、 OrderBy 、 ThenBy 、 OrderByDescending 、 ThenByDescending 、 GroupBy 、 Join 、 GroupJoin 。

如果一个查询运算符没有适合的查询语法,可以混合使用两种查询方式来得到最终结果,这样做的唯一限制是,在整个查询中,每个查询表达式的表达必须是完整的 (必须由 from 子句开始,由 select 或者 group 子句结束) 。

(P289)

在比较复杂的查询中,混合使用两种查询语法进行查询的方式非常高效。

有时候,即使混合使用了两种查询语法,也没有写出真正简练的 LINQ 查询,但注意不要因此养成只使用一种查询语法的习惯。如果习惯只使用一种语法形式的,在遇到复杂查询情况时,很难找到一种真正高效的方式去解决问题。

在 LINQ 中,另一个很重要的特性是延迟执行,也可以说是延迟加载,它是指查询操作并不是在查询运算符定义的时候执行,而是在真正使用集合中的数据时才执行。

绝大部分标准的 LINQ 查询运算符都具有延迟加载这种特性,当然也有例外,以下是几个例外的运算符 :

  1. 那些返回单个元素或者返回一个数值的运算符;

  2. 转换运算符 : ToArray 、 ToList 、 ToDictionary 、 ToLookup ;

以上这些运算符都会触发 LINQ 语句立即执行,因为它们的返回值类型不支持延迟加载。

(P290)

在 LINQ 中,延迟加载特性有很重要的意义,这种设计将查询的创建和查询的执行进行了解耦,这使得我们可以将查询分成多个步骤来创建,有利于查询表达式的书写,而且在执行的时候按照一个完整的结构去查询,减少了对集合的查询次数,这种特性在对数据库的查询中尤为重要。

子查询中的表达式有额外的延迟加载限制。无论是聚合运算符还是转换运算符,如果出现在子查询中,它们都会被强制地进行延迟加载。

(P292)

LINQ 查询运算符之所以有延迟加载功能,是因为每个运算符的返回值不是一个一般的数组或者集合,而是一个经过封装的序列,这种序列通常情况下并不直接存储数据元素,它封装并使用运行时传递给它的集合,元素也由其他集合来存储它实际上只是维护自己与数据集合的一种依赖关系,当有查询请求时,再到它依赖的序列中进行真正的查询。

查询运算符实际上是封装一系列的转换函数,这种转换函数可以将与之关联的数据集转换为各种形式的序列。如果输出集合不需要转换的话,那么就不用执行查询运算符封装的转换操作,这个时候查询运算符实际上就是一个委托,进行数据转发而已。

(P293)

如果使用运算符流语法对集合进行查询,会创建多个层次的封装集合。

在使用 LINQ 语句的返回集合时,实际是在原始的输入集合中进行查询,只不过在进入原始集合之前,会经过上面这些封装类的处理,在不同层次的封装类中,系统都会对查询做相应的修改,这使得 LINQ 语句使用的各种查询条件会被反映到最终的查询结果中。

(P294)

如果在 LINQ 查询语句的最后加上 ToList 方法,会强制 LINQ 语句立刻执行,查询结果会被保存到一个 List 类型的集合中。

LINQ 的延迟加载特性有这样一种功能 : 不论查询语句是连续书写的还是分多个步骤完成的,在执行之前,都会被组合成一个完整的对象模型,而且两种书写方式所产生的对象模型是一样的。

LINQ 查询是一个低效率的流水线。

(P295)

LINQ 使用的是需求驱动的模型,先请求再有数据。

在 LINQ 中,所谓子查询就是包含在另一个查询的 Lambda 表达式中的查询语句。

一个子查询实际上就是一个独立的 C# 表达式,可以是 LINQ 表达式,也可以是普通的逻辑判断,所以只要是符合 C# 语法规则的内容,都可以放在 Lambda 表达式的右侧作为子查询来使用。也就是说,子查询的使用规则是由 Lambda 表达式的规则所决定的。

“子查询” 这个词,在通常意义下,概念非常宽泛,我们只关注 LINQ 下的子查询。在运算符流语法中,子查询是指包含在 Lambda 表达式中的查询语句。在查询表达式中,只要包含在其他查询语句中的查询,都是子查询,但是 from 子句除外。

子查询一般有两个作用 : 一个是为父查询确定查询范围,一般是一个较小的查询范围,另一个作用是为外层查询的 Lambda 表达式提供参数。

(P296)

子查询在什么时候执行完全是由外部查询决定的,当外部查询开始执行时,子查询也同时执行,它们是同步的,在整个查询中,子查询的执行结果被作为父查询的某个组成部分。我们可以认为查询的开始命令是从外向内传递的,对本地集合的查询严格按照这种由外向内的顺序进行;但对数据库的查询,则没有那么严格,只是原则上按照这种方式进行。

另一种理解方式是,子查询会在需要返回查询结果时执行,那什么时候需要子查询返回查询结果决定于外部查询什么时候被执行。

(P297)

在执行本地查询时,单独书写子查询是一种常用的查询方式。但是当子查询中的数据和外部查询有紧密关联的时候,即内部数据需要用到外部数据的值时,这种方式不适合,最好写成一个表达式。

(P298)

在子查询中使用单个元素或者聚合函数的时候,整个 LINQ 查询语句并不会被强制执行,外部查询还是以延迟加载的方式执行。这是因为子查询是被间接执行的,在本地集合查询中,它通过委托的驱动来执行;而在远程数据源的查询中,它通过表达式树的方式执行。

如果 Select 语句中已经包含了子查询,在这种情况下如果是本地查询,那么相当于将源序列重新封装到一个新的序列中,集合中的每个元素都是以延迟加载的方式执行的。

书写复杂的 LINQ 查询表达式的三种方式 :

  1. 递增式的书写方式;

  2. 使用 into 关键字;

  3. 包装查询语句;

实际上无论用何种书写方式,在运行时,LINQ 查询表达式都会被编译成相同的查询语句来运行。

在使用多个查询条件进行查询的时候,这种递增式的书写方式比较实用。

(P299)

根据上下文的不同, into 关键字在查询表达式中有两种完全不同的功能。这里首先介绍如何使用 into 关键字延长查询 (另一种是和 GroupJoin 配合使用) 。

在 LINQ 查询中,一般会用到集合的映射,也就是在 Select 方法中将查询结果直接组装成新的集合,这种映射一般在查询的最后执行。但是如果在映射之后还想对新集合执行查询的话,就可以使用 into 关键字来完成。

(P300)

注意,into 关键字只能出现在 select 和 group 关键字之后,into 会重新创建一个新的查询,在新的查询中,我们可以再次使用 where 、 orderby 、 select 关键字。

into 关键字的作用就是在原来的查询中重新创建一次新的查询,在执行前,这种带 into 的查询表达式会被编译成运算符流的查询语句,因此使用 into 运算符并不会带来性能上的损失。

包含了多个层次的查询表达式,在语义和执行上都和递增式的 LINQ 查询语句相同,它们本质上没有区别,唯一的区别就是查询关键字的使用顺序。

在多层次查询中,内部查询是在传递带之前执行的。而子查询则是传送带上的一部分,它会随着整个传送带的运行而执行。

(P302)

所谓匿名类型指的是没有显式定义过的类型,在查询过程中,可以使用这种类型来封装查询结果。实际上这个类并不是没有定义,只是不用我们自己定义,编译器会自动定义这个类型。

要在 C# 代码中定义一种编译时才能确定的类型,唯一的选择是使用 var 关键字,此时 var 关键字就不仅仅是为了便于书写,而是不得不这么写,因为我们不知道匿名类型的名字。

(P303)

使用 let 关键字,可以在查询中定义一个新的临时变量来存放某些步骤的查询结果。

编译器在编译 let 关键字的时候,会把它翻译成一个匿名类型,这个匿名类型中包含了之前的范围变量 n 和一个新的表达式变量。也就是说,编译器将 n 翻译成了前面的匿名类型查询。

let 还有以下两个优点 :

  1. 保留了前面查询中的范围变量;

  2. 在一个查询中可以重复使用它定义的变量;

在 LINQ 查询中,在 where 关键字之前或之后可以使用任意多个 let 关键字。后面的 let 关键字会使用前面 let 关键字的返回类型,显然,let 关键字会在每一次使用时重新组成结果集。

let 关键字一般不用来返回数值类型的结果,更多使用在子查询中。

LINQ 包含两种查询 : 对本地集合的本地查询以及对远程数据的解释型查询。

对本地集合的查询,这种查询调用 IEnumerable<> 接口中定义的 Enumerable 方法实现了接口中所有的方法来完成具体的查询。

在解释型的查询中,所有的查询操作都是通过 IQueryable 接口中的方法完成的,具体的方法实现是在 Queryable 类中。在这种查询中,LINQ 语句不会被编译成 .NET Framework 中间语言 (IL),而会在运行时被解释成查询表达式树来执行。

(P304)

实际上,可以使用 Enumerable 中的方法来查询 IQueryable 类型的数据源,但会遇到一个问题,那就是查询的时候,远端的数据源必须被加载到本地内存中,然后以本地数据源的方式进行处理。可以想象,这种查询的效率非常低,每次都需要读取大量的数据,在本地进行筛选。这正是创建解释型查询的原因。

在 .NET Framework 中有两个类都实现了 IQueryable 接口,这两个类用于实现两种不同的查询 :

  1. LINQ to SQL;

  2. Entity Framework (EF);

这两种 LINQ-to-db 的查询技术实际上非常相似。

在对本地数据源的查询中,也可以使用 IQueryable 接口中的方法进行查询,只要在本地集合的最后使用一个 AsQueryable 方法即可。

IQueryable 实际上是对 IEnumerable 方法的扩展。

(P306)

查询表达式树是 System.Linq.Expression 命名空间下的一种对象模型,这种对象是在运行时被解释运行的 (这也是为什么 LINQ to SQL 和 EF 支持延迟加载)。

解释型的查询和本地数据查询的本质不同在于它们的执行方式。在遍历解释型的集合时,整个 LINQ 查询语句会被编译成一个完整的查询表达式树来加以执行。

(P307)

Entity Framework 也需要类似的标签,但是除了这些之外,他还需要一个额外的 XML 文件 Entity Data Model (EDM),在这个文件中定义了数据表和实体类的对应关系。

LINQ to SQL 和 EF 中可能定义了 30 种查询方式,但是在 SQL Server 的 SQL 查询中只有 10 种查询方式,而最终 LINQ 查询表达式要被翻译成 SQL 来执行,那么只能在 10 种查询方法中选一种来使用。如果在 LINQ 使用了一个功能很强大的运算符,但是在 SQL 中却没有相同功能的运算符,那么 LINQ 中的这个运算符就会被翻译成其他的 SQL 语句来完成这项功能。

一个 LINQ 查询中可以同时使用解释型查询运算符和本地查询运算符。应用的典型方式就是把本地查询操作放在外层,将解释型的查询操作放在内层,在执行查询的时候,解释型的操作先执行,返回一个结果集合给外层的本地查询使用。这种查询模式经常用于 LINQ 对数据库的查询操作。

查询运算符绝不会修改输入序列,相反,它会返回一个新序列。这种设计是符合函数式编程规范的,LINQ 的思想实际上就起源于函数式编程。

(P309)

两种方式可以间接地调用 AsEnumerable 方法,那就是 ToArray 方法和 ToList 方法。使用 AsEnumerable 方法有下面两点好处,一是这个方法不会强制查询立即执行,但是如果希望查询立即执行的话,就要使用另外两个方法了;二是它不会创建本地的存储结构,因此它会比较节省资源。

当查询逻辑从数据库移到本地会降低查询的性能,特别是当查询的数据量比较大的时候,效率损失更加严重。同样针对上面这个示例,有一个更有效 (同时也更复杂) 的方式来完成上面的查询,那就是使用 SQL CLR 在数据库端实现正则表达式的查询。

(P310)

LINQ to SQL 和 EF 都是用 LINQ 来实现的对象的映射工具,它们之间的不同在于映射的方式,我们知道,在数据库查询中,映射的一端是数据库表,LINQ to SQL 可以将数据库表结构映射成对象,然后供调用者使用,这种映射严格按照数据库表结构,映射成的对象不需要我们定义。与之不同的是,EF 对这种映射做了一些改进,那就是允许我们定义实体类,也就是允许开发者定义数据库表被映射成什么类型。这种映射提供了一种更灵活的解决方案,但是它会降低查询性能,也增加了使用的复杂度,因为需要占用额外的时间去维护数据库和自定义的实体类间的映射关系。

L2S是由微软的 C# 团队完成的,在 Framework 3.5 中发布,而 EF 是由 ADO.NET 团队在 ADO.NET SP1 中发布的。后来 L2S 的开发和维护由 ADO.NET 团队来接管,由于开发重心的不同,在 .NET Framework 4.0 中对 L2S 的改变很少,而主要的改进集中在 EF 方面。

尽管在性能上和易用性上,EF 在 .NET Framework 4.0 中已经有了极大的改进,但是两种技术还是各有优势。L2S 的优点是简单易用、执行性能好,此外它生成的 SQL 语句的解释质量更好一些。EF 的优点是允许我们创建自定义的持久化的实体类,用于数据库的映射。另外 EF 允许使用同一个查询机制查询 SQL Server 之外的数据源,实际上 L2S 也支持这个功能,但是为了鼓励第三方的查询机制的出现,L2S 中没有对外公布这些机制。

EF 4.0 突出的改进是它支持几乎所有的 L2S 中的查询方法。

L2S 允许任何类来承载数据,只要类中加入了合适的标签即可。

[Table] 标签定义在 System.Data.Linq.Mapping 命名空间中,它定义的类型用来承载数据表中的一行数据。默认情况下,L2S 会认为这个类名和它对应的表名是相同的,如果想让两者不同的话,由于表名已经固定,只能更改对应的类名,更改方式是在 [Table] 标签中显式地指定类名。

在 L2S 中,如果一个类具有 [Table] 标签,就称这个类为实体,为了能够顺利使用,这个实体的结构必须与数据表的结构相匹配,多字段或少字段都不行。这种限制使得这种映射是一种低级别的映射。

(P311)

[Column] 标签用来指示数据表中的某列,如果实体中定义的列名和数据表中的别名不同,那么需要在 [Column] 标签中特别指出所对应的列名。

[Column] 标签中的 IsPrimaryKey 属性用于指示当前列是主键,在数据中这列用于唯一标识一条数据,在程序中也用这列区分不同的实体,将实体中的变换更新到数据库的时候,也需要使用这一列来确定写入的目标。

总的来讲,在定义实体类的时候,L2S 允许将数据库的字段映射对象 (实体中的属性) 定义成私有的,它可以访问到实体类中的私有变量。

实际上与数据库表对应的实体类是可以自动生成的,不用逐行书写,常用的生成工具有 Visual Studio (需要在 “工程” 菜单添加一个 “LINQ to SQL Classes” 选项)和命令行工具 SqlMetal 。

和 L2S 中的实体类相似,EF中允许开发者定义自己的实体类用于承载数据,不同的是,EF 中的实体类的定义要灵活得多,在理论上允许任何类型的类来作为实体类使用 (在某些特殊情况下需要实现一些接口) ,也就是说实体类中的结构不用和数据表中的字段完全对应。

和 L2S 不同的是,在 EF 中,要完成数据的映射和查询,之定义上面这个实体类是不够的。因为在 EF 中,查询并不是直接针对数据库进行的,它使用了一种更高级别的抽象模型,称为实体数据模型 (EDM , Entity Data Model) ,我们的查询语句是针对这个模型来定义的。

EDM 实际上是使用 XML 定义的一个 .edmx 类型的文件,这个文件包含三部分内容 :

  1. 概念模型 : 定义了数据库的信息,不同的数据库有不同的概念模型内容;

  2. 存储模型 : 定义了数据库的表结构;

  3. 映射 : 定义了数据库表和实体类之间的映射关系;

(P312)

创建 .edmx 文件最简单的方式是使用 Visual Studio ,在 “项目” 菜单中点击 “添加新项” ,在弹出的窗口中选择 “ADO.NET Entity Data Model” 。之后使用向导就可以完成实体类到数据库表的映射配置。这一系列操作不仅添加一个 .edmx 文件,还会创建涉及到的实体类。

在 EF 中实体类都是映射到概念模型上,所有对概念模型的查询和更新操作,都是由 Object Services 发起的。

EF 的设计者在设计的时候将映射关系想得比较简单,他们假设数据表和实体类之间的映射关系是 1 : 1 的,所以并没有提供专门的机制去完成一对多或者多对一的映射。尽管这样,如果确实需要这种特殊的映射关系,还是可以通过修改 .edmx 文件中的相关内容来实现。下面是几个常用的修改操作 :

  1. 多个表映射到一个实体类;

  2. 一个表映射到多个实体类;

  3. 按照 ORM 世界中的三种继承方式将继承的类映射到表;

三种继承策略是 :

  1. 每个分层结构一张表 : 一张表映射到整个类分层结构。该表中包含分隔符列,用于指出每个行应该映射到哪个类;

  2. 每个类一张表 : 一张表映射到一个类,意味着继承的类映射到多张表。查询某个实体时,EF 生成 SQL JOIN ,以合并其所有基类;

  3. 每个具体类一张表 : 一张单独的表映射到每个具体的类。这意味着基类映射到多张表,并且在查询基类的实体时, EF 生成 SQL UNION ;

比较一下,L2S 仅支持每个分层结构一张表。

EF 还支持 LINQ 之外的查询方式,有一种语言叫 Entity SQL (ESQL),使用这种语言,我们可以通过 EDM 查询数据库。这种查询方式非常便于动态地构建查询语句。

在创建了实体类之后 (如果是 EF 的话还需要有 EDM 文件),就可以对数据库进行查询了。在查询之前,首先要创建 DataContext (L2S) 或者 ObjectContext (EF) 对象,这个对象用于指定数据库连接字符串。

(P313)

直接创建 DataContext / ObjectContext 实例是一种很底层的使用方式,它可以展示出这两种类型是如何工作的。但在实际应用中,更常用的方式是创建类型化的 Context (继承自 DataContext / ObjectContext) 来使用。

对于 L2S 来说,我们只需为 DataContext 传递一个数据库连接字符串即可;而对于 EF ,传递的是数据库连接实体,这个实体中除了数据库连接字符串之外,还包括 EDM 文件的路径信息。 (如果通过 Visual Studio 创建 EDM 文件,那么系统会自动在项目的 app.config 文件中添加完整的数据库连接实体,可以从这个文件得到需要的信息) 。

然后我们就可以使用 GetTable (L2S) 或者 CreateObjectSet (EF) 对象了,这两个对象都是用于从数据库中读取数据。

Single 运算符会根据主键从结果集中取出一行记录。和 First 关键字不同的是,Single 运算符要求结果集中只有一条记录,当结果集中的结果多于一行时,它会抛出异常;而 First 关键字在这种情况下则不会抛出异常。

DataContext / ObjectContext 这两个对象实际上只做两件事情。第一,它作为一个工厂,将我们查询的数据组合成对象。第二,它会维护实体类的状态,如果查询出实体类中的值在类外改变了,它会记录下这个字段,然后便于更新回数据库。

在 EF 中,唯一的不同点是使用 SaveChanges 方法代替 SubmitChanges 方法。

(P314)

在对数据库的查询中,一个更好的方式是为每个数据库定义一个继承自 DataContext / ObjectContext 的子类,一般会为每个实体类都添加一个这样的属性,这种属性我们称之为类型化的 Context 。

尽管 DataContext / ObjectContext 都实现了 IDisposable 接口,而且 Dispose 方法会强制断开数据库连接,但是我们一般不通过调用 Dispose 方法来销毁这两个对象,因为 L2S 和 EF 在返回查询结果后会自动断开连接。

(P315)

DataContext / ObjectContext 对象有跟踪实体类状态的功能,当取出一个表中的数据保存到本地内存之后,如果下次再到数据库中查询某条已经存在的数据, DataContext / ObjectContext 并不会去数据库中读取数据,而是直接从内存中取出需要的数据。也就是说,在一个 context 的生命周期中,他不会将数据库中的某行记录返回两次 (数据记录之间使用主键进行区分) 。

L2S 和 EF 都允许关闭对象状态跟踪功能,为避免这些限制,在 L2S 中将 DataContext 对象的 ObjectTrackingEnabled 属性设置成 false 即可。在 EF 中禁用对象跟踪的功能要麻烦一点,它需要在每个实体中都添加下面的代码 :

context.Customers.MergeOption = MergeOption.NoTracking;

关闭对象状态跟踪功能之后,为了数据安全,通过 context 向数据库中提交更新的功能也同时被禁用。

(P316)

如果要从数据库中得到最新的数据,必须定义一个新的 context 对象,将旧的实体类传给这个对象,然后调用 Refresh 方法,这样,最新的数据就会被更新到实体类中。

在一个多层次的系统中,不能在系统的中间层定义一个静态的 DataContext 或者 ObjectContext 实例完成所有的数据库查询操作,因为 context 对象不能保证线程安全。正确的做法是在中间层的方法中,为每个请求的客户创建一个 context ,这样做的好处是可以减轻数据库的负担,因为维护和更新实体的任务被多个 context 对象分担。对于数据库来说,更新操作会通过多个事务执行完成,这显然比一个很大的事务要高效很多。

使用实体类生成工具还有一个特点,当表之间有关联关系的时候,我们可以直接使用关联表中的属性,实体类自动完成了关联的字段和关联表的映射。

(P317)

L2S 查询中 [Association] 标签的作用是提供生成 SQL 语句所需的信息;而 EF 中的 [EdmRelationshipNavigationProperty] 标签的作用是告诉 EF 要到 EDM 中去查找两个表的关联关系。

L2S 和 EF 的查询方式仍然是延迟加载,在 L2S 查询中,真正的查询会在遍历结果集时进行,而 EF 的查询则是在显式地调用了 Load 方法之后才会执行。

(P318)

可以通过设置下面这个属性使 EF 和 L2S 以相同的方式返回 EntityCollection 和 EntityReferences :

context.ContextOptions.DeferredLoadingEnabled = true;

(P319)

DataLoadOptions 类是 L2S 中一个特有的类,它有两个作用 :

  1. 它允许我们为 EntitySet 所关联的类指定一个筛选条件;

  2. 它可以强制加载特定的 EntitySets ,这样可以减少整个数据查询的次数;

(P320)

L2S 和 EF 都会跟踪实体类的状态,如果实体中的数据有所改变,我们可以将这些改变更新回数据库,更新的方式是调用 DataContext 类中的 SubmitChanges 方法,在 EF 中则是使用 ObjectContext 对象的 SaveChanges 方法。

除此之外,L2S 的 Table 类还提供了 InsertOnSubmit 和 DeleteOnSubmit 方法用于插入和删除数据表中的记录;而 EF 的 ObjectSet 类提供了 AddObject 和 DeleteObject 方法来完成相同的功能。

(P321)

SubmitChanges / SaveChanges 会记录 context 创建以来实体类中所有数据变化,然后将这些变化更新回数据库中,在更新的过程中,需要创建一个 TransactionScope 对象来帮助完成,以免更新过程中造成的错误数据。

也可以使用 EntitySet / EntityCollection 类中的 Add 方法向数据库中添加新的记录。在调用了 SubmitChanges 或者 SaveChanges 方法之后,实体中新添加的记录的外键信息会被自动取出来。

为新添加的实体对象添加主键值比较繁琐,因为我们需要保证这个主键是唯一的,解决办法是可以在数据库中定义自增类型的主键,或者使用 Guid 作为主键。

L2S 能够识别它们的关联关系并赋值是因为实体类中有这样的关联定义,而 EF 之所以可以自动识别关联并赋值是因为 EDM 中存储了这两种实体间的关联关系以及关联字段。

(P322)

当从 EntitySet / EntityCollection 对象中移除一行后,它的外键的值会自动设置成 null 。

L2S 和 EF 的 API 对比 :

  1. 各种操作的基础类 : DataContext (L2S) - ObjectContext (EF);

  2. 从数据库中取出指定类型的所有记录 : GetTable (L2S) - CreateObjectSet (EF);

  3. 方法的返回类型 : Table (L2S) - ObjectSet (EF);

  4. 将实体中的属性值的变化 (添加、删除等) 更新回数据库 : SubmitChanges (L2S) - SaveChanges (EF);

  5. 使用 conetext 更新的方式向数据库中添加新的记录 : InsertOnSubmit (L2S) - AddObject (EF);

  6. 使用 context 更新的方式删除记录 : DeleteOnSubmit (L2S) - DeleteObject (EF);

  7. 关联表中用于存放多条关联记录的属性 : EntitySet (L2S) - EntityCollection (EF);

  8. 关联表中用于存放单条关联记录的属性 : EntityRef (L2S) - EntityReference (EF);

  9. 加载关联属性的默认方式 : Lazy (L2S) - Explicit (EF);

  10. 构建立即加载的查询方式 : DataLoadOptions (L2S) - Include() (EF);

(P325)

一个查询表达式树是由一个微型的 DOM (Document Object Model ,文档对象模型) 来描述的。这个 DOM 中每个节点都代表了 System.Linq.Expressions 命名空间中的一个类型。

(P326)

Expression 的基类是 LambdaExpression ,LambdaExpression 是 Lambda 表达式树中所有节点的基类型,所有的节点类型都可以转换成这种基类型,因此保证了表达式树中节点的类型一致性。

Lambda 表达式需要接收参数,而普通的表达式则没有参数。

【第09章】

(P329)

标准查询运算符可以分为三类 :

  1. 输入是集合,输出是集合;

  2. 输入是集合,输出是单个元素或者标量值;

  3. 没有输入,输出是集合 (生成方法) ;

(P330)

[集合] --> [集合]

  1. 筛选运算符 —— 返回原始序列的一个子集。使用的运算符有 : Where 、 Take 、 TakeWhile 、 Skip 、 SkipWhile 、 Distinct ;

  2. 映射运算符 —— 这种运算符可以按照 Lambda 表达式指定的形式,将每个输入元素转换成输出元素。 SelectMany 用于查询嵌套的集合;在 LINQ to SQL 和 EF 中 Select 和 SelectMany 运算符可以执行内连接、左外连接、交叉连接以及非等连接等各种连接查询。使用的运算符有 : Select 、 SelectMany ;

  3. 连接运算符 —— 用于将两个集合连接之后,取得符合条件的元素。连接运算符支持内连接和左外连接,非常适合对本地集合的查询。使用运算符有 : Join 、 GroupJoin 、 Zip ;

  4. 排序运算符 —— 返回一个经过重新排序的集合,使用的运算符有 : OrderBy 、 ThenBy 、 Reverse ;

(P331)

  1. 分组运算符 —— 将一个集合按照某种条件分成几个不同的子集。使用的运算符有 : GroupBy ;

  2. 集合运算符 —— 主要用于对两个相同类型集合的操作,可以返回两个集合中共有的元素、不同的元素或者两个集合的所有元素。使用的运算符有 : Concat 、 Unoin 、 Intersect 、 Except ;

  3. 转换方法 Import —— 这种方法包括 OfType 、 Cast ;

  4. 转换方法 Export —— 将 IEnumerable 类型的集合转换成一个数组、清单、字典、检索或者序列,这种方法包括 : ToArray 、 ToList 、 ToDictionary 、 ToLookup 、 AsEnumerable 、 AsQueryable ;

[集合] --> [单个元素或标量值]

  1. 元素运算符 —— 从集合中取出单个特定的元素,使用的运算符有 : First 、 FirstOrDefault 、 Last 、 LastOrDefault 、 Single 、 SingleOrDefault 、 ElementAt 、 ElementAtOrDefault 、 DefaultIfEmpty ;

  2. 聚合方法 —— 对集合中的元素进行某种计算,然后返回一个标量值 (通常是一个数字) 。使用的运算符有 : Aggregate 、 Average 、 Count 、 LongCount 、 Sum 、 Max 、 Min ;

  3. 数量词 —— 一种返回 true 或者 false 的聚合方法,使用的运算符有 : All 、 Any 、 Contains 、 SequenceEqual ;

(P332)

[空] --> [集合]

第三种查询运算符不需要输入但可以输出一个集合。

生成方法 —— 生成一个简单的集合,使用的方法有 : Empty 、 Range 、 Repeat ;

(P333)

经过各种方法的筛选,最终得到的序列中的元素只能比原始序列少或者相等,绝不可能比原始序列还多。在筛选过程中,集合中的元素类型及元素值是不会改变的,和输入时始终保持一致。

如果和 let 语句配合使用的话,Where 语句可以在一个查询中出现多次。

(P334)

标准的 C# 变量作用域规则同样适用于 LINQ 查询。也就是说,在使用一个查询变量前,必须先声明,否则不能使用。

Where 判断选择性地接受一个 int 型的第二参数。这个参数用于指定输入序列中特定位置上的元素,在查询中可以使用这个数值进行元素的筛选。

下面几个关键字如果用在 string 类型的查询中将会被转换成 SQL 中的 LIKE 关键字 : Contains 、 StartsWith 、 EndsWith 。

Contains 关键字仅用于本地集合的比较。如果想要比较两个不同列的数据,则需要使用 SqlMethods.Like 方法。

SqlMethods.Like 也可以进行更复杂的比较操作。

在 LINQ to SQL 和 EF 中,可以使用 COntains 方法来查询一个本地集合。

如果本地集合是一个对象集合或其他非数值类型的集合,LINQ to SQL 或者 EF ,也可能把 Contains 关键字翻译成一个 EXISTS 子查询。

(P335)

Take 返回集合的前 n 个元素,并且放弃其余元素;Skip 则是跳过前 n 个元素,并且返回其余元素。

在 SQL Server 2005 中,LINQ to SQL 和 EF 中的 Take 和 Skip 运算符会被翻译成 ROW_NUMBER 方法,而在更早的 SQL Server 数据库版本中则会被翻译成 Top n 查询。

TakeWhile 运算符会遍历输入集合,然后输出每个元素,直到给定的判断为 false 时停止输出,并忽略剩余的元素。

SkipWhile 运算符会遍历输入集合,忽略判断条件为真之前的每个元素,直到给定的判断为 false 时输出剩余的元素。

在 SQL 中没有与 TakeWhile 和 SkipWhile 对应的查询方式,如果在 LINQ-to-db 查询中使用,将会导致一个运行时错误。

(P336)

Distinct 的作用是返回一个没有重复元素的序列,它会删除输入序列中的重复元素。在这里,判断两个元素是否重复的规则是可以自定义的,如果没有自定义,那么就使用默认的判断规则。

因为 string 实现了 IEnumerable 接口,所以我们可以在一个字符串上直接使用 LINQ 方法。

在查询一个数据库时, Select 和 SelectMany 是最常用的连接操作方法;对于本地查询来说,使用 Join 和 Group 的效率最好。

在使用 Select 时,通常不会减少序列中的元素数量。每个元素可以被转换成需要的形式,并且这个形式需要通过 Lambda 表达式来定义。

(P337)

在条件查询中,一般不需要对查询结果进行映射,之所以要使用 select 运算符,是为了满足 LINQ 查询必须以 select 或者 group 语句结尾的语法要求。

Select 表达式还接受一个整型的可选参数,这个参数实际上是一个索引,使用它可以得到输入序列中元素的位置。需要注意的是,这种参数只能在本地查询中使用。

可以在 Select 语句中再嵌套 Select 子句来构成嵌套查询,这种嵌套查询的结果是一个多层次的对象集合。

(P338)

内部的子查询总是针对外部查询的某个元素进行。

Select 内部的子查询可以将一个多层次的对象映射成另一个多层次的对象,也可以将一组关联的单层次对象映射成一个多层次的对象模型。

在对本地集合的查询中,如果 Select 语句中包含 Select 子查询,那么整个查询是双重的延迟加载。

子查询的映射在 LINQ to SQL 和 EF 中都可以实现,并且可以用来实现 SQL 的连接功能。

(P339)

我们将查询结果映射到匿名类中,这种映射方式适用于查询过程中暂存中间结果集的情况,但是当需要将结果返回给客户端使用的时候,这种映射方式就不能满足需求了,因为匿名类型只能在一个方法内作为本地变量存在。

(P341)

SelectMany 可以将两个集合组成一个更大的集合。

(P342)

在分层次的数据查询中,使用 SelectMany 和 Select 得到的结果是相同的,但是在查询单层次的数据源 (如数组) 的时候,Select 要完成同样的任务,就需要使用嵌套循环了。

SelectMany 的好处就是在于,无论输入集合是什么类型的,它输出的集合肯定是一个数组类型的二维集合,结果集的数据不会有层次关系。

在查询表达式语法中,from 运算符有两个作用,在查询一开始的 from 的作用都是引入查询集合和范围变量;其他任何位置再出现 from 子句,编译器都会将其翻译成 SelectMany 。

(P343)

在需要用到外部变量的情况下,选择使用查询表达式语法是最佳选择。因为在这种情况中,这种语法不仅便于书写,而且表达方式也更接近查询逻辑。

(P344)

在 LINQ to SQL 和 EF 中, SelectMany 可以实现交叉连接、不等连接、内连接以及左外连接。

(P345)

在标准 SQL 中,所有的连接都要通过 join 关键字实现。

在 Entity Framework 的实体类中,并不会直接存储一个外键值,而是存储外键所关联对象的集合,所以当需要使用外键所关联的数据时,直接使用实体类属性中附带的数据集合即可,不用像 LINQ to SQL 查询中那样手动地进行连接来得到外键集合中的数据。

对于本地集合的查询中,为了提高执行效率,应该尽量先筛选,再连接。

如果有需要的话,可以引入新的表来进行连接,查询时的连接并不限于两个表之间,多个表也可以进行。在 LINQ 中,可以通过添加一个 from 子句来实现。

(P347)

正确的做法是在 DefaultIfEmpty 运算符之前使用 Where 语句。

Join 和 GroupJoin 的作用是连接两个集合进行查询,然后返回一个查询结果集。他们的不同点在于,Join 返回的是非嵌套结构的数据集合,而 GroupJoin 返回的则是嵌套结构的数据集合。

Join 和 GroupJoin 的长处在于对本地集合的查询,也就是对内存中数据的查询效率比较高。它们的缺点是目前只支持内连接和左外连接,并且连接条件必须是相等连接。需要用到交叉连接或者非等值连接时,就只能选择 Select 或者 SelectMany 运算符。在 LINQ to SQL 或者 EF 查询中, Join 和 GroupJoin 运算符在功能上与 Select 和 SelectMany 是没有什么区别的。

(P352)

当 into 关键字出现在 join 后面的时候,编译器会将 into 关键字翻译成 GroupJoin 来执行。而当 into 出现在 Select 或者 Group 子句之后时,则翻译成扩展现有的查询。虽然都是 into 关键字,但是出现在不同的地方,差别非常大。有一点它们是相同的,into 关键字总是引入一个新的变量。

GroupJoin 的返回结果实际上是集合的集合,也就是一个集合中的元素还是集合。

(P355)

Zip 是在 .NET Framework 4.0 中新加入的一个运算符,它可以同时枚举两个集合中的元素 (就像拉链的两边一样) ,返回的集合是经过处理的元素对。

两个集合中不能配对的元素会直接被忽略。需要注意的是,Zip 运算符只能用于本地集合的查询,它不支持对数据库的查询。

经过排序的集合中的元素值和未排序之前是相同的,只是元素的顺序不同。

(P356)

OrderBy 可以按照指定的方式对集合中的元素进行排序,具体的排序方式可以在 KeySelector 表达式中定义。

如果通过 OrderBy 按照指定顺序进行排序后,集合中的元素相对顺序仍无法确定时,可以使用 ThenBy 。

ThenBy 关键字的作用是在前一次排序的基础上再进行一次排序。在一个查询中,可以使用任意多个 ThenBy 关键字。

(P357)

LINQ 中还提供了 OrderByDescending 和 ThenByDescending 关键字,这两个关键字也是用于完成对集合的排序功能,它们的功能和 OrderBy / ThenBy 相同,用法也一样,只是它们排序后的集合中的元素是按指定字段的降序排序。

在对本地集合的查询中,LINQ 会根据默认的 IComparable 接口中的算法对集合中的元素进行排序。如果不想使用默认的排序方式,可以自己实现一个 IComparable 对象,然后将这个对象传递给查询 LINQ 。

在查询表达式语法中我们没有办法将一个 IComparable 对象传递给查询语句,也就不能进行自定义的查询。

在使用了排序操作的查询中,排序运算符会将集合转换成 IEnumerable 类型的一个特殊子类。具体来说,对 Enumerable 类型的集合查询时,返回 IOrderedEnumerable 类型的集合;在对 Queryable 类型的集合查询时,返回 IOrderedQueryable 类型的集合。这两种子类型是为排序专门设计的,在它们上面可以直接使用 ThenBy 运算符来进行多次排序。

(P358)

在对远程数据源的查询中,需要用 AsQueryable 代替 AsEnumerable 。

(P359)

GroupBy 可以将一个非嵌套的集合按某种条件分组,然后将得到的分组结果以组为单位封装到一个集合中。

Enumerable.GroupBy 的内部实现是,首先将集合中的所有元素按照键值的关系存储到一个临时的字典类型的集合中。然后再将这个临时集合中的所有分组返回给调用者。这里一个分组就是一个键和它所对应的一个小集合。

默认情况下,分组之后的元素不会对原始元素做任何处理,如果需要在分组过程中对元素做某些处理的话,可以给元素选择器指定一个参数。

(P360)

GroupBy 只对集合进行分组,并不做任何排序操作,如果想要对集合进行排序的话,需要使用额外的 OrderBy 关键字。

在查询表达式语法中,GroupBy 可以使用下面这个格式来创建 : group 元素表达式 by 键表达式 。

和其他的查询一样,当查询语句中出现了 select 或者 group 的时候,整个查询就结束了,如果不想让查询就此结束,那么就需要扩展整个查询,可以使用 into 关键字。

在 group by 查询中,经常需要扩展查询语句,因为需要对分组后的集合进一步进行处理。

在 LINQ 中, group by 后面跟着 where 查询相当于 SQL 中的 HAVING 关键字。这个 where 所作用的对象是整个集合或者集合中的每个分组,而不是单个元素。

分组操作同样适用于对数据库的查询。如果是在 EF 中,在使用了关联属性的情况下,分组操作并不像在 SQL 中那样常用。

(P361)

LINQ 中的分组功能对 SQL 中的 “GROUP BY” 进行了很大的扩展,可以认为 LINQ 中的分组是 SQL 中分组功能的一个超集。

和传统 SQL 查询不同点是,在 LINQ 中不需要对分组或者排序子句中的变量进行映射。

当需要使用集合中多个键来进行分组时,可以使用匿名类型将这几个键封装到一起。

(P362)

Concat 运算符的作用是合并两个集合,合并方式是将第一个集合中所有元素放置到结果集中,然后再将第二个集合中的元素放在第一个结果集的后面,然后返回结果集。Union 执行的也是这种合并操作,但是它最后会将结果集中重复的元素去除,以保证结果集中每个元素都是唯一的。

当对两个不同类型但基类型却相同的序列执行合并时,需要显式地指定这两个集合的类型以及合并之后的集合类型。

Intersect 运算符用于取出两个集合中元素的交集。Except 用于取出只出现在第一个集合中的元素,如果某个元素在两个集合中都存在,那么这个元素就不会包含在结果中。

Enumerable.Except 的内部实现方式是,首先将第一个集合中的所有元素加载到一个字典集合中,然后再对比第二个集合中的元素,如果字典中的某个元素在第二个集合中出现了,那么就将这个元素从字典中移除。

(P363)

从根本上讲,LINQ 处理的是 IEnumerable 类型的集合,之所以现在众多的集合类型都可以使用 LINQ 进行处理,是因为编译器内部可以将其他类型的序列转换成 IEnumerable 类型的。

OfType 和 Cast 可以将非 IEnumerable 类型的集合转换成 IEnumerable 类型的集合。

Cast 和 OfType 运算符的唯一不同就是它们遇到不相容类型时的处理方式 : Cast 会抛出异常,而 OfType 则会忽略这个类型不相容的元素。

元素相容的规则与 C# 的 is 运算符完全相同,因此只能考虑引用转换和拆箱转换。

Cast 运算符的内部实现与 OfType 完全相同,只是省略了类型检查那行代码。

OfType 和 Cast 的另一个重要功能是 : 按类型从集合中取出元素。

(P365)

ToArray 和 ToList 可以分别将集合转换成数组和泛型集合。这两个运算符也会强制 LINQ 查询语句立即执行,也就是说当整个查询是延迟加载的时候,一旦遇到 ToArray 或者 ToList ,整个语句会被立即执行。

ToDictionary 方法也会强制查询语句立即执行,然后将查询结果放在一个 Dictionary 类型的集合中。 ToDictionary 方法中的键选择器必须为每个元素提供一个唯一的键,也就是说不同元素的键是不能重复的,否则在查询的时候系统会抛出异常。而 Tolookup 方法的要求则不同,它允许多个元素共用相同的键。

AsEnumerable 将一个其他类型的集合转换成 IEnumerable 类型,这样可以强制编译器使用 Enumerable 类中的方法来解析查询中的运算符。

AsQueryable 方法则会将一个其他类型的集合转换成 IQueryable 类型的集合,前提是被转换的集合实现了 IQueryable 接口。否则 IQueryable 会实例化一个对象,然后存储在本地数组外面,看起来是可以调用 IQueryable 中的方法,但实际上这些方法并没有真正的意义。

(P366)

所有以 "OrDefault" 结尾的方法有一个共同点,那就是当集合为空或者集合中没有符合要求的元素时,这些方法不抛出异常,而是返回一个默认类型的值 default(TSource) 。

对于引用类型的元素来说 default(TSource) 是 null ,而对于值类型的元素来说,这个默认值通常是 0 。

为了避免出现异常,在使用 Single 运算符时必须保证集合中有且仅有一个元素;而 SingleOrDefault 运算符则要求集合中有一个或零个元素。

Single 是所有元素运算符中要求最多的,而 FirstOrDefault 和 LastOrDefault 则对集合中的元素没有什么要求。

(P367)

在 LINQ to SQL 和 EF 中, Single 运算符通常应用于使用主键到数据库中查找特定的单个元素。

ElementAt 运算符可以根据指定的下标取出集合中的元素。

Enumerable.ElementAt 的实现方式是,如果它所查询的集合实现了 IList 接口,那么在取元素的时候,就使用 IList 中的索引器。否则,就使用自定义的循环方法,在循环中依次向后查找元素,循环 n 次之后,返回下一个元素。ElementAt 运算符不能在 LINQ to SQL 和 EF 中使用。

DefaultIfEmpty 可以将一个空的集合转换成 null 或者 default() 类型。这个运算符一般用于定义外连接查询。

(P368)

Count 运算符的作用是返回集合中元素的个数。

Enumerable.Count 方法的内部实现方式如下 : 首先判断输入集合有没有实现 ICollection 接口,如果实现了,那么它的就调用 ICollection.Count 方法得到元素个数。否则就遍历整个集合中的元素,统计出元素的个数,然后返回。

还可以为 Count 这个方法添加一个筛选条件。

LongCount 运算符的作用和 Count 是相同的,只是它的返回值类型是 int64 ,也就是它能用于大数据量的统计, int64 能统计大概 20 亿个元素的集合。

Min 和 Max 返回集合中最小和最大的元素。

如果集合没有实现 IComparable 接口的话,那么我们就必须为这两个运算符提供选择器。

选择器表达式不仅定义了元素的比较方式,还定义了最后的结果集的类型。

(P369)

Sum 和 Average 的返回值类型是有限的,它们内置了以下几种固定的返回值类型 : int 、 long 、 float 、 double 、 decimal 以及这几种类型的可空类型。这里返回值都是值类型,也就是,Sum 和 Average 的预期结果都是数字。而 Min 和 Max 则会返回所有实现了 IComparable 接口的类型。

更进一步讲, Average 值返回两种类型 : decimal 和 double 。

Average 为了避免查询过程中数值的精度损失,会自动将返回值类型的精度升高一级。

(P370)

Aggregate 运算符我们可以自定义聚合方法,这个运算符只能用于本地集合的查询中,不支持 LINQ to SQL 和 EF 。这个运算符的具体功能要根据它在特定情况下的定义来看。

Aggregate 运算符的第一个参数是一个种子,用于指示统计结果的初始值是多少;第二个参数是一个表达式,用于更新统计结果,并将统计结果赋值给新的变量;第三个参数是可选的,用于将统计结果映射成期望的形式。

Aggregate 运算符最大的问题是,它实现的功能通过 foreach 语句也可以实现,而且 foreach 语句的语法更清晰明了。 Aggregate 的主要用处在于处理比较大或者比较复杂的聚合操作。

(P372)

Contains 关键字接收一个 TSource 类型的参数;而 Any 的参数则定义了筛选条件,这个参数是可选的。

Any 关键字对集合中元素的要求低一点,只要集合中有一个元素符合要求,就返回 true 。

Any 包含了 Contains 关键字的所有功能。

如果在使用 Any 关键字的时候不带参数,那么只要集合中有一个元素符合要求,就返回 true 。

Any 关键字在子查询中使用特别广泛,尤其是在对数据库的查询中。

当集合中的元素都符合给定的条件时, All 运算符返回 true 。

SequenceEqual 用于比较两个集合中的元素是否相同,如果相同则返回 true 。它的筛选条件要求元素个数相同、元素内容相同而且元素在集合中的顺序也必须是相同的。

(P373)

Empty 、 Repeat 和 Range 都是静态的非扩展方法,它们只能用于本地集合中。

Empty 用于创建一个空的集合,它需要接收一个用于标识集合类型的参数。

和 “??” 运算符配合使用的话,Empty 运算符可以实现 DefaultEmpty 的功能。

Range 和 Repeat 运算符只能使用在整型集合中。

Range 接收两个参数,分别用于指示起始元素的下标和查询元素的个数。

Repeat 接收两个参数,第一个参数是要创建的元素,第二个参数用于指示重复元素的个数。

【第10章】

(P375)

在 .NET Framework 中提供了很多用于处理 XML 数据的 API 。从 .NET Framework 3.5 之后,LINQ to XML 成为处理通用 XML 文档的首选工具。它提供了一个轻量的集成了 LINQ 友好的 XML 文档对象模型,当然还有相应的查询运算符。在大多数情况下,它完全可以替代之前 W3C 标准的 DOM 模型 (又称为 XmlDocument) 。

LINQ to XML 中 DOM 的设计非常完善且高效。即使没有 LINQ ,单纯的 LINQ to XML 中 DOM 对底层 XmlReader 和 XmlWriter 类也进行了很好的封装,可以通过它来更简单地使用这两个类中的方法。

LINQ to XML 中所有的类型定义都包含在 System.Xml.Linq 命名空间中。

所有 XML 文件一样,在文件开始都是声明部分,然后是根元素。

属性由两部分组成 : 属性名和属性值。

(P376)

声明、元素、属性、值和文本内容这些结构都可以用类来表示。如果这种类有很多属性来存储子内容,我们可以用一个对象树来完全描述文档。这个树状结构就是文档对象模型 (Document Object Model) ,简称 DOM 。

LINQ to XML 由两部分组成 :

  1. 一个 XML DOM ,我们称之为 X-DOM ;

  2. 约 10 个用于查询的运算符;

可以想象, X-DOM 是由诸如 XDocument 、 XElement 、 XAttribute 等类组成的。有意思的是, X-DOM 类并没有和 LINQ 绑定在一起,也就是说,即使不使用 LINQ 查询,也可以加载、更新或存储 X-DOM 。

X-DOM 是集成了 LINQ 的模型 :

  1. X-DOM 中的一些方法可以返回 IEnumerable 类型的集合,使 LINQ 查询变得非常方便;

  2. X-DOM 的构造方法更加灵活,可以通过 LINQ 将数据直接映射成 X-DOM 树;

XObject 是整个继承结构的根, XElement 和 XDocument 则是平行结构的根。

XObject 是所有 X-DOM 内容的抽象基类。在这个类型中定义了一个指向 Parent 元素的链接,这样就可以确定节点之间的层次关系。另外这个类中还有一个 XDocument 类型的对象可供使用。

除了属性之外, XNode 是其他大部分 X-DOM 内容的基类。 XNode 的一个重要特性是它可以被有顺序地存放在一个混合类型的 XNodes 集合中。

XAttribute 对象的存储方式 —— 多个 XAttribute 对象必须成对存放。

(P377)

虽然 XNode 可以访问它的父节点 XElement ,但是它却对自己的子节点一无所知,因为管理子节点的工作是由子类 XContainer 来做的。 XContainer 中定义了一系列成员和方法来管理它的子类,并且是 XElement 和 XDocument 的抽象基类。

除了 Name 和 Value 之外, XElement 还定义了其他的成员来管理自己的属性,在绝大多数情况下, XElement 会包含一个 XText 类型的子节点, XElement 的 Value 属性同时包含了存取这个 XText 节点的 get 和 set 操作,这样可以更方便地设置节点值。由于 Value 属性的存在,我们可以不必直接使用 XText 对象,这使得对节点的赋值操作变得非常简单。

(P378)

XML 树的根节点是 XDocument 对象。更准确地说,它封装了根 XElement ,添加了 XDeclaration 以及一些根节点需要执行的指令。与 W3C 标准的 DOM 有所不同,即使没有创建 XDocument 也可以加载、操作和保存 X-DOM 。这种对 XDocument 的不依赖性使得我们可以很容易将一个节点子树移到另一个 X-DOM 层次结构中。

XElement 和 XDocument 都提供了静态 Load 和 Parse 方法,使用这两个方法,开发者可以根据已有的数据创建 X-DOM :

  1. Load 可以根据文件、 URI 、 Stream 、 TextReader 或者 XmlReader 等构建 X-DOM ;

  2. Parse 可以根据字符串构建 X-DOM ;

(P379)

在节点上调用 ToString 方法可将这个节点中的内容转换成 XML 字符串,默认情况下,转换后的 XML 字符串是经过格式化的,即使用换行和空格将 XML 字符串按层次结构逐行输出,且使用正确的缩进格式。如果不想让 ToString 方法格式化 XML ,那么可以指定 SaveOptions.DisableFormatting 参数。

XElement 和 XDocument 还分别提供了 Save 方法,使用这个方法可将 X-DOM 写入文件、 Stream 、 TextWriter 或者 XmlWriter 中。如果选择将 X-DOM 写入到一个文件中,则会自动写入 XML 声明部分。另外, XNode 类还提供了一个 WriteTo 方法,这个方法只能向 XmlWriter 中写入数据。

创建 X-DOM 树常用的方法是手动实例化多个节点,然后通过 XContainer 的 Add 方法将所有节点拼装成 XML 树,而不是通过 Load 或者 Parse 方法。

要构建 XElement 和 XAttribute ,只需提供属性名和属性值。

构建 XElement 时,属性值不是必须的,可以只提供一个元素名并在其后添加内容。

注意,当需要为一个对象添加属性值时,只需设置一个字符串即可,不用显式创建并添加 XText 子节点, X-DOM 的内部机制会自动完成这个操作,这使得活加属性值变得更加容易。

(P380)

X-DOM 还支持另一种实例化方式 : 函数型构建 (源于函数式编程) 。

这种构建方式有两个优点 : 第一,代码可以体现出 XML 的结构;第二,这种表达式可以包含在 LINQ 查询的 select 子句中。

之所以以函数型构建的方式定义 XML 文件,是因为 XElement (和 XDocument) 的构造方法都可重载,以接受 params 对象数组 : public XElement(XName name, params object[] content) 。

XContainer 类的 Add 方法同样也接收这种类型的参数 : public void Add(params object[] content) 。

所以,我们可以在构建或添加 X-DOM 时指定任意数目、任意类型的子对象。这是因为任何内容都是合法的。

XContainer 类内部的解析方式 :

  1. 如果传入的对象是 null ,那么就忽略这个节点;

  2. 如果传入对象是以 XNode 或者 XStreamingElement 作为基类,那么就将这个对象添加为 Node 对象,放到 Nodes 集合中;

  3. 如果传入对象是 XAttribute ,那么就将这个对象作为 Attribute 集合来处理;

  4. 如果对象是 string ,那么这个对象会被封装成一个 XText 节点,然后添加到 Nodes 集合中;

  5. 如果对象实现了 IEnumerable 接口,则对其进行枚举,每个元素都按照上面的规则来处理;

  6. 如果某个类型不符合上述任一条件,那么这个对象会被转换成 string ,然后被封装在 XText 节点上,并添加到 Nodes 集合中;

上述所有情况最终都是 : Nodes 或 Attributes 。另外,所有对象都是有效的,因为最终肯定可以调用它的 ToString 方法并将其作为 XText 节点来处理。

实际上, X-DOM 内部在处理 string 类型的对象时,会自动执行一些优化操作,也就是简单地将文本内容存放在字符串中。直到 XContainer 上调用 Nodes 方法时,才会生成实际的 XText 节点。

(P382)

与在 XML 中一样, X-DOM 中的元素和属性名是区分大小写的。

使用 FirstNode 与 LastNode 可以直接访问第一个或最后一个子节点;Nodes 返回所有的子节点并形成一个序列。这三个函数只用于直系的子节点。

(P383)

Elements() 方法返回类型为 XElement 的子节点。

Elements() 方法还可以只返回指定名字的元素。

(P384)

Element() 方法返回匹配给定名称的第一个元素。Element 对于简单的导航是非常有用的。

Element 的作用相当于调用 Elements() ,然后再应用 LINQ 的 FirstOrDefault 查询运算符给定一个名称作为匹配断言。如果没有找到所请求的元素,则 Element 返回 null 。

XContainer 还定义了 Descendants 和 DescendantNodes 方法,它们递归地返回子元素或子节点。

Descendants 接受一个可选的元素名。

(P385)

所有的 XNodes 都包含一个 Parent 属性,另外还有一个 AncestorXXX 方法用来找到特定的父节点。一个父节点永远是一个 XElement 。

Ancestors 返回一个序列,其第一个元素是 Parent ,下一个元素则是 Parent.Parent ,依次类推,直到根元素。

还可以使用 LINQ 查询 AncestorsAndSelf().Last() 来取得根元素。

另外一种方法是调用 Document.Root ,但只有存在 XDocument 时才能执行。

使用 PreviousNode 和 NextNode (以及 FirstNode / LastNode) 方法查找节点时,相当于从一个链表中遍历所有节点。事实上 XML 中节点的存储结构确实是链表。

(P386)

XNode 存储在一个单向链表中,所以 PreviousNode 并不是当前元素的前序元素。

Attributes 方法接受一个名称并返回包含 0 或 1 个元素的序列;在 XML 中,元素不能包含重复的属性名。

可以使用下面这几种方式来更新 XML 中的元素和属性 :

  1. 调用 SetValue 方法或者重新给 Value 属性赋值;

  2. 调用 SetElementValue 或 SetAttributeValue 方法;

  3. 调用某个 RemoveXXX 方法;

  4. 调用某个 AddXXX 或 ReplaceXXX 方法指定更新的内容;

也可以为 XElement 对象重新设置 Name 属性。

使用 SetValue 方法可以使用简单的值替换元素或者属性中原来的值。通过 Value 属性赋值会达到相同的效果,但只能使用 string 类型的数据。

调用 SetValue 方法 (或者为 Value 重新赋值) 的结果就是它替换了所有的子节点。

(P387)

最好的两个方法是 : SetElementValue 和 SetAttributeValue 。它们提供了一种非常便捷的方式来实例化 XElement 或 XAttribute 对象,然后调用父节点的 Add 方法,将新节点加入到父节点下面,从而替换相同名称的任何现有元素或属性。

Add 方法将一个子节点添加到一个元素或文档中。AddFirst 也一样,但它将节点插入集合的开头而不是结尾。

我们也可以通过调用 RemoveNodes 或 RemoveAttributes 将所有的子节点或属性全部删除。 RemoveAll 相当于同时调用了这两个方法。

ReplaceXXX 方法等价于调用 Removing ,然后再调用 Adding 。它们拥有输入参数的快照,因此 e.ReplaceNodes(e.Nodes) 可以正常进行。

AddBeforeSelf 、 AddAfterSelf 、 Remove 和 ReplaceWith 方法不能操作一个节点的子节点。它们只能操作当前节点所在的集合。这就要求当前节点都有父元素,否则在使用这些方法时就会抛出异常。此时 AddBeforeSelf 和 AddAfterSelf 方法非常有用,这两个方法可以将一个新节点插入到 XML 中的任意位置。

(P388)

Remove 方法可以将当前节点从它的父节点中移除。ReplaceWith 方法实现同样的操作,只是它在移除旧节点之后还会在同一位置插入其他内容。

通过 System.Xml.Linq 中的扩展方法,我们可以使用 Remove 方法整组地移除节点或者属性。

(P389)

Remove 方法的内部实现机制是这样的 : 首先将所有匹配的元素读取到一个临时列表中,然后枚举该临时列表并执行删除操作。这避免了在删除的同时进行查询操作所引起的错误。

XElement 和 XAttribute 都有一个 string 类型的 Value 属性,如果一个元素有 XText 类型的子节点,那么 XElement 的 Value 属性就相当于访问此节点的快捷方式,对于 XAttribute 的 Value 属性就是指属性值。

有两种方式可以设置 Value 属性值 : 调用 SetValue 方法或者直接给 Value 属性赋值。 SetValue 方法要复杂一些,因为它不仅可以接收 string 类型的参数,也可以设置其他简单的数据类型。

(P390)

由于有了 Value 的值,你可能会好奇什么时候才需要直接和 XText 节点打交道?答案是 : 当拥有混合内容时。

(P391)

向 XElement 添加简单的内容时, X-DOM 会将新添加的内容附加到现有的 XText 节点后面,而不会新建一个 XText 节点。

如果显式地指定创建新的 XText 节点,最终会得到多个子节点。

XDocument 封装了根节点 XElement ,可以添加 XDeclaration 、处理指令、说明文档类型以及根级别的注释。

XDocument 是可选的,并且能够被忽略或者省略,这点与 W3C DOM 不同。

XDocument 提供了和 XElement 相同的构造方法。另外由于它也继承了 XContainer 类,所以也支持 AddXXX 、 RemoveXXX 和 ReplaceXXX 等方法。但与 XElement 不同,一个 XDocument 节点可添加的内容是有限的 :

  1. 一个 XElement 对象 (根节点) ;

  2. 一个 XDeclaration 对象;

  3. 一个 XDocumentType 对象 (引用一个 DTD) ;

  4. 任意数目的 XProcessingInstruction 对象;

  5. 任意数目的 XComment 对象;

(P392)

对于 XDocument 来说,只有根 XElement 对象是必须的。 XDeclaration 是可选的,如果省略,在序列化的过程中会应用默认设置。

(P393)

XDocument 有一个 Root 属性,这个属性是取得当前 XDocument 对象单个 XElement 的快捷方式。其反向的链接是由 XObject 的 Document 属性提供的,并且可以应用于树中的所有对象。

XDocument 对象的子节点是没有 Parent 信息的。

XDeclaration 并不是 XNode 类型的,因此它不会出现在文档的 Nodes 集合中,而注释、处理指令和根元素等都会出现在 Nodes 集合中。

XDeclaration 对象专门存放在一个 Declaration 属性中。

XML 声明是为了保证整个文件被 XML 阅读器正确解析并理解。

XElement 和 XDocument 都遵循下面这些 XML 声明的规则 :

  1. 在一个文件名上调用 Save 方法时,总是自动写入 XML 声明;

  2. 在 XmlWriter 对象上调用 Save 方法时,除非 XmlWriter 特别指出,都则都会写入 XML 声明;

  3. ToString 方法从来都不返回 XML 声明;

如果不想让 XmlWriter 创建 XML 声明,可以在构建 XmlWriter 对象时,通过设置 XmlWriterSettings 对象的 OmitXmlDeclaration 和 ConformanceLevel 属性来实现。

是否有 XDeclaration 对象对是否写入 XML 声明没有任何影响。 XDeclaration 的目的是提示进行 XML 序列化进程,方式有两种 :

  1. 使用的文本编码标准;

  2. 定义 XML 声明中 encoding 和 standalone 两个属性的值 (如果写入声明) ;

XDeclaration 的构造方法接受三个参数,分别用于设置 version 、 encoding 和 standalone 属性。

(P394)

XML 编写器会忽略所指定的 XML 版本信息,始终写入 “1.0” 。

需要注意的是,XML 声明中指定的必须是诸如 “utf-16” 这样的 IETF 编码方式。

XML 命名空间有两个功能。首先,与 C# 的命名空间一样,它们可以帮助避免命名冲突。当要合并来自两个不同 XML 文件的数据时,这可能会成为一个问题。其次,命名空间赋予了名称一个绝对的意义。

(P395)

xmlns 是一个特殊的保留属性,以上用法使它执行下面两种功能 :

  1. 它为有疑问的元素指定了一个命名空间;

  2. 它为所有后代元素指定了一个默认的命名空间;

有前缀的元素不会为它的后代元素定义默认的命名空间。

(P396)

使用 URI (自定义的 URI) 作为命名空间是一种通用的做法,这可以有效地保证命名空间的唯一性。

对于属性来说,最好不使用命名空间,因为属性往往是对本地元素起作用。

有多种方式可以指定 XML 命名空间。第一种方式是在本地名字前面使用大括号来指定。第二种方式 (也是更好的一种方式) 是通过 XNamespace 和 XName 为 XML 设置命名空间。

(P397)

XName 还重载了 + 运算符,这样无需使用大括号即可直接将命名空间和元素组合在一起。

在 X-DOM 中有很多构造方法和方法都能接受元素名或者属性名作为参数,但它们实际上接受 XName 对象,而不是字符串。到目前为止我们都是在用字符串作参数,之所以可以这么用,是因为字符串可以被隐式转换成 XName 对象。

除非需要输出 XML ,否则 X-DOM 会忽略默认命名空间的概念。这意味着,如果要构建子 XElement ,必须显式地指定命名空间,因为子元素不会从父元素继承命名空间。

(P398)

在使用命名空间时,一个很容易犯的错误是在查找 XML 的元素时没有指定它所属的命名空间。

如果在构建 X-DOM 树时没有指定命名空间,可以在随后的代码中为每个元素分配一个命名空间。

【第11章】

(P407)

System.Xml ,命名空间由以下命名空间和核心类组成 :

System.Xml.* ——

  1. XmlReader 和 XmlWriter : 高性能、只向前地读写 XML 流;

  2. XmlDocument : 代表基于 W3C 标准的文档对象模型 (DOM) 的 XML 文档;

System.Xml.XPath —— 为 XPath (一种基于字符串的查询 XML 的语言) 提供基础结构和 API (XPathNavigator 类) ;

System.Xml.XmlSchema —— 为 (W3C) XSD 提供基础机构和 API ;

System.Xml.Xsl —— 为使用 (W3C) XSLT 对 XML 进行解析提供基础结构和 API ;

System.Xml.Serialization —— 提供类和 XML 之间的序列化;

System.Xml.XLinq —— 先进的、简化的、 LINQ 版本的 XmlDocument 。

W3C 是 World Web Consortium (万维网联盟) 的缩写,定义了 XML 标准。

静态类 XmlConvert 是解析和格式化 XML 字符串的类。

XmlReader 是一个高性能的类,能够以低级别、只向前的方式读取 XML 流。

(P408)

通过调用静态方法 XmlReader.Create 来实例化一个 XmlReader 对象,可以向这个方法传递一个 Stream 、 TextReader 或者 URI 字符串。

因为 XmlReader 可以读取一些可能速度较慢的数据源 (Stream 和 URI) ,所以它为大多数方法提供了异步版本,这样我们可以方便编写非阻塞代码。

XML 流以 XML 节点为单位。读取器按文本顺序 (深度优先) 来遍历 XML 流, Depth 属性返回游标的当前深度。

从 XmlReader 读取节点的最基本的方法是调用 Read 方法。它指向 XML 流的下一个节点,相当于 IEnumerator 的 MoveNext 方法。第一次调用 Read 会把游标放置在第一个节点,当 Read 方法返回 false 时,说明游标已经到达最后一个节点 在这个时候 XmlReader 应该被关闭。

(P409)

属性没有包含在基于 Read 的遍历中。

XmlReader 提供了 Name 和 Value 这两个 string 类型的属性来访问节点的内容。根据节点类型,内容可能定义在 Name 或 Value 上,或者两者都有。

(P410)

验证失败会导致 XmlReader 抛出 XmlException ,这个异常包含错误发生的行号 (LineNumber) 和位置 (LinePosition) 。当 XML 文件很大时记录这些信息会比较关键。

(P413)

XmlReader 提供了一个索引器以直接 (随机) 地通过名字或位置来访问一个节点的属性,使用索引器等同于调用 GetAttributes 方法。

(P415)

XmlWriter 是一个 XML 流的只向前的编写器。 XmlWriter 的设计和 XmlReader 是对称的。

和 XmlReader 一样,可以通过调用静态方法 Create 来构建一个 XmlWriter 。

(P416)

除非使用 XmlWriterSettings ,并设置其 OmitXmlDeclaration 为 true 或者 ConfermanceLevel 为 Fragment ,否则 XmlWriter 会自动地在顶部写上声明。并且后者允许写多个根节点,如果不设置的话会抛出异常。

WriteValue 方法写一个文本节点。它不仅接受 string 类型的参数,还可以接受像 bool 、 DateTime 类型的参数,实际在内部调用了 XmlConvert 来实现符合 XML 字符串解析。

WriteString 和调用 WriteValue 传递一个 string 参数实现的操作是等价的。

在写完开始节点后可以立即写属性。

(P417)

WriteRaw 直接向输出流注入一个字符串。也可以通过接受 XmlReader 的 WriteNode 方法,把 XmlReader 中的所有内容写入输出流。

XmlWriter 使代码非常简洁,如果相同的命名空间在父元素上已声明,它会自动地省略子元素上命名空间的声明。

(P420)

可以在使用 XmlReader 或 XmlWriter 使代码复杂时使用 X-DOM ,使用 X-DOM 是处理内部元素的最佳方式,这样就可以兼并 X-DOM 的易用性和 XmlReader 、 XmlWriter 低内存消耗的特点。

(P421)

XmlDocument 是一个 XML 文档的内存表示,这个类型的对象模型和方法与 W3C 所定义的模式一致。如果你熟悉其他符合 W3C 的 XML DOM 技术,就会同样熟悉 XmlDocument 类。但是如果和 X-DOM 相比的话, W3C 模型就显得过于复杂。

(P422)

可以实例化一个 XmlDocument ,然后调用 Load 或 LoadXml 来从一个已知的源加载一个 XmlDocument :

  1. Load 接受一个文件名、 流 (Stream) 、 文本读取器 (TextReader) 或者 XML 读取器 (XmlReader) ;

  2. LoadXml 接受一个 XML 字符串;

相对应的,通过调用 Save 方法,传递文件名, Stream 、 TextReader 或者 XmlWriter 参数来保存一个文档。

通过定义在 XNode 上的 ChildNodes 属性可以深入到此节点的下层树型结构,它返回一个可索引的集合。

而使用 ParentNode 属性,可以返回其父节点。

XmlNode 定义了一个 Attributes 属性用来通过名字或命名空间或顺序位置来访问属性。

(P423)

InnerText 属性代表所有子文本节点的联合。

设置 InnerText 属性会用一个文本节点替换所有子节点,所以在设置这个属性时要谨慎以防止不小心覆盖了所有子节点。

InnerXml 属性表示当前节点中的 XML 片段。

如果节点类型不能有子节点, InnerXml 会抛出一个异常。

XmlDocument 创建和添加新节点 :

  1. 调用 XmlDocument 其中一个 CreateXXX 方法;

  2. 在父节点上调用 AppendChild 、 PrependChild 、 InsertBefore 或者 InsertAfter 来添加新节点到树上;

要创建节点,首先要有一个 XmlDocument ,不能像 X-DOM 那样简单地实例化一个 XmlElement 。节点需要 “寄生” 在一个 XmlDocument 宿主上。

(P424)

可以以任何属顺序来构建这棵树,即便重新排列添加子节点后的语句顺序,对此也没有影响。

也可以调用 RemoveChild 、 ReplaceChild 或者 RemoveAll 来移除节点。

使用 CreateElement 和 CreateAttribute 的重载方法可以指定命名空间和前缀。

CreateXXX (string name);
CreateXXX (string name, string namespaceURI);
CreateXXX (string prefix, string localName, string namespaceURI);

参数 name 既可以是本地名称 (没有前缀) ,也可以是带前缀的名称。

参数 namespaceURI 用在当且仅当声明 (而不是仅在引用) 一个命名空间时。

XPath 是 XML 查询的 W3C 标准。在 .NET Framework 中, XPath 可以查询一个 XmlDocument ,就像用 LINQ 查询 X-DOM 。然而 XPath 应用更广泛,它也在其他 XML 技术中被使用,例如 XML Schema 、 XLST 和 XAML 。

XPath 查询按照 XPath 2.0 数据模型 (XPath Data Model) 来表示。 DOM 和 XPath 数据模型都表示一个 XML 文档树。区别是 XPath 数据模型纯粹以数据为中心,采取了 XML 文本的格式。例如在 XPath 数据模型中,CDATA 部分不是必需的,因为 CDATA 存在的唯一原因是可以在文本中包含 XML 的一些标识符。

(P425)

可以使用下面的方式在代码中实现 XPath 查询 :

  1. 在一个 XmlDocument 或 XmlNode 上调用 SelectXXX 方法;

  2. 从一个 XmlDocument 或者 XPathDocument 上生成一个 XPathNavigator ;

  3. 在 XNode 上调用一个 XPathXXX 扩展方法;

SelectXXX 方法接受一个 XPath 查询字符串。

(P426)

XPathNavigator 是 XML 文档的 XPath 数据模型上的一个游标,他被加载并提供了一些基本方法可以在文档树上移动光标。

XPathNavigator 的 Select* 方法可以使用一个 XPath 字符串来表达更复杂的导航或查询以返回多个节点。

可以从一个 XmlDocument 、 XPathDocument 或者另一个 XPathNavigator 上来生成 XPathNavigator 实例。

(P427)

在 XPath 数据模型中,一个节点的值是文本元素的连接,等同于 XmlDocument 的 InnerText 属性。

SelectSingleNode 方法返回一个 XPathNavigator 。 Select 方法返回一个 XPathNodeInterator 以在多个 XPathNavigator 上进行简便地遍历。

为了更快地查询,可以把 XPath 编译成一个 XPathExpression ,然后传递给 Select* 方法。

(P428)

XmlDocument 和 XPathNavigator 的 Select* 方法有对应的重载函数来接受一个 XmlNamespaceManager 。

XPathDocument 是符合 W3C XPath 数据模型的只读的 XML 文档。使用 XPathDocument 后跟一个 XPathNavigator 要比一个单纯的 XmlDocument 快,但是不能对底层的文档进行更改。

(P429)

XSD 文档本身就是用 XML 来写的,并且 XSD 文档也是用 XSD 来介绍的。

可以在读或处理 XML 文件或文档时用一个或多个模式来验证它,这样做有以下几个理由 :

  1. 可以避免更少的错误检查和异常处理;

  2. 模式检验可以查出注意不到的错误;

  3. 错误信息比较详细重要;

为进行验证,可以把模式加入到 XmlReader 、 XmlDocument 或者 X-DOM 对象中,然后像通常那样读取或加载 XML 文档。模式验证会在内容被读的时候自动进行,所以输入流没有被读取两次。

(P430)

在 System.Xml 命名空间下包含一个 XmlValidatingReader 类,这个类存于 .NET Framework 2.0 之前的版本中,用来进行模式验证,现在已经不再使用。

(P431)

XSLT (Entensible Stylesheet Language Transformations ,扩展样式表转换语言) 是一种 XML 语言,它介绍了如何把一种 XML 语言转化为另一种。这种转化的典型就是把一个 (描述数据的) XML 文档转化为一个 (描述格式化文档的) XHTML 文档。

【第12章】

(P432)

有些对象要求显式地卸载代码来释放资源,如打开的文件、锁、执行中的系统句柄和非托管对象。在 .NET 的术语中,这叫做销毁 (Disposal) ,它由 IDisposable 接口来实现。

那些占用托管内存的未使用对象必须在某些时候被回收,这个功能被称为垃圾回收,它由 CLR 执行。

销毁不同于垃圾回收的是,销毁通常是显式调用,而垃圾回收则完全自动进行。换言之,程序员要关心释放文件句柄、锁和操作系统资源等,而 CLR 则关心释放内存。

C# 的 using 语句从语法上提供了对实现 IDisposable 接口的对象调用 Dispose 方法的捷径,它还使用了 try / finally 块。

(P433)

finally 语句块保证 Dispose 方法一定被调用,即使是抛出异常或代码提前离开这个语句块。

在简单的情况下,编写自定义的可销毁类型只需要实现 IDisposable 接口并编写 Dispose 方法。

在销毁的逻辑中,.NET Framework 遵循了一系列实际存在的规则。这些规则并不是硬编码在 .NET Framework 或 C# 语言中;它们的目的是为使用者定义一致的协议。它们是 :

  1. 一旦被销毁,对象无法恢复。对象也不能重新被激活,调用它的方法或属性将抛出 ObjectDisposedException 异常;

  2. 重复调用对象的 Dispose 方法不会产生异常;

  3. 如果可销毁对象 x 包含或 “封装” 或 “占有” 可释放资源对象 y , x 的 Dispose 方法自动调用 y 的 Dispose 方法 —— 除非接收到其他指令;

除了 Dispose 方法,一些类还定义了 Close 方法。 .NET Framework 对 Close 方法的语义并不是完全一致,尽管几乎所有的情况都是下面的一种 :

  1. 从功能上等同于 Dispose 方法;

  2. 从功能上是 Dispose 方法的子集;

(P434)

一些类定义了 Stop 方法,它们可以像 Dispose 方法一样释放非托管资源,但不同于 Dispose 方法的是,它允许重新开始。

在 WinRT 中, Close 可以认为与 Dispose 相同。事实上,运行时会将 Close 方法映射到 Dispose 方法上,使它们的类型同样可以在 using 语句中使用。

包含非托管资源句柄的对象几乎总是要求销毁,目的是为了释放这些句柄。

如果一个类型是可销毁的,它经常 (而非总是) 直接或间接地引用非托管句柄。

有 3 种情况不能释放 :

  1. 当通过静态字段或属性获得共享对象时;

  2. 当对象的 Dispose 方法执行不需要的操作时;

  3. 当对象的方法在设计时不是必须的,而且释放那个对象将增加程序的复杂性时;

(P436)

StreamWriter 必须公开另一个方法 (Flush 方法) 来保证使用者不调用 Dispose 方法也能执行必要的清理工作。

Dispose 方法本身并没有释放内存,只有垃圾回收时才释放内存。

无论对象是否要求使用 Dispose 方法来自定义清理逻辑,某些情况下在堆上被占用的内存必须被释放。 CLR 通过垃圾回收器完全自动地处理这方面工作。永远不能自动释放托管内存。

(P437)

垃圾回收并不是在对象没有引用之后立即执行。

垃圾回收器在每次回收时并没有回收所有的垃圾。相反的,内存管理器将对象分为不同的代,垃圾回收器收集新代 (最近分配的对象) 的垃圾比旧代 (长时间存活的对象) 的垃圾更频繁。

垃圾回收器试图在垃圾回收所花费的时间和应用程序内存使用 (工作区) 上保持平衡。因此,应用程序会使用比实际需要更多的内存,特别是构造大的临时数组。

根保持对象存活。如果对象没有直接或间接地由根引用,那么它将被垃圾回收器选中。

根有以下三种 :

  1. 局部变量或执行方法中的参数 (或在调用它的栈的方法中);

  2. 静态变量;

  3. 准备运行终止器的对象;

(P438)

Windows Runtime 依靠 COM 的引用计数机制来释放内存,而非依靠自动化的垃圾回收器。

在对象从内存中被释放之前,它的终止器将运行 (如果它有终止器的话) 。终止器像构造方法一样声明,但是它有 ~ 符号作前缀。

虽然与构造函数的声明相似,但是析构器无法声明为 public 或 static ,不能有参数,而且不能调用基类。

(P439)

终止器很有用,但是它有一些附带条件 :

  1. 终止器使分配和内存回收变得缓慢 (垃圾回收器将对执行的终止器保持追踪) ;

  2. 终止器延长了对象和任意引用对象的生命周期 (它们必须等待下一次垃圾回收来实际删除) ;

  3. 无法预测终止器以什么顺序调用一系列的对象;

  4. 对对象的终止器何时被调用只有有限的控制;

  5. 如果终止器的代码被阻碍,其他对象也不能被终结;

  6. 如果应用程序没有被完全地卸载,终止器也许会被规避;

总之,终止器尽管在有些时候你确实需要它,通常你不想使用它,除非绝对必要。如果确实要使用它,需要 100% 确定理解它所做的一切。

实现终止器的准则 :

  1. 保证终止器执行得很快;

  2. 永远不要在终止器中中断;

  3. 不要引用其他可终结对象;

  4. 不要抛出异常;

终止器的一个很好的用途是当忘记对可销毁对象调用 Dispose 方法的时候提供一个备份;对象迟一点被销毁通常比没有被销毁好。

(P440)

无参数的版本没有被声明成虚方法 (virtual) ,它只是简单地用 true 作为参数调用的增强版本。

增强版本包含实际的销毁逻辑,它是受保护的 (protected) 和虚拟的 (virtual) ,这为子类添加它们自己的销毁逻辑提供了安全的方法。

请注意我们在没有参数的 Dispose 方法中调用了 GC.SuppressFinalize 方法,这防止当垃圾回收器在之后捕捉这个对象时终止器也同时运行的情况。从技术上讲这并不必要,因为 Dispose 方法能够接受重复调用。但是,这样可以提高效率,因为允许对象 (和它引用的对象) 在一个周期中被回收。

(P441)

复活对象的终止器不会第二次执行,除非调用 GC.ReRegisterForFinalize 方法。

(P442)

请注意在终止器方法中只调用一次 ReRegisterForFinalize 方法。如果调用了两次,对象将会被注册两次并且经历两次终结过程。

CLR 使用分代式 “标记-紧缩型” 垃圾回收器来执行存储在托管堆上对象的自动内存管理。垃圾回收器被认为是追踪型垃圾回收器,因为它不会干涉每次对对象的访问,而是立刻激活并追踪存储在托管堆上对象的记录,以此来决定哪些对象被认为是垃圾并被回收。

垃圾回收器通过执行内存分配 (通过 new 关键字) 开始一次垃圾回收,在内存分配或者某个内存起始点被分配之后,或者在其他减少应用程序内存的时候。这个过程也可以通过调用 System.GC.Collect 方法手动开始。在垃圾回收时,所有的线程也许都会被冻结。

垃圾回收器从根对象引用开始,按对象记录前进,标记它所有接触的对象为可到达的。一旦这个过程结束,所有没有被标记的对象被认为是无用的,将会被垃圾回收器回收。

没有终止器的无用对象将立刻被删除;有终止器的对象将在垃圾回收结束之后在终止器中排队进行处理。这些对象将在下一次对这代对象的垃圾回收过程中被选中回收 (除非复活) 。

然后将剩余的 “活动” 对象移到堆的开头 (紧缩) ,释放出更多的对象空间。这种压缩操作有两个目的 : 避免出现内存片段,允许垃圾回收器在分配新对象时始终在堆的末尾分配内存。这可避免为可能非常耗时的任务维护剩余内存片段的列表。

如果在垃圾回收之后没有足够的内存来分配新的对象,操作系统将无法分配更多的内存,这时将抛出 OutOfMemoryException 异常。

垃圾回收包含多种优化技术来减少垃圾回收的时间。

(P443)

最重要的优化是垃圾回收是分代的。

基本上讲,垃圾回收器将托管堆分为三代。刚刚被分配的对象在 Gen 0 里,在一轮回收幸存下来的对象在 Gen 1 里,其他所有对象都在 Gen 2 里。

CLR 将 Gen 0 部分保持在相对较小的空间内 (在 32 位工作站 CLR 上最大是 16MB ,典型的大小是几百 KB 到几 MB) 。当 Gen 0 部分被填满之后,垃圾回收器引发 Gen 0 的回收,这经常发生。垃圾回收器对 Gen 1 执行相似的内存限制 (Gen 1 扮演着 Gen 2 的缓存角色) ,因此 Gen 1 的回收也相对地快速和频繁。然后,包括 Gen 2 的完全回收花费更长的时间,发生得不那么频繁。

存活周期短的对象非常有效地被垃圾回收器使用。

(P444)

对大于某一限度 (当前是 85000 字节) 的对象,垃圾回收器使用特殊的堆即 “大对象堆” 。这避免了过多的 Gen 0 回收,分配一系列 16MB 的对象也许会在每次分配之后引起一次 Gen 0 的回收。

大对象堆并不是分代的 : 所有对象都按 Gen 2 来处理。

垃圾回收器在回收的时候必定会冻结 (阻止) 执行线程一段时间,这包括 Gen 0 和 Gen 1 回收发生的整个时间。

可以在任何时间通过调用 GC.Collect 方法强制垃圾回收。调用 GC.Collect 方法而没有参数将发起完全回收。如果传入一个整数值,只有整数值的那一代将被回收,因此 GC.Collect(0) 只执行一次快速的 Gen 0 回收。

(P445)

总的来说,通过允许垃圾回收器来决定何时回收来获得最好的性能。强制回收不必要地将 Gen 0 对象提升到 Gen 1 中,这将降低性能,也将影响垃圾回收器的自我调节能力,即垃圾回收器动态调整每一代回收的开始时间,以保证在应用程序执行的时候性能最大化。

(P446)

在 WPF 的主题中,数据绑定是另一个导致内存泄露的常见情况。

忘记计时器也能造成内存泄露。

(P447)

一个很好的准则是如果类中的任何字段被赋值给实现 IDisposable 接口的对象,类也应该实现 IDisposable 接口。

【第13章】

(P452)

可以使用预处理器指令有条件地编译 C# 中的任何代码段。预处理器指令是以 C# 符号开头特殊的编译器指令。不同于其他 C# 结构体的是,它必须出现在单独的一行。条件编译的预处理指令有 #if 、 #else 、 #endif 和 # elif 。

#if 指令表示编译器将忽略一段代码,除非定义了特定的符号。可以用 #define 指令或编译开关来定义一个符号。 #define 指令应用于特定的文件;编译开关应用于整个程序集。

define 指令必须在文件顶端。

(P453)

#else 语句和 C# 的 else 语句很类似, #elif 等同于 #if 其后的 #else 。

|| 、 && 和 ! 运算符用于执行或、与和非运算。

要在程序集范围内定义符号,可在编译时指定 /define 开关。

Visual Studio 在 “项目属性” 中提供了输入条件编译符号的选项。

如果在程序集级别定义了符号,之后想在某些特定文件中取消定义,可使用 #undef 指令。

(P454)

[Conditional] 的另一个好处是条件性检测在调用方法被编译时执行,而不是在调用的方法被编译时。

Conditional 属性在运行时被忽略,因为它仅仅是给编译器的指令而已。

如果需要在运行时动态地启用或禁用某种功能, Conditional 属性将毫无用处,而是必须使用基于变量的方法。

(P455)

Debug 和 Trace 是提供基本日志和断言功能的静态类。这两个类很类似,主要的不同是它们的特定用途。 Debug 类用于调试版本; Trace 类用于调试和发布版本。

所有 Debug 类的方法都用 [Conditional("DEBUG")] 定义;

所有 Trace 类的方法都用 [Conditional("TRACE)] 定义;

这意味着所有调用标记为 DEBUG 或 TRACE 的方法都会被编译器忽略,除非定义了 DEBUG 或 TRACE 符号。默认情况下, Visual Studio 在项目的调试配置中定义了 DEBUG 和 TRACE 符号,同时只在发布配置中定义了 TRACE 符号。

Debug 和 Trace 类都提供了 Write 、 WriteLine 和 WriteIf 方法。默认情况下,这些方法向调试器的输出窗口发送消息。

Trace 类也提供了 TraceInformation 、 TraceWarning 和 TraceError 方法。这些方法和 Write 方法在行为上的不同取决于 TraceListeners 类。

Debug 和 Trace 类都提供了 Fail 和 Assert 方法。

Fail 方法给每一个在 Debug 或 Trace 类的 Listeners 集合中的 TraceListener 发送消息,默认在调试输出窗口和对话框中显示消息。

Assert 方法在布尔参数为 false 时仅仅调用 Fail 方法,这叫做使用断言。指定错误消息也是可选的。

Write 、 Fail 和 Assert 方法也被重载来接受字符串类型的额外信息,这在处理输出时很有用。

(P456)

Debug 和 Trace 类都有 Listeners 属性,包含了 TraceListener 实例的静态集合。它们负责处理由 Write 、 Fail 和 Trace 方法发起的内容。

(P457)

对于 Windows 事件日志,通过 Wirte 、 Fail 或 Assert 方法输出的消息在 Windows 事件查看器中总是显示为 “消息” 。但是,通过 TraceWarning 和 TraceError 方法输出的消息,则显示为 “警告” 或 “错误” 。

Trace 和 Debug 类提供了静态的 Close 和 Flush 方法来调用所有监听器的 Close 和 Flush 方法 (依次调用它所属的编写器和流的 Close 或 Flush 方法) 。 Close 方法隐式地调用 Flush 方法,关闭文件句柄,防止数据进一步被写入。

作为一般的规则,要在应用程序结束前调用 Close 方法,随时调用 Flush 方法来保证当前的消息数据被写入。这适用于使用流或基于文件的监听器。

(P458)

Trace 和 Debug 类也提供了 AutoFlush 属性,如果它为 true ,则在每条消息之后强制执行 Flush 方法。

如果使用任何文件或基于流的监听器,将 AutoFlush 设为 true 是很好的方法。否则,如果任何未处理的异常或关键的错误发生,最后 4KB 的诊断信息也许会丢失。

Framework 4.0 提供了叫做 “代码契约” 的新特性,用统一的系统代替了这些方法。这种系统不但支持简单的断言,也支持更加强大的基于契约的断言。

代码契约由 Eiffel 编程语言中的契约式设计原则而来,函数之间通过相互有义务和好处的系统进行交互。本质上讲,客户端 (调用方) 必须满足函数指定的先决条件和保证当函数返回时客户端能够依赖的后置条件。

代码契约的类型存在于 System.Diagnostics.Contracts 命名空间中。

先决条件由 Contract.Requires 定义,它在方法开始时被验证。后置条件由 Contract.Ensures 定义,它并不在它出现的地方被验证,而是当方法结束时被验证。

(P459)

先决条件和后置条件必须出现在方法的开始。优点是如果没有在按顺序编写的方法中实现契约,错误就会被检测出来。

代码契约的另一个限制是不能用它们来执行安全性检查,因为它们在运行时被规避 (通过处理 ContractFailed 事件) 。

代码契约由先决条件、后置条件、断言和对象不变式组成。这些都是可发现的断言。不同之处是它们何时被验证 :

  1. 先决条件在函数开始时被验证;

  2. 后置条件在函数结束之前被验证;

  3. 断言在它出现的地方被验证;

  4. 对象不变式在每个类中的公有函数之后被验证;

(P460)

代码契约完全通过调用 Contract 类中的 (静态) 方法来定义,这与契约语言无关。

契约不仅在方法中出现,也可以在其他函数中出现,例如构造方法、属性、索引器和运算符。

(P465)

无论重写的方法是否调用了基方法,二进制重写器能保证基方法的先决条件总是在子类中被执行。

(P467)

以下两个原因使 Contract.Assert 比 Debug.Assert 更受欢迎 :

  1. 通过代码契约提供的失败处理机制能获得更多的灵活性;

  2. 静态检测工具能尝试验证 Contract.Asserts ;

(P473)

DbgCLR 是 Visual Studio 中的调试器,和 .NET Framework SDK 一起免费下载,它是当没有 IDE 时最简单的调试选择,尽管必须下载整个 SDK 。

(P474)

Process.GetProcessXXX 方法通过名称或进程 ID 检索指定进程,或检索所有运行在当前或指定名称计算机中的进程,包括所有托管和非托管的进程。每一个 Process 实例都有很多属性映射到各种统计数据上,例如名称、 ID 、优先级、内存和处理器利用率、窗口句柄等。

Process.GetCurrentProcess 方法返回当前的进程。如果创建了额外的应用程序域,它们将共享同一个进程。

可以通过调用 Kill 方法来终止一个进程。

(P475)

也可以用 Process.Threads 属性遍历其他进程的所有线程。然而,获得的对象并不是 System.Threading.Thread 对象,而是 ProcessThread 对象,它用于管理而不是同步任务。

ProcessThread 对象提供了潜在线程的诊断信息,并允许控制它的一些属性,例如优先级和处理器亲和度。

(P476)

Exception 已经有 StackTrace 属性,但是这个属性返回的是简单的字符串而不是 StackTrace 对象。

如果注册了 EventLogTraceListener 类,之前使用的 Debug 和 Trace 类可以写入 Windows 事件日志。但是,可以使用 EventLog 类直接写入 Windows 事件日志而不使用 Trace 或 Debug 类。也可以使用这个类来读取和监视事件数据。

写入事件日志对 Windows 服务应用程序来说很有意义,因为如果出错了,不能弹出用户界面来提供给用户一些包含诊断信息的特殊文件。也因为 Windows 服务通常都写入 Windows 事件日志,如果服务出现问题, Windows 事件日志几乎是管理员首先要查看的地方。

(P477)

有三种标准的 Windows 事件日志,按名称分类 :

  1. 应用程序;

  2. 系统;

  3. 安全;

应用程序日志是大多数应用程序通常写入的地方。

要写入 Windows 事件日志 :

  1. 选择三种事件日志中的一种 (通常是应用程序日志) ;

  2. 决定源名称,必要时创建;

  3. 用日志名称、源名称和消息数据来调用 EventLog.WriteEntry 方法;

源名称使应用程序更容易分类。必须在使用它之前注册源名称,使用 CreateEventSource 方法可以实现这个功能,之后可以调用 WriteEntry 方法。

EventLogEntryType 可以是 Information 、 Warning 、 Error 、 SuccessAudit 或 FailureAudit 。

每一个在 Windows 事件查看器中都显示不同的图标。

CreateEventSource 也允许指定计算机名 : 这可以写入其他计算机的事件日志,如果有足够的权限。

要读取事件日志,用想访问的日志名来实例化 EventLog 类,并选择性地使用日志存在的其他计算机名。每一个日志项目能够通过 Entries 集合属性来读取。

(P478)

可以通过静态方法 EventLog.GetEventLogs 来遍历当前 (或其他) 计算机上的所有日志 (这需要管理员权限) 。通常这至少会打印应用程序日志、安全日志和系统日志。

通过 EntryWritten 事件,一条项目被写入到 Windows 事件日志时,将获得通知。对工作在本机的事件日志,无论什么应用程序记录日志都会被触发。

要开启日志监视 :

  1. 实例化 EventLog 并设置它的 EnableRaisingEvents 属性为 true ;

  2. 处理 EntryWritten 事件;

(P483)

Stopwatch 类提供了一种方便的机制来衡量执行时间。Stopwatch 使用了操作系统和硬件提供的最高分辨率机制,通常少于 1ms (对比一下, DateTime.Now 和 Environment.TickCount 有大约 15ms 的分辨率) 。

要使用 Stopwatch 调用 StartNew() 方法,它实例化 Stopwatch 对象并开始计时 (换句话说,可以手动实例化并在之后调用 Start 方法) 。 Elapsed 返回表示过去的时间间隔的 TimeSpan 对象。

Stopwatch 也公开了 ElapsedTicks 属性,它返回表示过去时间的 long 类型的数字。要将时间转换成秒,请除以 Stopwatch.Frequency 。 Stopwatch 也有 ElapsedMilliseconds 属性,这通常是最方便的。

调用 Stop 方法将终止 Elapsed 和 ElapsedTicks 。运行的 Stopwatch 并不会引起任何后台活动,因此调用 Stop 方法是可选的。

【第14章】

(P484)

程序并发执行代码的通用机制是多线程 (multithreading) 。 CLR 和操作系统都支持多线程,它是一种基础并发概念。因此,最基本的要求是理解线程的基本概念,特别是线程的共享状态。

(P485)

线程 (thread) 是一个独立处理的执行路径。

每一个线程都运行在一个操作系统进程中,这个进程是程序执行的独立环境。在单线程 (single-threaded) 程序中,在进程的独立环境中只有一个线程运行,所以该线程具有独立使用进程资源的权利。

在多线程 (multi-threaded) 程序中,在进程中有多个线程运行,它们共享同一个执行环境 (特别是内存) 。这在一定程度上反映了多线程处理的作用 : 例如,一个线程在后台获取数据,同时另一个线程显示所获得的数据,这些数据就是所谓的共享状态 (shared state) 。

Windows Metro 配置文件不允许直接创建和启动线程;相反,必须通过任务来操作线程。任务增加了间接创建线程的方法,这种方法增加了学习复杂性,所以最好从控制台应用程序开始,熟悉它们的使用方法,然后再直接创建线程。

客户端程序 (Console 、 WPF 、 Metro 或 Windows 窗体) 都从操作系统自动创建一个线程 (主线程) 开始。除非创建更多的线程 (直接或间接) ,否则这就是单线程应用程序的运行环境。

实例化一个 Thread 对象,然后调用它的 Start 方法,就可以创建和启动一个新的线程。

最简单的 Thread 构造方法接受一个 ThreadStart 代理 : 一个无参数方法,表示执行开始位置。

在单核计算机上,操作系统会给每一个线程分配一些 “时间片” (Windows 一般为 20 毫秒) ,用于模拟并发性,因此这段代码会出现连续的 x 和 y 。在 多核 / 多处理器 主机上执行时,虽然这个例子仍然会出现重复的 x 和 y (受控制台处理并发请求的机制影响) ,但是线程却能够真正实现并行执行 (分别由计算机上其他激活处理器完成) 。

(P486)

线程被认为是优先占用 (preempted) 它在执行过程与其他线程代码交叉执行的位置。这个术语通常可以解释出现的问题。

在线程启动之后,线程的 IsAlive 属性就会变成 true ,直到线程停止。当 Thread 的构造函数接收的代理执行完毕时,线程就会停止。在停止之后,线程无法再次启发。

每个线程都有一个 Name 属性,它用于调试程序。它在 Visual Studio 中特别有用,因为线程的名称会显示在 Threads 窗口和 Debug Location 工具栏上。线程名称只能设置一次;修改线程名称会抛出异常。

静态属性 Thread.CurrentThread 可以返回当前执行的线程。

在等待另一个线程结束时,可以调用另一个线程的 Join 方法。

Thread.Sleep 会将当前线程暂停执行一定的时间。

(P487)

调用 Thread.Sleep(0) ,会马上放弃线程的当前时间片,自动将 CPU 交给其他线程。

Thread.Yield() 方法也有相同的效果,但是它只会将资源交给在同一个处理器上运行的线程。

有时候,在生产代码中使用 Sleep(0) 或 Yield ,可以优化性能。它还是一种很好的诊断工具,可以帮助开发者发现线程安全问题 : 如果在代码任意位置插入 Thread.Yield() 会破坏程序,那么代码肯定存在 Bug 。

在等待线程 Sleep 或 Join 的过程中,还可以阻塞线程。

线程阻塞是指线程由于特定原因暂停执行,如 Sleeping 或执行 Join 后等待另一个线程停止。阻塞的线程会立刻交出 (yield) 它的处理器时间片,然后从这时开始不再消耗处理器时间,直至阻塞条件结束。使用线程的 ThreadState 属性,可以测试线程的阻塞状态。

ThreadState 是一个标记枚举量,它由三 “层” 二进制位数据组成。

ThreadState 属性可用于诊断程序,但是不适用于实现同步,因为线程状态可能在测试 ThreadState 和获取这个信息的时间段内发生变化。

当线程阻塞或未阻塞时,操作系统会执行环境切换 (context switch) 。这个操作会稍微增加负载,幅度一般在 1~2 毫秒左右。

如果一个操作将大部分时间用于等待一个条件的发生,那么它就称为 I / O 密集 (I / O - bound) 操作。

I / O 密集操作一般都会涉及输入或输出,但是这不是硬性要求 : Thread.Sleep 也是一种 I / O 密集操作。

如果一个操作将大部分时间用于执行 CPU 密集操作,那么它就称为计算密集 (compute-bound) 操作。

I / O 密集操作可以以两种方式执行 : 同步等待当前线程的操作完成 (如 Console.ReadLine 、Thread.Sleep 或 Thread.Join) ,或者异步执行,然后在将来操作完成时触发一个回调函数。

异步等待的 I / O 密集操作会将大部分时间花费在线程阻塞上。它们也可能在一个定期循环中自旋。

(P488)

自旋与阻塞有一些细微差别。首先,非常短暂的自旋可能非常适用于设置很快能满足的条件 (也许是几毫秒之内) ,因为它可以避免过载和环境切换延迟。

CLR 会给每一个线程分配独立的内存堆,从而保证本地变量的隔离。

如果线程拥有同一个对象实例的通用引用,那么这些线程就共享相同的数据。

(P489)

编译器会将 Lambda 表达式或匿名代理捕获的局部变量转换为域,所以它们也可以共享。

静态域是在线程之间共享数据的另一种方法。

(P490)

当两个线程同时争夺一个锁时 (它可以是任意引用类型的对象,这里是 _locker) ,其中一个线程会等待 (或阻塞) ,直到锁释放。这个例子保证一次只有一个线程能够进入它的代码块,因此 “Done” 只打印一次。在复杂的多线程环境中,采用这种方式来保护的代码就是具有线程安全性 (thread-safe) 。

锁并不是解决线程安全的万能法宝 —— 人们很容易在访问域时忘记锁,而且锁本身也存在一些问题 (如死锁) 。

(P491)

ParameterizedThreadStart 的局限性在于 : 它只接受一个参数。而且因为参数属于类型 object ,所以它通常需要进行强制转换。

Lambda 表达式是向线程传递数据的最方便且最强大的方法。

(P492)

在线程创建时任何生效的 try / catch / finally 语句块开始执行后都与线程无关。

(P493)

在运行环境中,应用程序的所有线程入口方法都需要添加一个异常处理方法 —— 就和主线程一样 (通常位于更高一级的执行堆栈中) 。

默认情况下,显示创建的线程都是前台线程 (foreground thread) 。无论是否还有后台线程 (background thread) 运行,只要有一个前台线程仍在运行,整个应用程序就会保持运行状态。当所有前台线程结束时,应用程序就会停止,而且所有仍在运行的后台线程也会随之中止。

线程的 前台 / 后台 状态与线程的优先级 (执行时间分配) 无关。

使用线程的 IsBackground 属性,可以查询或修改线程的后台状态。

(P494)

线程的 Priority 属性可以确定它与其他激活线程在操作系统中的相对执行时间长短。

如果同时激活多个线程,优先级就会变得很重要。提高一个线程的优先级时,要注意不要过度抢占其他线程的执行时间。如果希望一个线程拥有比其他进程的线程更高级的优先级,那么还必须使用 System.Diagnostics 的 Process 类,提高进程本身的优先级。

这种方法非常适合于一些工作量较少但要求较低延迟时间 (能够快速响应) 的 UI 进程中。在计算密集特别是带有用户界面的应用程序中,提高进程优先级可能会抢占其他进程的执行时间,从而影响整个计算机的运行速度。

有时候,一个线程需要等待来自其他线程的通知,这就是所谓的发送信号 (singaling) 。最简单的发送信号结构是 ManualResetEvent 。在一个 ManualResetEvent 上调用 WaitOne ,可以阻塞当前线程,使之一直等待另一个线程通过调用 Set “打开” 信号。

(P495)

在调用 Set 之后,信号仍然保持打开;调用 Reset ,就可以再次将它关闭。 ManualResetEvent 是 CLR 提供的多个信号发送结构之一。

(P496)

System.ComponentModel 命名空间中有一个抽象类 SynchronizationContext ,它实现了编程编列一般化。

WPF 、 Metro 和 Windows 窗体都定义和实例化了 SynchronizationContext 的子类,当运行在 UI 线程上时,它可以通过静态属性 SynchronizationContext.Current 获得。捕获这个属性,将来就可以在工作者线程上提交数据到 UI 控件。

(P497)

SynchronizationContext 还有一个专门用在 ASP.NET 的子类,它这时作为一个更微妙的角色,保证按照异步操作方式处理页面处理事件,并且保留 HttpContext 。

在 Dispatcher 或 Control 上调用 Post 与调用 BeginInvoke 的效果相同;另外 Send 方法与 Invoke 的效果相同。

Framework 2.0 引入了 BackgroundWorker 类,它使用 SynchronizationContext 类简化富客户端应用程序的工作者线程。BackgroundWorker 增加了相同的 Tasks 和异步功能,它也使用 SynchronizationContext 。

无论何时启动一个线程,都需要一定时间 (几百毫秒) 用于创建新的局部变量堆。线程池 (thread pool) 预先创建了一组可回收线程,因此可以缩短这段过载时间。要实现高效的并行编程和细致的并发性,必须使用线程池;它可用于运行一些短暂操作,而不会受到线程启动过载的影响。

在使用线程池中的线程 (池化线程) 时,还需要考虑下面这些问题 :

  1. 由于不能设置池化线程的 Name ,因此会增加代码调试难度;

  2. 池化线程通常都是后台线程;

  3. 池化线程阻塞会影响性能;

池化线程的优先级可以随意修改 —— 在释放回线程池时,优先级会恢复为普通级别。

使用属性 Thread.CurrentThread.IsThreadPoolThread ,可以确定当前是否运行在一个池化线程上。

在池化线程上运行代码的最简单方法是使用 Task.Run 。

(P498)

由于 Framework 4.0 之前不支持任务,所以可以改为调用 ThreadPool.QueueUserWorkItem 。

(P498)

使用线程池的情况有 :

  1. WCF 、 远程处理 (Remoting) 、 ASP.NET 和 ASMX Web Services 应用服务器;

  2. System.Timers.Timer 和 System.Threading.Timer;

  3. 并行编程结构;

  4. BackgroundWorker 类 (现在是多余的) ;

  5. 异步代理 (现在是多余的) ;

线程池还有另一个功能,即保证计算密集作业的临时过载不会引起 CPU 超负荷 (oversubscription) 。

超负荷是指激活的线程数量多于 CPU 内核数量,因此操作系统必须按时间片执行线程调度。超负荷会影响性能,因为划分时间片需要大量的上下文切换开销,并且可能使 CPU 缓存失效,而这是现代处理器实高性能的必要条件。

CLR 能够将任务进行排序,并且控制任务启动数量,从而避免线程池超负荷。它首先运行与硬件内核数量一样多的并发任务,然后通过爬山算法调整并发数量,在一个方向上不停调整工作负荷。如果吞吐量提升,那么它会在这个方向上继续调整 (否则切换到另一个方向) 。这样就保证能够发现最优性能曲线 —— 即使是计算机上同时发生的活动。

如果满足以下两个条件,则适合使用 CLR 的策略 :

  1. 大多数工作项目的运行时间都非常短 (小于 250ms ,最理想情况是小于 100ms) ,这样 CLR 就有大量的机会可以测量和调整;

  2. 线程池不会出现大量将大部分时间都浪费在阻塞上的作业;

阻塞是很麻烦的,因为它会让 CLR 错误地认为它占用了大量的 CPU 。 CLR 能够检测并补偿 (往池中注入更多的线程) ,但是这可能使线程池受到超负荷的影响。此外,这样也会增加延迟,因为 CLR 会限制注入新线程的速度,特别是应用程序生命周期的前期 (在客户端操作系统上更严重,因为它有严格的低资源消耗要求) 。

如果想要提高 CPU 的利用率,那么一定要保持线程池的整洁性。

线程是创建并发的底层工具,因此它具有一定的局限性。特别是 :

  1. 虽然很容易向启动的线程传入数据,但是并没有简单的方法可以从联合 (Join) 线程得到 “返回值” 。因此,必须创建一些共享域。当操作抛出一个异常时,捕捉和处理异常也是非常麻烦的;

  2. 当线程完成之后,无法再次启动该线程;相反,只能够联合 (Join) 它 (在进程中阻塞当前线程) 。

(P499)

这些局限性会影响并发性的实现;换而言之,不容易通过组合较小的并发操作实现较大的并发操作 (这对于异步编程而言非常重要) 。因此,这会增加对手工同步处理 (加锁、发送信号) 的依赖,而且很容易出现问题。

直接使用线程会对性能产生影响。而且,如果需要运行大量并发 I / O 密集操作,那么基于线程的方法仅仅在线程过载方面就会消耗大量的内存。

Task 类可以解决所有这些问题。与线程相比, Task 是一个更高级的抽象概念,它表示一个通过或不通过线程实现的并发操作。任务是可组合的 (compositional) —— 使用延续 (continuation) 将它们串联在一起。它们可以使用线程池减少启动延迟,而且它们可以通过 TaskCompletionSource 使用回调方法,避免多个线程同时等待 I / O 密集操作。

Task 类型是 Framework 4.0 引入的,作为并行编程库的组成部分。然后,它们后来 (通过使用等待者 awaiter) 进行了很多改进,从而在常见并发场景中发挥越来越大的作用,并且也是 C# 5.0 异步功能的基础类型。

从 Framework 4.5 开始,启动一个由后台线程实现的 Task ,最简单的方法是使用静态方法 Task.Run (Task 类似于 System.Threading.Tasks 命名空间) 。调用时只需要传入一个 Action 代理。

Task.Run 是 Framework 4.5 新引入的方法。在 Framework 4.0 中,调用 Task.Factory.StartNew ,可以实现相同的效果。前者相当于是后者的快捷方式。

Task 默认使用池化线程,它们都是后台线程。这意味着当主线程结束时,所有任务也会随之停止。因此,要在控制台应用程序中运行这些例子,必须在启动任务之后阻塞主线程。例如,挂起 (Waiting) 该任务,或者调用 Console.ReadLine 。

(P500)

Task.Run 会返回一个 Task 对象,它可用于监控任务执行过程,这一点与 Thread 对象不同。

注意这里没有调用 Start ,因为 Task.Run 创建的是 “热” 任务;相反,如果想要创建 “冷” 任务,则必须使用 Task 的构造函数,但是这种用法在实践中很少使用。

任务的 Status 属性可用于跟踪任务的执行状态。

调用任务的 Wait 方法,可以阻塞任务,直至任务完成,其效果等同于调用线程的 Join 。

可以在 Wait 中指定一个超时时间和一个取消令牌 (用于提前中止停止等待状态) 。

在默认情况下, CLR 会运行在池化线程上,这种线程非常适合执行短计算密集作业。如果要执行长阻塞操作,则可以按以下方式避免使用池化线程。

在池化线程上运行一个长任务问题并不大;但是如果要同时运行多个长任务 (特别是会阻塞的任务) ,则会对性能产生影响。在这种情况下,通常更好的方法是使用 TrackCreationOptions.LongRunning :

  1. 如果是运行 I / O 密集任务,则可以使用 TaskCompletionSource 和异步操作 (asynchronous functions) ,通过回调函数 (延续) 实现并发性,而不通过线程实现;

  2. 如果是运行计算密集任务,则可以使用一个 生产者 / 消费者 队列,控制这些任务的并发数量,避免出现线程和进程阻塞的问题;

Task 有一个泛型子类 Task ,它允许任务返回一个值。调用 Task.Run ,传入一个 Func 代理 (或者兼容的 Lambda 表达式) , 代替 Action ,就可以获得一个 Task

然后,查询 Result 属性,就可以获得结果。如果任务还没有完成,那么访问这个属性会阻塞当前线程,直至任务完成。

(P501)

Task 可以看作是 “将来” ,其中封装了后面很快生效的 Result 。

有趣的是,当 Task 和 Task 第一次出现在早期的 CTP 时,后者实际上是 Future

与线程不同,任务可以随时抛出异常。所以,如果任务中的代码抛出一个未处理异常 (换而言之,任务出错) , 那么这个异常会自动传递到调用 Wait() 的任务上或者访问 Task 的 Result 属性的代码上。

使用的 Task 的 IsFaulted 和 IsCanceled 属性,就可以不重新抛出异常而检测出错的任务。如果这两个属性都返回 false ,则表示没有错误发生;如果 IsCanceld 为 true ,则任务抛出了 OperationCanceledOperation ;如果 IsFaulted 为 true , 则任务抛出了另一种异常,而 Exception 属性包含了该错误。

如果使用了自主的 “设置后忘记的” 任务 (不通过 Wait() 或 Result 控制的任务,或者实现相同效果的延续) ,那么最好在任务代码中显式声明异常处理,避免出现静默错误,就像线程的异常处理一样。

自主任务上的未处理异常称为未监控异常 (unobserved exception) ,在 CLR 4.0 中,它们实际上会中止程序 (当任务跳出运行范围并被垃圾回收器回收时, CLR 会在终结线程上重新抛出异常) 。这种方式有利于提醒一些悄悄发生的问题;然而,错误发生时间可能并不准确,因为垃圾回收器可能会明显滞后于发生问题的任务。因此,在发现这种行为具有复杂的不同步性模式时 , CLR 4.5 删除了这个特性。

如果异常仅仅表示无法获得一些不重要的结果,那么忽略异常是最好的处理方式。

如果异常反映了程序的重大缺陷,那么忽略异常是很有问题。这其中的原因有两个 :

  1. 这个缺陷可能使程序处于无效状态;

  2. 这个缺陷可能导致更多的异常发生,而且无法记录初始错误也会增加诊断难度;

使用静态事件 TaskScheduler.UnobservedTaskException ,可以在全局范围订阅未监控的异常;处理这个事件,然后记录发生的错误,是一个很好的异常处理方法。

未监控异常有一些有趣的细微差别 :

  1. 如果在超时周期之后发生错误,那么等待超时的任务将生成一个未监控异常;

  2. 在错误发生之后检查任务的 Exception 属性,会使异常变成 “已监控异常” ;

延续 (continuation) 会告诉任务在完成后继续执行下面的操作。延续通常由一个回调方法实现,它会在操作完成之后执行一次。给一个任务附加延续的方法有两种。第一种方法是 Framework 4.5 新增加的,它非常重要,因为 C# 5.0 的异步功能使用了这种方法。

调用 GetAwaiter 会返回一个等待者 (awaiter) 对象,它的方法会让先导 (antecedent) 任务 (primeNumberTask) 在完成 (或出错) 之后执行一个代理。已经完成的任务也可以附加一个延续,这时延续就马上执行。

等待者 (awaiter) 可以是任意对象,但是它必须包含前面所示两个方法 (OnCompleted 和 GetResult) 和一个 Boolean 类型属性 IsCompleted 的对象,它不需要实现包含所有这些成员的特定接口或继承特定基类 (但是 OnCompleted 属性接口 INotifyCompletion) 。

(P503)

如果先导任务出现错误,那么当延续代码调用 awaiter.GetResult() 时就会重新抛出异常。我们不需要调用 GetResult ,而是直接访问先导任务的 Result 属性。调用 GetResult 的好处是,当先导任务出现错误时,异常可以直接抛出,而不会封装在 AggregateException 之中,从而可以实现更简单且更清晰的异常捕捉代码。

对于非泛型任务,GetResult() 会返回空值 (void) ,然后它的实用函数会单独重新抛出异常。

如果出现同步上下文,那么会自动捕捉它,然后将延续提交到这个上下文中。这对于富客户端应用程序而言非常实用,因为会将延续弹回 UI 线程。然而,在编写库时,通常不采用这种方法,因为开销相对较大的 UI 线程只会在离开库时运行一次,而不会在方法调用期间运行。

如果不出现同步上下文或者使用 ConfigureAwait(false) ,那么通常延续会运行在先导任务所在的线程上,从而避免不必要的过载。

ContinueWith 本身会返回一个 Task ,它非常适用于添加更多的延续。然而,如果任务出现错误,我们必须直接处理 AggregateException ,然后编写额外代码,将延续编列到 UI 应用程序中。而在非 UI 下文中,如果想要让延续运行在同一个线程上,则必须指定 TaskContinuationOptions.ExecuteSynchronously ;否则它会弹回线程池。 ContinueWith 特别适用于并行编程场景。

TaskCompletionSource 可以创建一个任务,它不包含任何必须在后面启动和结束的操作。它的实现原理是提供一个可以手工操作的 “附属” 任务 —— 用于指定操作完成或出错的时间。这种方法非常适合于 I / O 密集作业 : 可以利用所有任务的优点 (它们能够生成返回值、异常和延续) ,但不会在操作执行期间阻塞线程。

TaskCompletionSource 用法很简单、直接初始化就可以。它包含一个 Task 属性,它返回一个可以等待和附加延续的任务 —— 和其他任务一样。然而,这个任务完全通过下面的方法由 TaskCompletionSource 对象进行控制。

(P504)

调用这些方法,就可以给任务发送信号,将任务修改为完成、异常或取消状态。这些方法都只能调用一次 : 如果多次调用 SetResult 、 SetException 或 SetCanceled ,它们就会抛出异常,而 Try * 等方法则会返回 false 。

TaskCompletionSource 的真正作用是创建一个不绑定线程的任务。

(P505)

Delay 方法非常实用,因此它成为 Task 类的一个静态方法。

Task.Delay 是 Thread.Sleep 的异步版本。

(P506)

同步操作 (synchronous operation) 在返回调用者之前才完成它的工作。

在大多数情况下,异步操作 (asynchronous operation) 则在返回调用者之后才完成它的工作。

异步方法使用频率较小,并且需要初始化并发编程,因为它的作业会继续与调用者并行处理。

异步方法一般会快速 (或立刻) 返回给调用者;因此,它们也称为非阻塞方法。

到目前为止,我们学习的异步方法都可以认为是通用方法 :

  1. Thread.Start ;

  2. Task.Run ;

  3. 给任务附加延续的方法;

异步编程的原则是以异步方式编写运行时间很长 (或可能很长) 的函数。这与编写长运行时间函数的传统同步方法相反,它会在一个新线程或任务上调用这些函数,从而实现所需要的并发性。

异步方法的不同点是它会在长运行时间函数之中而非在函数之外初始化并发性。这样做有两个优点 :

  1. I / O 密集并发性的实现不需要绑定线程,因此可以提高可伸缩性和效率;

  2. 富客户端应用程序可以减少工作者线程的代码,因此可以简化线程的安全实现;

在传统的同步调用图中,如果图中出现一个运行时间很长的操作,我们就必须将整个调用图转移到一个工作者线程中,以保证 UI 的高速响应。因此,我们最终会得到一个跨越许多方法的并发操作 (过程级并发性) ,而且这时需要考虑图中每一个方法的线程安全性。

使用异步调用图,就可以在真正需要时才启动线程,因此可以降低调用图中线程的使用频率 (或者在特定操作中完全不需要使用线程,如 I / O 密集操作) 。其他方法则可以在 UI 线程上运行,从而可以大大简化线程安全性的实现。

(P507)

Metro 和 Silverlight .NET 鼓励使用异步编程,甚至一些运行时间较长的方法完全不会出现同步执行版本。相反,它们使用一些可以返回任务 (或者可以通过扩展方法转换为任务的对象) 的异步方法。

任务非常适合异步编程,因为它们支持异步编程所需要的延续。编写 Delay 时使用了 TaskCompletionSource ,它是一种实现 “底层” I / O 密集异步方法的标准方法。

(P509)

如果不想增加程序复杂性,那么必须使用 async 和 await 关键字实现异步性。

(P510)

C# 5.0 引入了 async 和 await 关键字。这两个关键字可用于编写异步代码,它具有与同步代码相当的结构和简单性,并且摒弃了异步编程的复杂结构。

为了完成编译,我们必须在包含的方法上添加 async 修饰符。

(P511)

修饰符 async 会指示编译器将 await 视为一个关键字,而非在方法中随意添加的修饰符 (这样可以保证 C# 5.0 之前编写并使用 await 作为修饰符的代码不会出现编译错误) 。

async 修饰符只能应用到返回 void 、 Task 或 Task 的方法 (和 lambda 表达式) 上。

添加 async 修饰符的方法就是所谓的异步函数,因为它们通常本身也是异步的。

await 表达式的最大特点在于它们可以出现在代码的任意位置。具体地, await 表达式可以出现在异步方法中除 catch 或 finally 语句块、 lock 表达式、 unsafe 上下文或可执行入口 (Main 方法) 之外的任意位置。

(P513)

直接并发的代码要避免访问共享状态或 UI 控件。

(P514)

在 C# 5.0 之前,异步编程很难实现,原因不仅仅在缺少语言支持,还因为 .NET 框架是通过 EAP 和 APM 等模式实现异步功能,而非通过任务返回方法。

(P515)

在调用图上层启动工作者线程是很冒险的做法。

如果使用异步函数,则可以将返回类型 void 修改为 Task ,使方法本身适合采用异步实现 (即可等待的) ,其他方面都不需要修改。

注意,方法体内不需要显式返回一个任务。编译器会负责生成任务,它会在方法完成或者出现未处理异常时发出信号。这样就很容易创建异步调用链。

编译器会扩展异步函数,它会将任务返回给使用 TaskCompletionSource 的代码,用于创建任务,然后再发送信号或异常中止。

(P516)

当一个返回任务的异步方法结束时,执行过程会返回等待它的程序 (通过一个延续) 。

如果方法体返回 TResult ,则可以返回一个 Task

(P517)

使用 C# 异步函数进行程序设计的基本原则 :

  1. 以同步方式编写方法;

  2. 使用异步方法调用替换同步方法,然后等待它们;

  3. 除了 “最顶级的” 方法 (一般是 UI 控件的事件处理器) ,将异步方法的返回类型修改为 Task 或 Task ,使它们变成可等待的方法;

编译能够为异步函数创建任务,意味着在很大程度上,我们只需要在创建 I / O 密集并发性的底层方法中显式创建一个 TaskCompletionSource 实例。 (而对于创建计算密集并发性的方法,则可以使用 Task.Run 创建函数) 。

(P519)

只要添加 async 关键字、 未命名 (unnamed) 方法 (lambda 表达式和匿名方法) 也一样可以采用异步方式执行。

在 WinRT 中,与 Task 等价的是 IAsyncAction ,而与 Task 等价的是 IAsyncOperation (位于 Windows.Foundation 命名空间) 。

这两个类都可以通过 System.Runtime.WindowsRuntime.dll 程序集的 AsTask 扩展方法转换为 Task 或 Task 。这个程序集也定义了一个 GetAwaiter 方法,他可以操作 IAsyncAction 和 IAsyncOpera
tion 类型,它们可以直接执行等待操作。

(P520)

由于 COM 类型系统的限制, IAsyncOperation 并不是基于 IAsyncAction ,它们继承一个通用基本类型 IAsyncInfo 。

AsTask 方法也有重载方法,可以接受一个取消令牌和一个对象 IProgress

AsyncVoidMethodBuilder 会捕捉未处理异常 (在无返回值的异步函数中) ,然后将它们提交到同步上下文中 (如果有) ,以保证触发全局异常处理事件。

(P521)

注意,在 await 之前或之后抛出异常并没有任何区别。

(P526)

Framework 4.5 提供了大量返回任务的异步方法,它们都可用于代替 await (主要与 I / O 相关) 。很多方法 (至少有一部分) 采用了一种基于任务的异步模式 (Task-based Asynchronous Pattern , TAP) ,这是到目前为止最合理的形式。一个 TAP 方法必须 :

  1. 返回一个 “热” (正在运行的) Task 或 Task

  2. 拥有 “Async” 后缀 (除了一些特殊情况) ;

  3. 如果支持取消 和 / 或 进度报告,重载后可接受取消令牌 和 / 或 IProgress

  4. 快速返回调用者 (具有一小段初始同步阶段) ;

  5. 在 I / O 密集操作中不占线程 ;

TAP 方法很容易通过 C# 异步函数实现。

(P527)

使用统一协议调用异步函数 (它们都一致返回任务) 的一个优点是,可以使用和编写任务组合器 (Task Combinator) —— 一些适用于组合各种用途的任务的函数。

CLR 包含两个任务组合器 : Task.WhenAny 和 Task.WhenAll 。

Task.WhenAny 返回这样一个任务 : 当任务组中任意一个任务完成,它也就完成。

(P528)

Task.WhenAll 返回这样一个任务 : 当传入的所有任务都完成时,它才完成。

(P530)

最老的模式是 APM (Asynchronous Programming Model) ,它使用一对以 “Begin” 和 “End” 开头的方法,以及一个接口 IAsyncResult 。

基于事件的异步模式 (Event-based Asynchronous Pattern, EAP) 在 Framework 2.0 时引入,它是代替 APM 的更简单方法,特别是在 UI 场景中。然而,他只能通过有限的类型实现。

EAP 只是一个模式,它并没有任何辅助类型。

实现 EAP 需要编写大量的模板代码,因此这个模式的代码相当复杂。

(P532)

位于 System.ComponentModel 的 BackgroundWorker 是 EAP 的通用实现。它允许富客户端应用启动一个工作者线程,然后执行完成和报告百分比进度,而不需要显式捕捉同步上下文。

RunWorkerAsync 启动操作,然后触发一个池化工作者线程的 DoWork 事件。它还会捕捉同步上下文,而且当操作完成或出错时, RunWorkerCompleted 事件就会通过同步上下文触发 (像延续一样) 。

BackgroundWorker 可以创建过程级并发性,其中 DoWork 事件完全运行在工作者线程上。如果需要在该事件处理器上更新 UI 控件 (而非提交完成百分比进度) ,则必须使用 Dispatcher.BeginInvoke 或类似的方法。

【第15章】

(P533)

System.IO 命名空间中的类型,即底层 I / O 功能的基础。

.NET Framework 也支持一些更高级的 I / O 功能,形式包括 SQL 连接和命令 、 LINQ to SQL 和 LINQ to XML 、 WCF 、 Web Services 和 Remoting 。

.NET 流体系结构主要包括以下概念 : 后备存储流、装饰器流和流适配器。

后备存储是支持输入和输出的终端,例如文件或网络连接。准确地说,它可以是下面的一种或两种 :

  1. 支持顺序读取字节的源;

  2. 支持顺序写入字节的目标;

但是,除非对程序员公开,否则后备存储是无用的。

Stream 正是实现这个功能的标准 .NET 类;它支持标准的读、写和寻址方法。与数组不同,流不是直接将所有数据保存到内存中,而是按序列方式处理数据 —— 一次一个字节或一个可管理大小的块。因此,无论后备存储的大小如何,流都只占用很少的内存。

流分成两类 :

后备存储流 —— 它们是与特定类型后备存储硬连接的, 例如 FileStream 或 NetworkStream ;

装饰器流 —— 它们使用另一种流,以某种方式转换数据,例如 DeflateStream 或 CryptoStream ;

(P534)

装饰器流具有以下体系结构优势 :

  1. 它们能够释放用于实现自我压缩和加密的后备存储流;

  2. 在装饰后,流不受接口变化的影响;

  3. 装饰器支持实时连接;

  4. 装饰器支持相互连接 (例如,压缩器后紧跟一个加密器) ;

后备存储流和装饰器流都只支持字节。虽然这种方式既灵活又高效,但是应用程序通常采用更高级的方式,例如文本或 XML 。通过在一个类中创建专门支持特定格式的类型化方法,并在这个类中封装一个流,适配器弥补了这个缺陷。

适配器会封装一个流,这与装饰器类似。然而,与装饰器不同的是,适配器本身不是一个流;它一般会完全隐藏面向字节的方法。

总之,后备存储流负责处理原始数据;装饰器流支持透明的二进制转换。

适配器支持一些处理更高级类型的类型化方法。

为了构成一个关系链,我们只需要将一个对象传递给另一个对象的构造函数。

抽象的 Stream 类是所有流的基类。它定义了三种基础操作的方法和属性 : 读取、写入和查找;以及一些管理任务,例如关闭、清除和配置超时。

(P536)

要实现异步读或写,只需要调用 ReadAsync / WriteAsync ,替代 Read / Write ,然后等待表达式。

使用异步方法,不需要捆绑线程就可以轻松编写适应慢速流 (可能是网络流) 的响应式和可扩展应用。

一个流可能支持只读、只写、读写。如果 CanWrite 返回 false ,那么流就是只读的;如果 CanRead 返回 false ,那么流就是只写的。

Read 可以将流中的一个数据块读取到数组中。它返回接收到的一些字节,字节数一定小于或等于 count 参数。如果它小于 count ,那么表示已经到达流的结尾,或者流是以小块方式提供数据的 (通常是网络流) 。无论是哪一种情况,数组的剩余字节都是不可写的,它们之前的值都是保留的。

(P537)

ReadByte 方法简单一些 : 它每次只读取一个字节,在流结束时返回 -1 。ReadByte 实际上返回的是一个 int ,而不是 byte ,因为后者不能为 -1 。

Write 和 WriteByte 方法都支持将数据发送到流中。当它们无法发送指定的字节时,就会抛出一个异常。

在 Read 和 Write 方法中,参数 offset 指的是缓冲数组中开始读或写的索引位置,而不是流中的位置。

如果 CanSeek 返回 true ,那么表示流是可查找的。在一个可查找的流中 (例如文件流) ,我们可以通过调用 SetLength 查询或修改它的 Length ,也可以随时修改正在读写的 Position 。 Position 属性是与流的开始位置相关的;然而,Seek 方法则支持移动到流的当前位置或结束位置。

修改 FileStream 的 Position 属性一般需要几毫秒时间。如果要在循环中执行几百万次位置修改,那么 Framework 4.0 中新的 MemoryMappedFile 类可能比 FileStream 更适合。

如果流不支持查找 (例如加密流) ,那么确定其长度的唯一方法是遍历整个流。而且,如果需要重新读取之前的位置,必须先关闭这个流,然后再重新从头开始读取。

流在使用完毕之后必须清理,以释放底层资源,例如文件和套接字句柄。一个保证关闭的简单方法是在块中初始化流。通常,流采用以下标准的清理语法 :

  1. Dispose 和 Close 的功能相同;

  2. 重复清除或关闭流不会产生错误;

(P538)

关闭一个装饰流会同时关闭装饰器及其后备存储流。在装饰器系列中,关闭最外层的装饰器 (系列的头部) 会关闭整个系列。

有一些流 (例如文件流) 会将数据缓冲到后备存储并从中取回数据,减少回程,从而提升性能。这意味着写入到流中的数据不会直接存储到后备存储器;而是等到缓冲区填满时再写入。Flush 方法可以强制将所有内部缓冲的数据写入。当流关闭时,Flush 会自动被调用。

如果 CanTimeout 返回 true ,那么流支持读写超时设定。网络流支持超时设定;文件流和内存流则不支持。对于支持超时设定的流,ReadTimeout 和 WriteTimeout 属性可用来确定以毫秒为单位的预期超时时间,其中 0 表示不设定超时。 Read 和 Write 方法会在超时发生时抛出一个异常。

通过 Stream 的静态 Null 域,能够获得一个 “空流” 。

(P539)

FileStream 不适用于 Metro 应用。相反,要转而使用 Windows.Storage 的 Windows Runtime 类型。

实例化 FileStream 的最简单方法是使用 File 类的以下静态方法之一 :

  1. File.OpenRead() // 只读;

  2. File.OpenWrite() //只写;

  3. File.Create() // 读-写;

如果文件已经存在,那么 OpenWrite 和 Create 的行为是不同的。 Create 会截去全部已有的内容; OpenWrite 则会原封不动地保留流中从位置 0 开始的已有内容。如果写入的字节小于文件已有字节,那么 OpenWrite 所产生的流会同时保存新旧内容。

我们还可以直接实例化一个 FileStream 。它的构造函数支持所有特性,允许指定文件名或底层文件句柄、文件创建和访问模式、共享选项、缓冲和安全性。

下面的静态方法能够一次性将整个文件读取到内存中 :

  1. File.ReadAllText (返回一个字符串);

  2. File.ReadAllLines (返回一个字符串数组);

  3. File.ReadAllBytes (返回一个字节数组);

下面的静态方法能够一次性写入一个完整的文件 :

  1. File.WriteAllText ;

  2. File.WriteAllLines ;

  3. File.WriteAllBytes ;

  4. File.AppendAllText (适用于给日志文件附加内容) ;

从 Framework 4.0 开始,增加了一个静态方法 File.ReadLines 。这个方法与 ReadAllLines 类似,唯一不同的是它返回一个延后判断的 IEnumerable 。这个方法效率更高,因为它不会一次性将整个文件加载到内存中。

(P540)

文件名可以是绝对路径,也可以是当前目录的相对路径。我们可以通过静态的 Environment.CurrentDirectory 属性来访问或修改当前目录。

当程序启动时,当前目录不一定是程序执行文件所在的路径。因此,一定不要使用当前目录来定位与可执行文件一起打包的额外运行时文件。

AppDomain.CurrentDomain.BaseDirectory 会返回应用程序根目录,正常情况下它就是程序可执行文件所在的文件夹。使用 Path.Combine 可以指定相对于这个目录的文件名。

我们还可以通过 UNC 路径读写一个网络文件。

FileStream 的所有构造函数接受文件名需要一个 FileMode 枚举参数。

用 File.Create 和 FileMode.Create 处理隐藏文件会抛出一个异常。必须先删除隐藏文件再重新创建。

只使用文件名和 FieMode 创建一个 FileStream 会得到 (只有一种异常) 一个可读写的流。如果传入一个 FileAccess 参数,就可以要求降低读写模式。

(P541)

FileMode.Append 是最奇怪的一个方法 : 使用这个模式会得到只写流。相反,要附加读写支持,我们使用 FileMode.Open 或 FileMode.OpenOrCreate ,然后再查找流的结尾。

创建 FileStream 时可以选择的其他参数 :

  1. 一个 FileShare 枚举值,描述了在完成文件处理之前,可以给同一个文件的其他进程授予的访问权限 (None 、 Read[default] 、 ReadWrite 或者 Write) ;

  2. 以字节为单位的内部缓冲区大小 (当前的默认值是 4KB) ;

  3. 一个标记,表示是否由操作系统管理异步 I / O ;

  4. 一个 FileSecurity 对象,描述给新文件分配什么用户和角色权限;

  5. 一个 FileOptions 标记枚举值,包括请求操作系统加密 (Encrypted) 、在临时文件关闭时自动删除 (DeleteOnClose) 和优化提示 (RandomAccess 和 SequentialScan) 。此外,还有一个 WriteThrough 标记要求操作系统禁用写后缓存,适用于事务文件或日志。

使用 FileShare.ReadWrite 打开一个文件允许其他进程或用户同时读写同一个文件。为了避免混乱,我们可以使用以下方法在读或写文件之前锁定文件的特定部分 :

public virtual void Lock(long position, long length);

public virtual voio UnLock(long position, long length);

如果所请求的文件段的部分或全部已经被锁定,那么 Lock 会抛一个异常。

MemoryStream 使用一个数组作为后备存储。这在一定程度是与使用流的目的相违背的,因为整个后备存储都必须一次性驻留在内存中。然而, MemoryStream 仍然有一定的用途,一个示例是随机访问一个不可查找的流。

(P542)

调用 ToArray 可以将一个 MemoryStream 转换为一个字节数组。GetBuffer 方法也可以实现相同操作,而且效率更高,它将返回一个底层存储数组的直接引用。但是,这个数组通常会比流的实际长度长一些。

MemoryStream 的关闭和清除不是必需的。如果关闭了一个 MemoryStream ,我们就无法再读或写这个流,但是我们仍然可以调用 ToArray 来获得底层数据。消除实际上不会对内存流执行任何操作。

PipeStream 是在 Framework 3.5 引入的。它支持一种简单的方法,其中一个进程可以通过 Windows 管道协议与另一个进程进行通信。

管道的类型有两种 :

  1. 匿名管道 —— 支持在同一个 computer.id 的父子进程之间单向通信;

  2. 命名管道 —— 支持同一台计算机或 Windows 网络中不同计算机的任意进程之间进行通信;

管道很适合用于在同一台计算机上进行进程间通信 (IPC) : 它不依赖于任何网络传输,性能更高,也不会有防火墙问题。

管道是基于流实现的,所以一个进程会等待接收字节,而另一个进程则负责发送字节。另一种进程通信方法可以通过共享内存块实现。

PipeStream 是一个抽象类,它有 4 个实现子类。其中两个支持匿名管道和两个支持命名管道 :

  1. 匿名管道 —— AnonymousPipeServerStream 和 AnonymousPipeClientStream ;

  2. 命名管道 —— NamedPipeServerStream 和 NamedPipeClientStream ;

命名管道使用更简单。

(P543)

管道是一个底层概念,它支持发送和接收字节 (或消息,即字节组) 。

WCF 和 Remoting API 支持使用 IPC 通道进行通信的更高级消息框架。

通过命名管道,各方将使用一个同名管道进行通信。这个协议定义了两个不同的角色 : 客户端和服务器。客户端与服务器之间的通信采用以下方式 :

  1. 服务器初始化一个 NamedPipeServerStream , 然后调用 WaitForConnection ;

  2. 客户端初始化一个 NamedPipeClientStream , 然后调用 Connect (使用一个可选的超时时间) ;

命名管道流默认是双向通信的,所以任何一方都可以读或写它们的流。这意味着客户端和服务器都必须同意使用一种协议来协调它们的操作,所以双方是不能同时发送或接收消息的。

通信双方还需要统一每次传输的数据长度。

为了支持传输更长的消息,管道提供了一种消息传输模式。如果启用这个模式,调用 Read 的一方可以通过检查 IsMessageComplete 属性来确定消息是否完成传输。

(P544)

只需要等待 Read 返回 0 ,我们就可以确定一个 PipeStream 是否完成消息的读取。这是因为,与其他大多数流不同,管道流和网络流并没有确定的结尾。相反,它们会在消息传输期间临时中断。

匿名管道支持在父子进程之间进行单向通信。匿名管道不使用系统级名称,而是通过一个私有句柄进行调整。

与命名管道一样,匿名管道也区分客户端和服务器角色。然而,通信系统有一些不同,它采用以下方法 :

  1. 服务器初始化一个 AnonymousPipeServerStream ,提交一个 In 或 Out 的 PipeDirection ;

  2. 服务器调用 GetClientHandleAsString 获取管道的标识,然后传递回客户端 (一般作为启动子进程的一个参数) ;

  3. 子进程初始化一个 AnonymousPipeClientStream ,指定相反的 PipeDirection ;

  4. 服务器调用 DisposeLocalCopyOfClientHandle ,释放第 2 步产生的本地句柄;

  5. 父子进程通过 读 / 写 流来进行通信;

因为匿名管道是单向的,所以服务器必须为双向通信创建两个管道。

(P545)

与命名管道一样,客户端和服务器必须协调它们的发送和接收,并且统一每一次传输的数据长度。但是,匿名管道不支持消息模式,所以必须实现自己的消息长度认同协议。一种方法是在每次传输的前 4 个字节中发送一个整数值,定义后续消息的长度。

BitConverter 类具有一些用于转换整数和 4 字节数组的方法。

BufferedStream 可以装饰或包装另一个具有缓冲功能的流,它是 .NET Framework 的诸多核心装饰流类型之一。

(P546)

缓冲能够减少后备存储的方法,从而提高性能。

组合使用 BufferedStream 和 FileStream 的好处并不明显,因为 FileStream 已经有内置的缓冲了。它的唯一用途可能就是扩大一个已有 FileStream 的缓冲区。

关闭一个 BufferedStream 会自动关闭底层的后备存储流。

Stream 只支持字节处理;要读写一些数据类型,例如字符串、整数或 XML 元素,我们必须插入适配器。下面是 Framework 支持的适配器 :

  1. 文本适配器 (处理字符串和字符数据) —— TextReader 、 TextWriter 、 StreamReader 、 StreamWriter 、 StringReader 、 StreamWriter ;

  2. 二进制适配器 (处理基本数据类型,例如 int 、 bool 、 string 和 float) —— BinaryReader 、 BinaryWriter ;

  3. XML 适配器 —— XmlReader 、 XmlWriter ;

(P547)

TextReader 和 TextWriter 都是专门处理字符和字符串的适配器的抽象基类。它们在框架中都是两个通用的实现 :

  1. StreamReader / StreamWriter —— 使用 Stream 存储它的原始数据,将流的字节转换成字符或字符串;

  2. StringReader / StringWriter —— 使用内存字符串实现 TextReader / TextWriter ;

不需要将位置前移,Peek 就可以返回流中的下一个字符。

如果到达流的末尾,那么 Peek 与不带参数的 Read 都会返回 -1 ;否则,它们会返回一个能够强制转换为 char 的整数。

接收一个char[] 缓冲区参数的 Read 重载函数功能与 ReadBlock 方法相似。

Windows 的新换行字符是模仿机械打字机的 : 回车符后面加上一个换行符。 C# 字符串表示是 “\r\n” 。如果顺序调换,结果可能是两行,也可能一行也没有。

WriteLine 会给指定文本附加 CR + LF 。我们可以使用 NewLine 属性修改这些字符,这对于支持 UNIX 文件格式的互操作性很有用。

和 Stream 一样,TextReader 和 TextWriter 为它们的 读 / 写 方法提供了基于任务的异步版本。

因为文本适配器通常与文件有关,所以 File 类也有一些静态方法支持快捷处理,例如 CreateText 、 AppendText 和 OpenText 。

(P549)

TextReader 和 TextWriter 本身是与流或后备存储无关的抽象类。然而,类型 StreamReader 和 StreamWriter 都与底层的字节流相关,所以它们必须进行字符和字节之间的转换。它们是通过 System.Text 命名空间的 Encoding 类进行这些操作的,创建 StreamReader 或 StreamWriter 需要选择一种编码方式。如果不进行选择,那么就使用默认的 UTF-8 编码。

如果明确指定一个编码方式,默认情况下 StreamWriter 会在流开头写入一个前缀,用于指定编码方式。这通常不是一种好做法。

最简单的编码方式是 ASCII ,因为每一个字符都是用一个字节表示的。

ASCII 编码将 Unicode 字符集的前 127 个字符映射为一个字节,其中包括键盘上的所有字符。

默认的 UTF-8 编码方式也能够映射所有分配的 Unicode 字符,但是更复杂一些。它将前 127 个字符编码为一个字节,以兼容 ASCII ;其他字符则编码为一定数量的字节 (通常是两个或三个) 。

UTF-8 在处理西方字母时很高效,因为最常用的字符只需 1 个字节。只需要忽略 127 之后的字节,它就能够轻松向下兼容 ACSII 。缺点是在流中查找是很麻烦的,因为字符的位置与它在流中的字节位置是无关的。

另一种方式是 UTF-16 (在 Encoding 类中仅仅标记为 “Unicode”) 。

技术上, UTF-16 使用 2 个或 4 个字节来表示一个字符 (所分配或保护的 Unicode 字符接近一百万个,所以 2 个字节并不总是足够的) 。然而,因为 C# 的 char 类型本身只有 16 位,所以 UTF-16 编码方式总是使用 2 个字节来表示一个 .NET 的 char 类型。这样就能够很容易转到流中特定的字符索引。

UTF-16 使用 2 个字节前缀来确定字节对采用 “小字节序” 还是 “大字节序” (最低有效字节在前还是最高有效字节在前) 。 Windows 系统采用的默认标准是小字节序。

(P551)

StringReader 和 StringWriter 适配器并不封装流;相反,它们使用一个字符串或 StringBuilder 作为底层数据源。这意味着不需要进行任何的字节转换,事实上,这些类所执行的操作都可以通过字符串或 StringBuilder 与一个索引变量轻松实现。并且它们的优点是与 StreamReader / StreamWriter 使用相同的基类。

BinaryReader 和 BinaryWriter 能够读写基本的数据类型 : bool 、 byte 、 char 、 decimal 、 float 、 double 、 short 、 int 、 long 、 sbyte 、 unshort 、 uint 和 ulong 以及字符串和数组等。

与 StreamReader 和 StreamWriter 不同的是,二进制适配器能够高效地存储基本数据类型,因为它们位于内存中。所以,一个 int 占用 4 个字节;一个 double 占用 8 个字节。字符串是通过文本编码 (与 StreamReader 和 STreamWriter 一样) 写入的,但是带有长度前缀,从而不需要特殊分隔符就能够读取一系列字符串。

(P552)

BinaryReader 也支持读入字节数组。

清理流适配器有 4 种方法 :

  1. 只关闭适配器;

  2. 先关闭适配器,然后再关闭流;

  3. (对于编写器) 先清理适配器,然后再关闭流;

  4. (对于读取器) 直接关闭流;

对于适配器和流, Close (关闭) 和 Dispose (清理) 是同义词。

关闭一个适配器会自动关闭底层的流。

因为嵌入语句是从内向外清理的,所以适配器先关闭,然后再关闭流。

一定不要在关闭和清理编写器之前关闭一个流,这样会丢失仍在适配器中缓存的所有数据。

(P553)

我们要调用 Flush 来保证将 StreamWriter 的缓冲区数据写入到底层的流中。

流适配器及其可选的清理语法并没有实现扩展的清理模式,即在终结器中调用 Dispose 。这可以避免垃圾回收器找到弃用的适配器时自动清理这个适配器。

从 Framework 4.5 开始, StreamReader / StreamWriter 有一个新的构造方法,它可以让流在清理之后仍然保持打开。

System.IO.Compression 命名空间提供了两个通用压缩流 : DeflateStream 和 GZipStream 。这两个类都使用与 ZIP 格式类似的流行压缩算法。它们的区别是 : GZipStream 会在开头和结尾写入一个额外的协议 —— 其中包括检测错误的 CRC 。 GZipStream 还遵循一个其他软件可识别的标准。

这两种流都支持读写操作,但是有以下限制条件 :

  1. 压缩时总是在写入流;

  2. 解压缩时总是在读取流;

DeflateStream 和 GZipStream 都是装饰器;它们负责压缩或解压缩构造方法传入的另一个流。

非重复性二进制文件数据的压缩效果很差 (缺少设计规范性的加密数据的压缩比是最差的), 这种压缩适用于大多数文本文件。

在 DeflateStream 构造方法传入的额外标记,表示在清除底层流时不采用普通的协议。

(P555)

Framework 4.5 引入了一个新特性 : 支持流行的 Zip 文件压缩格式,实现方法是 System.IO.Compression 中 (位于 System.IO.Compression.dll) 新增加的 ZipArchive 和 ZipFile 类。与 DeflateStream 和 GZipStream 相比,这种格式的优点是可以处理多个文件,并且兼容 Windows 资源管理器及其他压缩工具创建的 Zip 文件。

ZipArchive 可以操作流,而 ZipFile 则负责操作更常见的文件。 (ZipFile 是 ZipArchive 的静态帮助类) 。

ZipFile 的 CreateFromDirectory 方法可以将指定目录的所有文件添加到一个 Zip 文件中。

而 ExtractToDirectory 则执行相反操作,可以将一个 Zip 文件解压缩到一个目录中。

在压缩时,可以指定是否优化文件大小或压缩速度,以及是否在存档文件中包含源文件目录名称。

ZipFile 包含一个 Open 方法,它可以 读 / 写 各个文件项目。这个方法会返回一个 ZipArchive 对象 (也可以通过使用一个 Stream 对象创建 ZipArchive 实例而获得) 。当调用 Open 时,必须指定一个文件名,并且指定存档文件操作方式 : Read 、 Create 或 Update 。然后,使用 Entries 属性遍历现有的项目,或者使用 GetEntry 查询某个文件。

ZipArchiveEntry 还有 Delete 方法, ExtractToFile 方法 (实际是 ZipFileExtensions 类的扩展方法) 和 Open 方法 (返回一个 可读 / 可写 的 Stream) 。调用 ZipArchive 的 CreateEntry 或者 CreateEntryFromFile 扩展方法,可以创建新项目。

使用 MemoryStream 创建 ZipArchive ,也可以在内存中实现相同效果。

System.IO 命名空间有一些执行 “实用的” 文件与目录操作的类型。对于大多数特性,我们可以选择两种类型 : 一种采用静态方法,另一种采用实例方法 :

  1. 静态类 —— File 和 Directory ;

  2. 实例方法类 (使用文件或目录名创建) —— FileInfo 和 DirectoryInfo ;

(P556)

此外,还有一个静态类 Path ,它不操作文件或目录;相反,它具有一些文件名或目录路径的字符处理方法。 Path 也能够帮助处理临时文件。

所有这些类都不适用于 Metro 应用。

File 是一个静态类,它的方法都接受文件名参数。这个文件名可以是相对当前目录的路径,也可以是一个目录的完整路径。

如果目标文件已存在,那么 Move 会抛出一个异常;但是 Replace 不会,这两个方法允许将文件重命名或移动到另一个目录。

如果文件被标记为只读,那么 Delete 会抛出一个 UnauthorizedAccessException ;调用 GetAttribtes 可以预先判断其属性。

(P557)

FileInfo 提供了一个更简单的修改文件只读标记的方法 (IsReadOnly) 。

执行解压缩,可以将 CompressEx 替换成 UncompressEx 。

透明加密和压缩需要特殊的文件系统支持。 NTFS (硬盘中使用最广泛的格式) 支持这些特性; CDFS (在 CD-ROM 中) 和 FAT (在可移动内存卡中) 则不支持。

(P558)

GetAccessControl 和 SetAccessControl 方法支持通过 FileSecurity 对象 (位于命名空间 System.Security .AccessControl) 查询和修改操作系统授予用户和角色的权限。在创建一个新文件时,我们可以给FileStream 的构造函数传入一个 FileSecurity ,以指定它的权限。

(P559)

静态的 Directory 类具有一组与 File 类相似的方法,用于检查目录是否存在 (Exists) 、移动目录 (Move) 、 删除目录 (Delete) 、获取 / 设置 创建时间或最后访问时间,以及 获取 / 设置 安全权限。

使用 File 和 Directory 的静态方法,我们可以方便地执行一个文件或目录操作。如果需要一次性调用多个方法, FileInfo 和 DirectoryInfo 类支持一种简化这种调用的对象模型。

FileInfo 以实例方法的形式支持大部分的 File 静态方法以及一些额外的属性,例如 Extension 、 Length 、 IsReadOnly 和 Directory (返回一个 DirectoryInfo 对象) 。

(P560)

静态的 Path 类定义了一些处理路径和文件名的方法和字段。

(P561)

Combine 是非常有用的,它可用来组合目录和文件名或者两个目录,而不需要先检查名称后面是否有反斜杠。

GetFullPath 可以将一个相对于当前目录的路径转换为一个绝对路径。它接受例如 ....\file.txt 这样的值。

GetRandomFileName 会返回一个完全唯一的 8.3 格式文件名,但不会真正创建文件。

GetTempFileName 会使用一个自增计数器生成一个临时文件名,这个计数器每隔 65,000 次重复一遍。然后,它会用这个名称在本地临时目录创建一个 0 字节的文件。

System.Environment 类的 GetFolderPath 方法提供查找特殊文件夹的功能。

Environment.SpecialFolder 是一个枚举类型,它的值包括 Windows 中的所有特殊目录。

(P563)

DriveInfo 类可用来查询计算机的驱动器信息。

(P564)

静态的 GetDrives 方法会返回所有映射的驱动器,包括 CD-ROM 、内存卡和网络连接。

FileSystemWatcher 类可用来监控一个目录 (或者子目录) 的活动。当有文件或子目录被创建、修改、重命名、删除以及属性变化时, FileSystemWatcher 都会触发相应的事件。无论是用户还是进程执行这些操作,这些事件都会触发。

(P565)

因为 FileSystemWatcher 在一个独立线程上接收事件,所以事件处理代码中必须使用异常处理语句,防止错误使应用程序崩溃。

Error 事件不会通知文件系统错误;相反,它表示的是 FileSystemWatcher 的事件缓冲区溢出,因为它已经被 Changed 、 Created 、 Deleted 或 Renamed 占用。我们可以通过 InternalBufferSize 属性修改缓冲区大小。

IncludeSubdirectories 会递归执行。

Metro 应用都不能使用 FileStream 和 Directory / File 类。相反, Windows.Storage 命名空间包含一些具有相同用途的 WinRT 类型,其中两个主要类是 StorageFolder 和 StorageFile 。

StorageFolder 类表示一个目录,调用 StorageFolder 的静态方法 GetFolderFromPathAsync ,指定文件夹的完整路径,就可以获得一个 StorageFolder 对象。

(P566)

StorageFile 是操作文件的基础类。使用静态类 StorageFile.GetFileFromPathAsync ,可以使用完整路径获得一个文件实例;调用 StorageFolder 或 IsStorageFolder 对象的 GetFileAsync 方法,则可以使用相对路径获得一个文件实例。

(P567)

内存映射文件是 Framework 4.0 新增加的。它们有两个主要特性 :

  1. 文件数据的高效随机访问;

  2. 在同一台计算机的不同进程之间共享内存;

内存映射文件的类型位于 System.IO.MemoryMappedFiles 命名空间。在内部,它们是封装了支持内存映射文件的 Win32 API 。

虽然常规的 FileStream 也支持随机文件 I / O (通过设置流的 Position 属性实现) ,但是它在连续 I / O 方面进行了优化。一般原则大致是 :

  1. FileStream 的连续 I / O 速度要比内存映射文件快 10 倍;

  2. 内存映射文件的随机 I / O 速度要比 FileStream 快 10 倍;

修改 FileStream 的 Position 属性可能需要耗费几毫秒时间,并在循环中会进一步累加。 FileStream 不适用于多线程访问,因为它在读或写时位置会发生改变。

要创建一个内存映射文件,我们要 :

  1. 获取一个普通的 FileStream ;

  2. 使用文件流实例化 MemoryMappedFile ;

  3. 在内存映射文件对象上调用 CreateViewAccessor ;

最后一步可以得到一个 MemoryMappedViewAccessor 对象,它具有一些随机读写简单类型、结构和数组的方法。

(P568)

内存映射文件也可以作为同一台计算机上不同进程间共享内存的一种手段。一个进程可以调用 MemoryMappedFile.CreateNew 创建一个共享内存块,而另一个进程则可以用相同的名称调用 MemoryMappedFile.OpenExisting 来共享同一个内存块。虽然它仍然是一个内存映射文件,但是已经完全脱离磁盘而进入内存中。

在 MemoryMappedFile 中调用 CreateViewAccessor 可以得到一个视图访问器,它可以用来执行随机位置的 读 / 写。

(P569)

Read / Write 方法可以接受数字类型、 bool 、 char 以及包含值类型元素或域的数组和结构体。引用类型 (及包含引用类型的数组或结构体) 是禁止使用的,因为它们无法映射到一个未托管的内存中。

我们还可以通过指针直接访问底层的未托管内存。

指针在处理大结构时的优势是 : 它们可以直接处理原始数据,而不需要使用 Read / Write 在托管内存和未托管内存之间进行数据复制。

每一个 .NET 程序都可以访问该程序独有的本地存储区域,即独立存储 (isolated storage) 。如果程序无法访问标准文件系统,那么很适合使用独立存储。使用受限 “互联网” 权限的 Silverlight 应用和 ClickOnce 应用就属于这种情况。

(P570)

在安全性方面,隔离存储区的作用更多的是阻止其他的应用程序进入,而不是阻止其中的应用程序出去。隔离存储区的数据受到严格保护,不会受到其他运行在最严格权限集之下的 .NET 应用程序的***。

在沙箱中运行的应用程序可以通过权限设置获得有限的隔离存储区配额。默认情况下,互联网和 Silverlight 应用程序在 Framework 4.0 中的配额是 1MB 。

【第16章】

(P575)

Framework 在 System.Net.* 命名空间中包含各种支持标准网络协议通信的类,例如 HTTP 、 TCP / IP 和 FTP 。下面是其中一些主要组件的小结 :

  1. WebClient 外观类 —— 支持通过 HTTP 或 FTP 执行简单的 下载 / 上传 操作;

  2. WebRequest 和 WebResponse 类 —— 支持更多的客户端 HTTP 或 FTP 操作;

  3. HttpListener 类 —— 可用来编写 HTTP 服务器;

  4. SmtpClient 类 —— 通过支持 SMTP 创建和发送电子邮件;

  5. Dns 类 —— 支持域名和地址之间的转换;

  6. TcpClient 、 UdpClient 、 TcpListener 和 Socket 类 —— 支持传输层和网络层的直接访问。

Framework 支持主要的 Internet 协议,但是它的功能不仅限于 Internet 连接,诸如 TCP / IP 等协议也可以广泛应用于局域网上。

大多数类型都位于传输层或应用层。

传输层定义了发送和接受字节的基础协议 (TCU 或 UDP) ;

应用层则定义支持特定应用程序的上层协议,例如获取 Web 页 (HTTP) 、 传输文件 (FTP) 、 发送邮件 (SMTP) 和域名与 IP 地址转换 (DNS) 。

通常,在应用层编程是最方便的。然而,有一些原因要求我们必须直接在传输层上进行操作,例如当需要使用一种 Framework 不支持的应用程序协议 (例如 POP3) 来接收邮件时。此外,当需要为某个特殊应用程序 (例如对等客户端) 发明一种自定义协议时,也是如此。

HTTP 属于应用层协议,它专门用于扩展通用的通信。它的基本运行方式是 “请给我这个 URL 的网页” ,可以很好地理解为 “返回使用这些参数调用这个方法的结果值” 。 HTTP 具有丰富的特性,它们在多层次业务应用程序和面向服务的体系结构中是非常有用的,例如验证和加密协议、消息组块、可扩展头信息和 Cookie ,并且多个服务器应用程序可以共享一个端口和 IP 地址。因此, HTTP 在 Framework 中得到很好的支持,包括直接支持以及通过 WCF 、 Web Services 和 ASP.NET 等技术实现的更高级支持。

(P576)

Framework 提供 FTP 客户端支持,这是最常用的 Internet 文件发送和接受协议。服务器端支持是通过 IIS 或 UNIX 服务器软件等形式实现的。

DNS (Domain Name Service : 域名服务) —— 域名和 IP 地址转换;

FTP (File Transfer Protocol : 文件传输协议) —— Internet 文件发送和接收的协议;

HTTP (Hypertext Transfer Protocol : 超文本传输协议) —— 查询网页和运行 Web 服务;

IIS (Internet Information Services : Internet 信息服务) —— 微软的 Web 服务器软件;

IP (Internet Protocol : Internet 协议) —— TCP 与 UDP 之下的网络层协议;

LAN (Local Area Network : 局域网) —— 大多数 LAN 使用 TCP / IP 等 Internet 协议;

POP (Post Office Protocol : 邮局协议) —— 查询 Internet 邮件;

SMTP (Simple Mail Transfer Protocol : 简单邮件传输协议) —— 发送 Internet 邮件;

TCP (Transmission and Control Protocol : 传输和控制协议) —— 传输层 Internet 协议,大多数更高级服务的基础;

UDP (Universal Datagram Protocol : 低开销服务使用传输层 Internet 协议,例如 “通用数据报协议” ) ;

UNC (Universal Naming Convention : 通用命名转换) —— \computer\sharename\filename

URI (Uniform Resource Identifier : 统一资源标识符) —— 使用普遍的资源命名系统;

URL (Uniform Resource Locator : 统一资源定位符) —— 技术意义(逐渐停止使用) - URI 子集;流行意义 - URI 简称;

(P577)

要实现通信,计算机或设备都需要一个地址。 Internet 使用了两套系统 :

  1. IPv4 : 这是目前的主流地址系统; IPv4 地址有 32 位。当用字符串表示时, IPv4 地址可以写为用点号分隔的 4 个十进制数。地址可能是全世界唯一的,也可能在一个特定子网中是唯一的;

  2. IPv6 : 这是更新的 128 位地址系统。这些地址用字符串表示为用冒号分隔的十六进制。 .NET Framework 中要求地址加上方括号;

System.Net 命名空间的 IPAddress 类是采用其中一种协议的地址。它有一个构造函数可以接收字节数组,以及一个静态的 Parse 方法接收正确格式的字符串。

TCP 和 UDP 协议将每一个 IP 地址划分为 65535 个端口,从而允许一台计算机在一个地址上运行多个应用程序,每一个应用程序使用一个端口。许多程序都分配有标准端口,例如,HTTP 使用端口 80 ;SMTP 使用端口 25 。

从 49152 到 65535 的 TCP 和 UDP 端口是官方保留的,它们只用于测试和小规模部署。

IP 地址和端口组合在 .NET Framework 中是使用 IPEndPoint 类表示的。

(P578)

防火墙可以阻挡端口。在许多企业环境中,事实上只有少数端口是开放的,通常情况下,只开放端口 80 (不加密 HTTP) 和端口 443 (安全 HTTP) 。

URI 是一个具有特殊格式的字符串,它描述了一个 Internet 或 LAN 的资源,例如网页、文件或电子邮件地址。

正确的格式是由 IETF (Internet Engineering Task Force) 定义的。

URI 一般分成三个元素 : 协议 (scheme) 、 权限 (authority) 和路径 (path) 。

System 命名空间的 Uri 类正是采用这种划分方式,为每一种元素提供对应的属性。

Uri 类适合用来验证 URI 字符串的格式或将 URI 分割成相应的组成部分。另外,可以将 URI 作为一个简单的字符串进行处理,大多数网络连接方法都有接收 Uri 对象或字符串的重载方法。

在构造函数中传入以下字符串之一,就可以创建一个 Uri 对象 :

  1. URI 字符串;

  2. 硬盘中的一个文件的绝对路径;

  3. LAN 中一个文件的 UNC 路径;

文件和 UNC 路径会自动转换为 URI : 添加协议 “file:” ,反斜杠会转换为斜杠。 Uri 的构造函数在创建 Uri 之前也会对传入的字符串执行一些基本的清理操作,包括将协议和主机名转换为小写、删除默认端口号和空端口号。如果传入一个不带协议的 URI 字符串,那么会抛出一个 UriFormatException 。

(P579)

Uri 有一个 IsLoopback 属性,它表示 Uri 是否引用本地主机 (IP 地址为 127.0.0.1) ;以及一个 IsFile 属性,它表示 Uri 是否引用一个本地或 UNC (IsUnc) 路径。如果 IsFile 返回 true , LocalPath 属性会返回一个符合本地操作系统习惯的 AbsolutePath (带反斜杠) ,然后可以用它来调用 File.Open 。

Uri 的实例有一些只读属性。要修改一个 Uri ,我们需要实例化一个 UriBuilder 对象,这是一个可写属性,它可以通过 Uri 属性转换为 Uri 。

Uri 也具有一些比较和截取路径的方法。

URI 后面的斜杠是很重要的,服务器会根据它来决定是否处理路径组成部分。

WebRequest 和 WebResponse 是管理 HTTP 和 FTP 客户端活动及 “file:” 协议的通用基类。它们封装了这些协议共用的 “请求 / 响应” 模型 : 客户端发起请求,然后等待服务器的响应。

WebClient 是一个便利的门店 (facade) 类,它负责调用 WebRequest 和 WebResponse ,可以节省很多编码。 WebClient 支持字符串、字节数组、文件或流;而 WebRequest 和 WebResponse 只支持流。但是, WebClient 也不是万能的,因为它也不支持某些特性 (如 cookie) 。

HttpClient 是另一个基于 WebRequest 和 WebResponse 的类 (更准确说是基于 HttpWebRequest 和 HttpWebResponse) ,并且是 Framework 4.5 新引入的类。

WebClient 主要作为 请求 / 响应 类之上薄薄的一层,而 HttpClient 则增加了更多的功能,能够处理基于 HTTP 的 Web API 、 基于 REST 的服务和自定义验证模式。

(P580)

WebClient 和 HttpClient 都支持以字符串或字节数组方式处理简单的文件 下载 / 上传 操作。它们都拥有一些异步方法,但是只有 WebClient 支持进度报告。

WinRT 应用程序不能使用 WebClient ,它必须使用 WebRequest / WebResponse 或 HttpClient (用于 HTTP 连接) 。

WebClient 的使用步骤 :

  1. 实例化一个 WebClient 对象;

  2. 设置 Proxy 属性值;

  3. 在需要验证时设置 Credentials 属性值;

  4. 使用相应的 URI 调用 DownloadXXX 或 UploadXXX 方法;

UploadValues 方法可用于以 POST 方法参数提交一个 HTTP 表单的值。

WebClient 还包含一个 BaseAddress 属性,可用于为所有地址添加一个字符串前缀。

(P581)

WebClient 被动实现了 IDisposable —— 因为它继承了 Component 。然而,它的 Dispose 方法在运行时并没有执行太多实际操作,所以不需要清理 WebClient 的实例。

从 Framework 4.5 开始, WebClient 提供了长任务方法的异步版本,它们会返回可以等待的任务。

await webClient.DownloadTaskAsync() 这些方法使用 “TaskAsync” 后缀,不同于使用 “Async” 后缀的 EAP 旧异步方法。但是,新方法不支持取消操作和进度报告的标准 “TAP” 模式。相反,在处理延续时,必须调用 WebClient 对象的 CancelAsync 方法;而处理进度报告时,则需要处理 DownloadProgressChanged / UploadProgressChanged 事件。

如果需要使用取消操作或进度报告,那么要避免使用同一个 WebClient 对象依次执行多个操作,因为这样会形成竞争条件。

WebRequest 和 WebResponse 比 WebClient 复杂,但是更加灵活。下面是开始使用的步骤 :

  1. 使用一个 URI 调用 WebRequest.Create ,创建一个 Web 请求实例;

  2. 设置 Proxy 属性;

  3. 如果需要验证身份,则设置 Credentials 属性;

如果要上传数据,则 :

  1. 调用请求对象的 GetRequestStream ,然后在流中写入数据。如果需要处理响应,则转到第 5 步。

如果要下载数据,则 :

  1. 调用请求对象的 GetResponse ,创建一个 Web 响应实例;

  2. 调用响应对象的 GetResponseStream ,然后 (可以使用 StreamReader) 从流中读取数据;

(P582)

静态方法 Create 会创建一个 WebRequest 类型的子类实例。

将 Web 请求对象转换为具体的类型,就可以访问特定协议的特性。

“https:” 协议是指通过安全套接层 (Secure Sockets Layer, SSL) 实现的安全 (加密) HTTP 。 WebClient 和 WebRequest 都会在遇到这种前缀时激活 SSL 。

“file:” 协议会将请求转发到一个 FileStream 对象,其目的是确定一个与读取 URI 一致的协议,它可能是一个网页、 FTP 站点或文件路径。

(P583)

WebRequest 包含一个 Timeout 属性,其单位为毫秒。如果出现超时,那么程序就会抛出一个 WebException 异常,其中包含一个 Status 属性 : WebExceptionStatus.Timeout 。 HTTP 的默认超时时间为 100 秒,而 FTP 的超时时间为无限。

WebRequest 对象不能回收并用于处理多个请求 —— 每一个实例只适用于一个作业。

HttpClient 是 Framework 4.5 新引入的类,它在 HttpWebRequest 和 HttpWebResponse 之上提供了另一层封装。它的设计是为了支持越来越多的 Web API 和 REST 服务,在处理比获取网页等更复杂的协议时实现比 WebClient 更佳的体验。具体地 :

  1. 一个 HttpClient 就可以支持并发请求。如果要使用 WebClient 处理并发请求,则需要为每一个并发线程创建一个新实例,这时需要自定义请求头、 cookie 和 验证模式,因此会比较麻烦;

  2. HttpClient 可用于编写和插入自定义消息处理器。这样可以创建单元测试桩函数,以及创建自定义管道 (用于记录日志、压缩、加密等) 。调用 WebClient 的单元测试代码则很难编写;

  3. HttpClient 包含丰富且可扩展的请求头与内容类型系统;

HttpClient 不能完全代替 WebClient ,因为它不支持进度报告。

WebClient 也有一个优点,它支持 FTP 、 file:// 和 自定义 URI 模式,它也适用于所有 Framework 版本。

使用 HttpClient 的最简单方法是创建一个实例,然后使用 URI 调用其中一个 Get* 方法。

HttpClient 的所有 I / O 密集型方法都是异步的 (它们没有同步实现版本) 。

与 WebClient 不同,想要获得最佳性能的 HttpClient ,必须重用相同的实例 (否则诸如 DNS 解析操作会出现不必要的重复执行)。

HttpClient 允许并发操作。

HttpClient 包含一个 Timeout 属性和一个 BaseAddress 属性,它会为每一个请求添加一个 URI 前缀。

HttpClient 在一定程度上就是一层实现 : 通常使用的大部分属性都定义在另一个类中,即 HttpClientHandler 。

(P584)

GetStringAsync 、 GetByteArrayAsync 和 GetStreamAsync 方法是更常用的 GetAsync 方法的快捷方法。

HttpResponseMessage 包含一些访问请求头 和 HTTP StatusCode 的属性。与 WebClient 不同,除非显式调用 EnsureSuccessStatusCode ,否则返回不成功状态不会抛出异常。然而,通信或 DNS 错误会抛出异常。

HttpResponseMessage 包含一个 CopyToAsync 方法,它可以将数据写到另一个流中,适用于将输入写到一个文件中。

GetAsync 是 HTTP 的 4 种动作相关的 4 个方法之一 (其他方法是 PostAsync 、 PutAsync 和 DeleteAsync) 。

创建一个 HttpRequestMessage 对象,意味着可以自定义请求的属性,如请求头和内容本身,它们可用于上传数据。

在创建一个 HttpRequestMessage 对象之后,设置它的 Content 的属性,就可以上传内容。这个属性的类型是抽象类 HttpContent 。

大多数自定义请求的属性都不是在 HttpClient 中定义,而是在 HttpClientHandler 中定义。后者实际上是抽象类 HttpMessageHandler 的子类。

HttpMessageHandler 非常容易继承,同时也提供了 HttpClient 的扩展点。

(P586)

代理服务器 (proxy server) 是一个中间服务器,负责转发 HTTP 和 FTP 请求。

代理本身拥有地址,并且可能需要执行身份验证,所以只有特定的局域网用户可以访问互联网。

创建一个 WebClient 或 WebRequest 对象,就可以使用 WebProxy 对象通过代理服务器转发请求。

(P587)

如果要使用 HttpClient 访问代理,那么首先要创建一个 HttpClientHandler ,设置它的 Proxy 属性,然后将它传递给 HttpClient 的构造方法。

如果已知不存在代理,那么可以在 WebClient 和 WebRequest 对象上将 Proxy 属性设置为 null 。否则, Framework 可能会尝试自动检查代理设置,这会给请求增加 30 秒延迟。如果 Web 请求执行速度过慢,那么很可能就是这个原因造成的。

HttpClientHandler 还有一个 UseProxy 属性,将它设置为 false ,就可以将 Proxy 属性置空,从而禁止自动检测。

如果在创建 NetworkCredential 时提供一个域,那么就会使用基于 Windows 的身份验证协议。如果想要使用当前已验证的 Windows 用户,则可以在代理的 Credentials 属性上设置静态的 CredentialCache.DefaultNetworkCredentials 值。

创建一个 NetworkCredential 对象,将它设置到 WebClient 或 WebRequest 的 Credentials 属性上,就可以向 HTTP 或 FTP 站点提供用户名和密码。

(P588)

身份验证最终由一个 WebRequest 子类型处理,它会自动协商一个兼容协议。

(P589)

WebRequest 、 WebResponse 、 WebClient 及其流都会在遇到网络或协议错误时抛出一个 WebException 异常。

HttpClient 也有相同行为,但是它将 WebException 封装在一个 HttpRequestException 中。

使用 WebException 的 Status 属性,就可以确定具体的错误类型,它会返回一个枚举值 WebExceptionStatus 。

(P591)

WebClient 、 WebRequest 和 HttpClient 都可以添加自定义 HTTP 请求头,以及在响应中列举请求头信息。请求头只是一些 键 / 值 对,其中包含相应的元数据,如消息内容类型或服务器软件。

HttpClient 包含了一些强类型集合,其中包含与标准 HTTP 头信息相对应的属性。 DefaultRequestHeaders 属性包含适用于每一个请求的头信息。

HttpRequestMessage 类的 Headers 属性包含请求特有的头信息。

查询字符串只是通过问号 (?) 附加到 URI 后面的字符串,它可用于向服务器发送简单的数据。

WebClient 包含一个字典风格的属性,它可以简化查询字符串的操作。

(P592)

如果要使用 WebRequest 或 HttpClient 实现相同效果,那么必须手工赋给请求 URI 正确格式的字符串。

如果查询中包含符号或空格,那么必须使用 Uri 的 EscapeDataString 方法才能创建合法的 URI 。

EscapeDataString 与 EscapeUriString 类似,唯一不同的是前者进行了特殊字符的编码,如 & 和 = ,否则它们会破坏查询字符串。

WebClient 的 UploadValues 方法可以以 HTML 表单的方式提交数据。

NameValueCollection 中的键与 HTML 表单的输入框相对应。

使用 WebRequest 上传表单数据的操作更为复杂,如果需要使用 cookies 等特性,则必须采用这种方法。下面是具体的操作过程 :

  1. 将请求的 ContentType 设置为 “application/x-www-form-urlencoded” ,将它的方法设置为 “POST” ;

  2. 创建一个包含上传数据的字符串,并且将其编码为 : name1=value1&name2=value2&name3=value3...

  3. 使用 Encoding.UTF8.GetBytes 将字符串转换为字节数组;

  4. 将 Web 请求的 ContentLength 属性设置为字节数组的长度;

  5. 调用 Web 请求的 GetRequestStream ,然后写入数据数组;

  6. 调用 GetResponse ,读取服务器的响应。

(P593)

Cookie 是一种 名称 / 值 字符串对,它是 HTTP 服务器通过响应头发送到客户端的。 Web 浏览器客户端通常会记住 cookie ,然后在终止之前,后续请求都会将它们重复发送给服务器 (相同地址) 。

Cookie 使服务器知道它是否正在连接之前连接过的相同客户端,从而不需要在 URI 重复添加复杂的查询字符串。

默认情况下, HttpWebRequest 会忽略从服务器接收的任意 cookie 。为了接收 cookie ,必须创建一个 CookieContainer 对象,然后将它分配到 WebRequest 。然后,就可以列举响应中接收到的 cookie 。

(P594)

WebClient 门面类不支持 cookie 。

(P596)

可以使用 HttpListener 类编写自定义 HTTP 服务器。

(P599)

对于简单的 FTP 上传和下载操作,可以使用 WebClient 按照前面的方式实现。

(P600)

静态的 Dns 类封装了 DNS (Domain Name Service ,域名服务) ,它可以执行原始 IP 地址和人性化的域名之间的转换操作。

GetHostAddresses 方法可以将域名转换为 IP 地址 (或地址) 。

(P601)

GetHostEntry 方法则执行相反操作,将地址转换为域名。

GetHostEntry 方法还接受一个 IPAddress 对象,所以我们可以用一个字节数组来表示 IP 地址。

在使用 WebRequest 或 TcpClient 等类时,域名会自动解析为 IP 地址。然而,如果想要在应用程序的生命周期内向同一个地址发送多个网络请求,有时候需要先使用 DNS 将域名显式地转换为 IP 地址,然后再直接使用得到的 IP 地址进行通信,从而提高运行性能。这样就能够避免重复解析同一个域名,有助于 (使用 TcpClient 、 UdpClient 或 Socket ) 处理传输层协议。

System.Net.Mail 命名空间的 SmtpClient 类可用来通过普遍使用的简单邮件传输协议 (Simple Mail Transfer Protocol ,SMTP) 发送邮件消息。

要发送一条简单的文本消息,我们需要实例化 SmtpClient ,将它的 Host 属性设置为 SMTP 服务器地址,然后调用 Send 。

为了防止垃圾邮件, Internet 中大多数 SMTP 服务器都只接受来自 ISP 订阅者的连接,所以我们需要使用适合当前连接的 SMTP 地址才能成功发送邮件。

MailMessage 对象支持更多的选项,包括添加附件。

SmtpClient 可以为需要执行身份验证的服务器指定 Credentials ,如果支持 EnableSsl ,也可以将 TCP Port 修改为非默认值。通过修改 DeliveryMethod 属性,我们可以使用 SmtpClient 代替 IIS 发送邮件消息,或者直接将消息写到指定目录下的一个 .eml 文件中。

(P602)

TCP 和 UDP 是大多数 Internet (与局域网) 服务所依赖的传输层协议的基础。

HTTP 、 FTP 和 SMTP 使用 TCP ; DNS 使用 UDP 。

TCP 是面向连接的,具有可靠性机制; UDP 是无连接的,负载更小,并且支持广播。

BitTorrent 和 Voice over IP 都使用 UDP 。

传输层比其他上层协议具有更高灵活性,性能可能也更高,但是它要求用户自己处理一些具体任务,如身份验证和加密。

对于 TCP ,我们可以选择使用简单易用的 TcpClient 和 TcpListener 外观类,或者使用功能丰富的 Socket 类。事实上,它们可以混合使用,因为我们可以通过 TcpClient 的 Client 属性获得底层的 Socket 对象。Socket 类包含更多的配置选项,它支持网络层 (IP) 的直接访问,也支持一些非 Internet 协议,如 Novell 的 SPX/IPX 。

和其他协议一样, TCP 也区分客户端和服务器 : 客户端发起请求,而服务器则等待请求。

NetworkStream 提供一种双向通信手段,同时支持从服务器发送和接收字节数据。

(P604)

TcpClient 和 TcpListener 提供了基于任务的异步方法,可用于实现可扩展的并发性。使用这些方法,只需要将阻塞方法替换为它们对应的 *Async 版本方法,然后等待任务返回。

(P605)

.NET Framework 并没有提供任何 POP3 的应用层支持,所以要从一个 POP3 服务器接收邮件,必须在 TCP 层编写代码。

(P606)

Windows Runtime 通过 Windows.Networking.Sockets 命名空间实现 Tcp 功能。与 .NET 实现一样,其中主要有两个类,分别充当服务器和客户端角色。在 WinRT 中,它们分别是 StreamSocketListener 和 StreamSocket 。

【第17章】

(P608)

序列化与反序列化,通过它对象可以表示成一个纯文本或者二进制形式。

序列化是把内存中的一个对象或者对象图 (一组互相引用的对象) 转换成一个字节流或者一组可以保存或传输的 XML 节点。反序列化正好相反,它把一个数据流重新构造成一个内存中的对象或对象图。

序列化和反序列化通常用于 :

  1. 通过网络或应用程序边界传输对象;

  2. 在文件或数据库中保存对象的表示;

序列化与反序列化也用于深度克隆对象。

数据契约和 XML 序列化引擎也可以被用作通用目的工具来加载和保存已知结构的 XML 文件。

.NET Framework 从两个角度来支持序列化与反序列化 : 第一,从想进行序列化和反序列化对象的客户端角度; 第二,从想控制其如何被序列化的类型角度。

在 .NET Framework 中有 4 种序列化机制 :

  1. 数据契约序列化器;

  2. 二进制序列化器;

  3. (基于属性的) XML 序列化器 (XmlSerializer) ;

  4. IXmlSerializable 接口;

(P609)

其中前三种 “引擎” 可以完成大部分或所有序列化操作。而最后的 IXmlSerializable 接口是一个可以通过使用 XmlReader 和 XmlWriter 进行序列化的起桥梁作用的钩子 (hook) 。

IXmlSerializable 可以联合数据契约序列化器或者 XmlSerializer 来处理更复杂的 XML 序列化任务。

IXmlSerializable 的分数假设已经使用 XmlReader 和 XmlWriter 最优化地 (手) 写代码。

XML 序列化引擎要求回收相同的 XmlSerializer 对象以达到更佳的性能。

出现这三种引擎在一定程度上是由于历史原因。 Framework 在序列化上基于两个完全不同的目的 :

  1. 真实的序列化包含类型及其引用的 .NET 对象图;

  2. XML 和 SOAP 消息之间的互操作标准;

第一种由 Remoting 的需求而产生;而第二种是由于 Web 服务。写一个序列化引擎来同时完成这两项任务非常困难,所以 Microsoft 编写了两个引擎 : 二进制序列化器和 XML 序列化器。

后来在 .NET Framework 3.0 中出现 WCF 时,其部分目标在于统一 Remoting 和 Web 服务。这就要求一个新的序列化引擎,所以就出现了数据契约序列化器。数据契约序列化器统一了旧有的两个和消息有关的引擎的特性。但是在这个上下文之外,这两个旧的序列化引擎还是很重要的。

数据契约序列化器在这三种序列化引擎中是最新的也是最有用的引擎,并被 WCF 使用。它在下面两种情形下尤其强大 :

  1. 通过符合标准的消息协议来交换信息;

  2. 需要好的版本容差能力,并且能够保留对象引用;

数据契约序列化器支持一种数据契约模型 : 它能帮助把类型的底层细节与被序列化过的数据结构解耦。这为我们提供了优秀的版本容差性,也就意味着我们可以反序列化从早期或者后来版本序列化过来的数据类型。甚至可以反序列化已经被重命名或者被移到不同程序集中的类型。

(P610)

数据契约序列化器可以处理大多数的对象图,尽管它需要比二进制序列化器更多的辅助。如果能够灵活地构造 XML ,它也可被用作通用目的的读写 XML 文件的工具。但是如果需要存储数据属性或者要处理随机出现的 XML 元素,就不能使用数据契约序列化器了。

二进制序列化器比较容易使用、非常的自动化,并且在 .NET Remoting 中自始至终都被很好地支持。

Remoting 在同一进程中的两个应用域之间通信时使用二进制序列化器。

二进制序列化器被高度地自动化了 : 只需要一个属性就可以使一个复杂类型可完全序列化。当所有类型都要求被高保真序列化时,二进制序列化器要比数据契约序列化器快。但是它把类型的内部结构与被序列化数据的格式紧密耦合,导致了比较差的版本容差性 (在 Framework 2.0 之前,即使添加一个字段也会成为破坏版本的变化) 。二进制引擎也不是真正地为生成 XML 而设计的,尽管它为基于 SOAP 的消息提供了一个有限的可以和简单类型互操作的格式化器。

XML 序列化引擎只能产生 XML ,它没有其他能够保持和恢复复杂对象图的引擎那么强大 (它不能够恢复共享的对象引用) 。但是对于处理比较随意的 XML 结构,它是三者之中最灵活的。

XML 引擎也提供了较好的版本容差性。

XMLSerializer 被 ASMX Web 服务使用。

实现 IXmlSerializable 意味着通过使用一个 XmlReader 和 XmlWriter 来完成序列化。 IXmlSerializable 接口被 XmlSerializer 和数据契约序列化器所识别,所以它可以有选择地被用来处理更复杂的类型。它也可以直接被 WCF 和 ASMX Web 服务使用。

(P611)

WCF 总是使用数据契约序列化器,尽管它可以和其他引擎的属性和接口进行互操作。

Remoting 总是使用二进制序列化引擎。

Web 服务总是使用 XMLSerializer 。

使用数据契约序列化器的基本步骤 :

  1. 决定是使用 DataContractSerializer 还是 NetDataContractSerializer ;

  2. 使用 [DataContract] 和 [DataMember] 属性修饰要序列化的对象和成员;

  3. 实例化序列化器后调用 WriteObject 或 ReadObject ;

如果选择 DataContractSerializer ,同时需要注册已知类型 (也能够被序列化的子类型) ,并且要决定是否保留对象引用。

可能也需要采取特殊措施来保证集合能被正确地序列化。

与数据契约序列化器相关的类型被定义在 System.Runtime.Serialization 命名空间中,并包含在同名的程序集中。

有两个数据契约序列化器 :

  1. DataContractSerializer —— .NET 类型与数据契约类型松耦合;

  2. NetDataContractSerializer —— .NET 类型与数据契约类型紧耦合;

DataContractSerializer 可以产生可互操作的符合标准的 XML 。

(P612)

如果通过 WCF 通信或者 读 / 写 一个 XML 文件,可能倾向于使用 DataContractSerializer 。

选择序列化器后,下一步就是添加相应的属性到要序列化的类型和成员上,至少应该 :

  1. 添加 [DataContract] 属性到每个类型上;

  2. 添加 [DataMember] 属性到每个包含的成员上;

(P613)

DataContractSerializer 的构造方法需要一个根对象类型 (显式序列化的对象类型) ,相反的, NetDataContractSerializer 就不需要。

NetDataContractSerializer 在其他方面与 DataContractSerializer 的用法相同。

两种序列化器都默认使用 XML 格式化器。

使用 XmlReader ,可以为了可读性让输出包含缩进。

指定名称和命名空间可以把契约标识与 .NET 类型名称解耦。它能够保证当重构和改变类型的名称或命名空间时,序列化不会受到影响。

(P614)

[DataMember] 可以支持 public 和 private 字段和属性。字段和属性的数据类型可以是下列类型的任何一种 :

  1. 任何基本类型;

  2. DateTime 、 TimeSpan 、 Guid 、 Uri 或 Enum 值;

  3. 上述类型的 Nullable 类型;

  4. Byte[] (在 XML 中序列化为 base 64) ;

  5. 任何用 DataContract 修饰的已知类型;

  6. 任何 IEnumerable 类型;

  7. 任何被 [Serializable] 修饰,或者实现了 ISerializable 的类型;

  8. 实现了 IXmlSerializable 的任何类型;

可以同时使用二进制格式化器和 DataContractSerializer 或者 NetDataContractSerializer ,过程是一样的。

二进制格式化器输出会比 XML 格式化器稍微小一些,当类型中包含大的数组时就会明显地看到小得多。

在使用 NetDataContractSerializer 时,不需要特别地处理子类的序列化,除非子类需要 [DataContract] 属性。

DataContractSerializer 必须要了解它可能序列化或反序列化的所有子类型。

(P616)

当序列化子类型时,不管使用哪种序列化器, NetDataContractSerializer 会导致性能上的损失。就好像是当遇到子类型时,它就必须停下来思考一下。

当在一个应用程序服务器上处理大量并发请求时才会考虑序列化性能。

(P617)

NetDataContractSerializer 总是会保留引用相等性。而 DataContractSerializer 不会,除非指定它保留。

可以在构造 DataContractSerializer 时指定参数 preserveObjectReferences 为 true 来要求引用完整性。

(P618)

如果某个成员对于一个类型是非常重要的,可以通过指定 [IsRequired] 要求它必须出现,如果成员没有出现,在序列化时会抛出一个异常。

数据契约序列化器对数据成员的数据要求极其苛刻。反序列化器实际上会跳过任何被认为在序列外的成员。

在序列化成员时按下面的顺序 :

  1. 从基类到子类;

  2. 根据 Order 从低到高 (对于 [Order] 属性被设置的数据成员) ;

  3. 字母表顺序 (使用传统的字符串比较法) ;

(P619)

要指定顺序的主要原因是为了遵循特定的 XML Schema 。 XML 元素的顺序等同于数据成员顺序。

(P620)

数据契约序列化器可以保持和恢复可遍历集合。

(P622)

如果要在序列化之前或之后执行一个自定义方法,可以通过在方法上标记以下属性 :

  1. [OnSerializing] —— 指示在序列化之前调用这个方法;

  2. [OnSerialized] —— 指示在序列化之后调用这个方法;

  3. [OnDeserializing] —— 指示在反序列化之前调用这个方法;

  4. [OnDeserialized] —— 指示在反序列化之后调用这个方法;

自定义方法只能定义一个 StreamingContext 类型的参数。这个参数是为了与二进制引擎保持一致而被要求的,它不被数据契约序列化器使用。

[OnSerializing] 和 [OnDeserialized] 在处理超出数据契约引擎能力之外的成员时有用,例如一个超额的集合或者没有实现标准接口的集合。

(P623)

[OnSerializing] 标记的方法也可以被用作有条件的序列化字段。

注意数据契约反序列化器会绕过字段初始化器和构造方法。标记了 [OnDeserializing] 的方法在反序列化过程中起着伪造构造方法的作用,并且它对初始化被排除在序列化外的字段很有用。

使用这 4 个属性修饰的方法可能是私有的,如果子类需要参与其中,那么它们可以使用相同的属性定义自己的方法,然后它们一样可以执行。

(P624)

数据契约序列化器也可以序列化标记了二进制序列化引擎中的属性或接口类型。这种功能是非常重要的,因为这是为了支持已经被写入 Framework 3.0 以下版本 (包括 .NET Framework) 中的二进制引擎。

下面两项可以标记一个可被二进制引擎序列化的类型 :

  1. [Serializable] 属性;

  2. 实现 ISerializable ;

二进制互操作性对于序列化已有类型并且需要同时支持这两种引擎的情况比较有用。它也提供了扩展数据契约序列化器的另一种方式,因为二进制引擎的 ISerializable 要比数据契约属性更灵活。但是,数据契约序列化器不能通过 ISerializable 格式化添加的数据。

(P625)

数据契约序列化器的一个限制是它几乎不能控制 XML 的结构。在一个 WCF 应用程序中,这实际上是有好处的,因为它使得基础结构更容易符合标准消息协议。

如果需要控制 XML 的结构,可以实现 IXmlSerializable 接口,然后使用 XmlReader 和 XmlWriter 来手动地读和写 XML ,数据契约序列化器仅允许在那些需要这一控制的类型上执行这些操作。

二进制序列化引擎被 Remoting 隐式地使用,它可以用来完成把对象保存到磁盘或从磁盘上还原对象之类的任务。二进制序列化被高度地自动化了,并可以用最少的操作来处理复杂的对象图。

有两种方式让一个类型支持二进制序列化。第一种是基于属性;第二种是实现 ISerializable 接口。添加属性相对比较简单,而实现 ISerializable 更灵活。实现 ISerializable 主要是为了 :

  1. 动态地控制什么要被序列化;

  2. 让可序列化类型能够被其他部分更友好地继承;

一个类型可以使用单个属性指定为可序列化的。

[Serializable] 属性使序列化器包含类型中所有的字段。这既包含私有字段,也包含公共字段 (但不包含属性) 。每一个字段本身都可序列化,否则就会抛出一个异常。基本 .NET 类型,例如 string 和 int 支持序列化 (许多其他 .NET 类型也是) 。

[Serializable] 属性不能被继承,所以子类不会自动成为可序列化的,除非也在子类上标记上这个属性。

对于自动属性,二进制序列化引擎会序列化底层的被编译出的字段。但是,当增加属性时,重新编译这个类型会改变这个字段的名称,这就会破坏已序列化数据的兼容性。处理方法就是在 [Serializable] 的类型里避免使用自动属性或者实现 ISerializable 接口。

(P626)

为了序列化一个实例,可以实例化一个格式化器,然后调用 Serialize 方法。在二进制引擎中有两个可用的格式化器 :

  1. BinaryFormatter —— 两者之中效率稍高,在更少的时间里产生更小的输出。它的命名空间是 System.Runtime.Serialization.Formatters.Binary ,程序集为 mscorlib 。

  2. SoapFormatter —— 它支持在使用 Remoting 时基本的 SOAP 样式的消息。它的命名空间是 System.Runtime.Serialization.Formatters.Soap ,程序集为 System.Runtime.Serialization.Formatters.Soap.dll ;

SoapFormatter 没有 BinaryFormatter 实用。 SoapFormatter 不支持泛型或者筛选对版本容差有必要的额外数据。

反序列化器在重新创建对象时会绕过所有的构造方法。在这个过程中实际调用了 FormatterServices.GetUninitializedObject 方法来完成这个工作。可以自己调用这个方法来实现可能会非常复杂的设计模式。

序列化过的数据包含类型和程序集的全部信息,所以如果试图把序列化的结果转换到一个不同程序集中的类型,结果会产生一个错误。在反序列化过程中,序列化器会完全恢复对象引用到序列化的状态。集合同样如此,它会对集合像其他类型一样处理 (所有在 System.Collections.* 下的类型都被标记为可序列化) 。

二进制引擎可以处理大且复杂的对象图而不需要特别辅助 (不用保证所有参与的成员都可序列化) 。唯一要注意的是,序列化器的性能会随着对象图的引用数量的增加而降低。这样在一个要处理大量并发请求的 Remoting 服务器上就会成为一个问题。

(P627)

不同于数据契约对要序列化的字段使用选择性加入方针,二进制引擎使用选择性排除方针。

对于不想序列化的字段,必须显式地使用 [NonSerialized] 属性来标记它们。

不序列化的成员在反序列化后总是为空或 null ,即使在构造方法或字段初始化器中设置了它们。

(P628)

二进制引擎也支持 [OnSerializing] 和 [OnSerialized] 属性,这两个属性用来标记在序列化之前或之后要被调用的方法上。

默认,添加一个字段会破坏已经序列化的数据的兼容性,除非新的字段附加了 [OptionalField] 属性。

(P629)

版本健壮性十分重要,避免重命名和删除字段,同时避免追溯性地添加 [NonSerialized] 属性,永远不要改变字段的类型。

如果在双向通信时,要求版本健壮性,必须使用二进制格式化器,否则需要通过实现 ISerializable 来手动地控制序列化。

实现 ISerializable 可以让一个类型完全控制其二进制序列化和反序列化。

GetObjectData 在序列化时被触发,它的任务就是把想序列化的所有字段存放到 SerializationInfo (一个 名称 / 值 的字典) 对象里。

(P630)

把 GetObjectData 方法设置为 virtual 可以让子类扩展序列化而不用重新实现这个接口。

SerializationInfo 也包含相应的属性以用来控制实例应该反序列化的类型和程序集。

StreamingContext 参数是它包含的结构,一个枚举值指示这个序列化的实例保存的位置 (磁盘、 Remoting 等,尽管这个值不总是有) 。

除了实现 ISerializable ,一个控制其序列化的类型也需要提供一个反序列化构造方法,这个方法包含和 GetObjectData 方法一样的两个参数。构造方法可以被声明为任何访问级别,运行时总能够找到它。特别是,可以声明它为 protected 级别,这样子类就可以调用它了。

(P632)

Framework 提供了专门的 XML 序列化引擎,即在 System.Xml.Serializaion 命名空间下的 XmlSerializer 。它适合把 .NET 类型序列化为 XML 文件,它也被 ASMX Web 服务隐式地使用。

和二进制类似,可以使用以下两种方式 :

  1. 在类型上使用定义在 System.Xml.Serialization 上的属性;

  2. 实现 IXmlSerializable ;

然而不同于二进制引擎,实现接口 (例如 IXmlSerializable ) 就会完全避开引擎,要完全使用 XmlReader 和 XmlWriter 来实现序列化。

为了使用 XmlSerializer ,要实例化它,并调用 Serialize 和 Deserialize 方法传入 Stream 和对象实例。

(P633)

Serialize 和 Deserialize 方法可以与 Stream 、 XmlWriter / XmlReader 或者 TextWriter / TextReader 一起工作。

XmlSerializer 可以序列化没有标记任何属性的类型。

默认,它会序列化类型上的所有公共字段和属性。

可以使用 [XmlIgnore] 属性来排除不想被序列化的成员。

不同于其他两个引擎, XmlSerializer 不能识别 [OnDeserializing] 属性,在反序列化时依赖于一个无参数的构造方法,如果没有无参的构造方法,就会抛出一个异常。

尽管 XmlSerializer 可以序列化任何类型,但是它会识别以下类型,并且会进行特殊的处理 :

  1. 基本类型、 DateTime 、 TimeSpan 、 Guid 以及这些类型的可空类型版本;

  2. Byte[] (它会被转化为 base64 编码) ;

  3. 一个 XmlAttribute 或者 XmlElement (它们的内容会被注入到流中) ;

  4. 任何实现了 IXmlSerializable 的类型;

  5. 任何集合类型;

XML 反序列化器允许版本容差 : 如果缺少元素或属性,或者有多余的数据出现,它都可以正常工作。

(P634)

字段和属性默认都被序列化为 XML 元素。

默认的 XML 命名空间为空 (不同于数据契约序列化器使用类型的命名空间) 。

为了指定一个 XML 命名空间, [XmlElement] 和 [XmlAttribute] 都接受一个 Namespace 的参数。也可以对类型本身使用 [XmlRoot] 来给它分配名称和命名空间。

XmlSerializer 会按照成员在类中定义的顺序写元素。可以通过在 XmlElement 属性上指定 Order 值来改变这个顺序。

一旦使用了 Order ,所有要序列化的成员都得使用。

而反序列化器并不关心元素的顺序,不管元素以任何顺序出现,类型总能够被恰当地反序列化。

(P635)

XmlSerializer 会自动地递归对象引用。

(P636)

如果有两个属性或字段引用了相同的对象,那么这个对象会被序列化两次。如果想保留引用相等性,必须使用其他的序列化引擎。

(P637)

XmlSerializer 识别和序列化具体的集合类型,而不需要其他干涉。

(P640)

实现 IXmlSerializable 的规则如下 :

  1. ReadXml 应该读取最外层起始元素,然后读取内容,最后才是最外层结束元素;

  2. WriteXml 应该只写入内容;

通过 XmlSerializer 序列化和反序列化时会自动调用 WriteXml 和 ReadXml 方法。

【第18章】

(P641)

程序集是 .NET 中的基本部署单元,也是所有类的容器。

程序集包含已编译的类和它们的 IL 代码、运行时资源,以及用于控制版本、安全性和引用其他程序集的信息。

程序集也为类解析和安全许可定义了边界。

一般来说,一个程序集包含单个 PE (Windows Portable Executable ,可移植的执行体) 文件,如果是应用程序,则带有 .exe 扩展名;如果是可重用的库,则扩展名为 .dll 。

程序集包含 4 项内容 :

  1. 一个程序集清单 —— 向 .NET 运行时提供信息,例如程序集的名称、版本、请求的权限以及引用的其他程序集;

  2. 一个应用程序清单 —— 向操作系统提供信息,例如程序集应该被如何部署和是否需要管理提升;

  3. 一些已编译的类 —— 程序集中定义的类的 IL 代码和元数据;

  4. 资源 —— 嵌入程序集中的其他数据,例如图像和可本地化的文本;

所有这些内容中,只有程序集清单是必需的,尽管程序集几乎总是包含已编译的类。

程序集不管是可执行文件还是库,结构是类似的。主要的不同点是,可执行文件定义一个入口点。

(P641)

程序集清单有两个目的 :

  1. 向托管宿主环境描述程序集;

  2. 到程序集中模块、类和资源的目录;

因此,程序集是自描述的。

(P642)

消费者可以发现程序集的数据、类和函数等所有内容,无需额外的文件。

程序集清单不是显式地添加到程序集的,而是作为编译的一部分自动嵌入到程序集中的。

下面总结了程序集清单中存储的主要数据 :

  1. 程序集的简单名称;

  2. 版本号 (AssemblyVersion) ;

  3. 程序集的公共密钥和已签名的散列 (如果是强命名的) ;

  4. 一系列引用的程序集,包括它们的版本和公共密钥;

  5. 组成程序集的一系列模块;

  6. 程序集定义的一系列类和包含每个类的模块;

  7. 一组可选的由程序集要求或拒绝的安全权限 (AssemblyPermission) ;

  8. 附属程序集针对的文化 (AssemblyCulture) ;

清单也可以存储以下信息数据 :

  1. 完整的标题和描述 (AssemblyTitle 和 AssemblyDescription) ;

  2. 公司和版权信息 (AssemblyCompany 和 AssemblyCopyright) ;

  3. 显式版本 (AssemblyInformationVersion) ;

  4. 自定义数据的其他属性;

这些数据有些来自提供给编译器的参数,其他的数据来自程序集属性 (括号中的内容) 。

可以利用 .NET 工具 ildasm.exe 查看程序集清单的内容。

可以利用程序集属性指定绝大部分清单内容。

这些声明通常都定义在项目的一个文件中。

Visual Studio 为此对每个新 C# 项目都在 Properties 文件夹中自动创建一个名为 AssemblyInfo.cs 的文件,预定义了一组默认的程序集属性,为进一步的自定义提供起点。

应用程序清单是一个 XML 文件,它向操作系统提供关于程序集的信息。如果存在的话,应用程序清单在 .NET 托管宿主环境加载程序集之前被读取和处理,因而可以影响操作系统如何启动应用程序的进程。

(P643)

Metro 应用有更详细的配置清单,它包含程序功能声明,它决定了操作系统所分配的权限。编辑这个文件的最简单方法是使用 Visual Studio ,双击配置清单文件就可以显示编辑界面。

可以用两种方式部署 .NET 应用程序清单 :

  1. 作为程序集所在文件夹中的一个特殊命名的文件;

  2. 嵌入程序集中;

作为一个单独的文件,其名称必须匹配程序集的名称,后缀为 .manifest 。

.NET 工具 ildasm.exe 对嵌入式应用程序清单的存在视而不见。但是如果在 Solution Explorer 中双击程序集, Visual Studio 会指出嵌入式应用程序清单是否存在。

程序集的内容实际上存储在一个或多个称为模块的中间容器中。

一个模块对应于一个包含程序集内容的文件。

采用额外的容器层的原因是,为了在构建包含多种编程语言中编译的代码的程序集时,允许程序集跨多个文件,这是一个很有用的特性。

(P644)

在多文件程序集中,主模块总是包含清单;其他的模块可以包含 IL 和资源。清单描述组成程序集的所有其他模块的相对位置。

多文件程序集必须从命令行编译, Visual Studio 中不支持。

为了编译程序集,需要利用 /t 开关调用 csc 编译器来创建每个模块,然后再用程序集链接器工具 al.exe 将它们链接起来。

尽管很少有需要多文件程序集的情况,即使在处理单模块程序集时,但是时常需要了解模块这一额外的容器层。主要应用场景跟反射有关。

System.Refelction 中的 Assembly 类是在运行时访问程序集元数据的入口。

有很多方式可以获得程序集对象,最简单的方式是通过 Type 的 Assembly 属性。

(P645)

也可以通过调用 Assembly 的静态方法来获得 Assembly 对象 :

  1. GetExecutingAssembly —— 返回定义当前正在执行的函数的程序集;

  2. GetCallingAssembly —— 跟 GetExecutingAssembly 执行相同的操作,但是针对的是调用当前正在执行的函数的函数;

  3. GetEntryAssembly —— 返回定义应用程序初始入口方法的程序集;

一旦有了 Assembly 对象,就可以使用它的属性和方法来查询程序集的元数据和反射它的类。

程序集成员 :

  1. FullName 、 GetName —— 返回完全限定的名称或者 AssemblyName 对象;

  2. CodeBase 、 Location —— 程序集文件的位置;

  3. Load 、 LoadFrom 、 LoadFile —— 手动将程序集加载到当前应用程序域中;

  4. GlobalAssemblyCache —— 指出程序集是否定义在 GAC 中;

  5. GetSatelliteAssembly —— 找到给定文化的卫星程序集;

  6. GetType 、 GetTypes —— 返回定义在程序集中的一个或所有类;

  7. EntryPoint —— 返回应用程序的入口方法,例如 MethodInfo ;

  8. GetModules 、 ManifestModule —— 返回程序集的所有模块或主模块;

  9. GetCustomAttributes —— 返回程序集的属性;

强命名的程序集具有唯一的、不可更改的身份。通过向清单添加以下两类元数据来实现 :

  1. 属于程序集创作者的唯一编号;

  2. 程序集的已签名散列,证实程序集产生的唯一编号持有者;

这需要一个 公共 / 私有 密钥对。公共密钥提供唯一的身份识别号,私有密钥帮助签名。

强名称签名不同于 Authenticode 签名。

公共密钥对于保证程序集引用的唯一性有价值 : 强命名的程序集将公共密钥合并到它的身份中。签名对于安全性有价值,它防止恶意人员篡改程序集。没有私有密钥,无法发布程序集的修改版本时不出现其签名中断 (导致加载时错误) 。

(P646)

向弱命名的程序集添加一个强名称会更改它的身份。因此,有必要一开始就给生产型程序集 (Production Assembly) 命名一个强名称。

强命名的程序集也可以注册在 GAC 中。

要给程序集命名一个强名称,首先利用实用工具 sn.exe 生成一个 公共 / 私有 密钥对。

强命名的程序集不能引用弱命名的程序集。这是要强命名所有生产型程序集的另一个重要原因。

每个程序集具有一个独立的密钥对是有利的,在以后转移某个特定应用程序 (以及它引用的程序集) 的所有权时,可以做到最小暴露。但是使得创建可以识别所有程序集的安全策略更难了,也使得验证动态加载的程序集更为困难了。

在有数百个开发人员的组织中,你可能想要限制对程序集进行签名的密钥对的访问,原因有两个 :

  1. 如果密钥对泄露,你的程序集就不再是不可篡改的了;

  2. 测试程序集如果已签名和泄露,就会被恶意地宣称为真正的程序集;

延迟签名的程序集用正确的公共密钥进行标记,但是没有用私有密钥签名。

(P647)

延迟签名的程序集相当于被篡改的程序集,通常会被 CLR 拒绝。

要延迟签名,需要一个只包含公共密钥的文件。

必须从命令行手动禁用程序集验证,否则,程序集将不会执行。

程序集的身份包含四种来自其清单的元数据 :

  1. 它的简单名称;

  2. 它的版本 (如果未指定,就是 0.0.0.0 ) ;

  3. 它的文化 (如果不是卫星程序集, 就是 neutral) ;

  4. 它的公共密钥标记 (如果不是强命名的, 就是 null) ;

(P648)

完全限定程序集名称是一个包含 4 个身份识别组件的字符串。

如果程序集没有 AssemblyVersion 属性,则版本显示为 “0.0.0.0” 。如果未签名,则其公共密钥标记显示为 “null” 。

Assembly 对象的 FullName 属性返回它的完全限定名称。编译器在清单中记录程序集引用时总是使用完全限定名称。

完全限定程序集名称不包含它在磁盘上的目录路径。

AssemblyName 类的完全限定程序集名称的每一个组件都具有一个类型化属性。 AssemblyName 有两个目的 :

  1. 解析或构建完全限定程序集名称;

  2. 存储一些额外的数据,以帮助解析 (寻找) 程序集;

可以通过以下三种方式获得 AssemblyName :

  1. 实例化一个 AssemblyName ,提供完全限定名称;

  2. 在一个现有 Assembly 上调用 GetName ;

  3. 调用 AssemblyName.GetAssemblyName ,提供到磁盘上程序集文件的路径;

(P649)

可以不用任何参数实例化一个 AssemblyName ,然后设置它的每个属性以构建完全限定名称。以这种方式构造的 AssemblyName 是易变的。

Version 本身是一个强类型化的表示,具有 Major 、 Minor 、 Build 和版本号属性。

GetPublicKey 返回完全加密的公共密钥。

GetPublicToken 返回建立身份时使用的最后 8 个字节。

由于版本是程序集名称的一个有机部分,所以改变 AssemblyVersion 属性就会改变程序集的身份。这将影响与引用程序集的兼容性,在不间断的更新中会出现意想不到的情况。要解决这个问题,有以下两个独立的程序集级别的属性用于表示与版本相关的信息,两者都被 CLR 省略 :

  1. AssemblyInformationVersion —— 显示给最终用户的版本。这在 “Windows File Properties” 对话框中作为 “Product Version” 出现。可以包含任何字符串。通常程序中的所有程序集会被分配相同的信息版本号;

  2. AssemblyFileVersion —— 用于引用此程序集的构建号。这在 “Windows File Properties” 对话框中作为 “File Version” 出现。跟 AssemblyVersion 一样,它必须包含一个字符串,最多由 4 个用句点分隔的数字组成;

Authenticode 是一个代码签名系统,其目的是证明发行商的身份。

Authenticode 和强名称签名是独立的,可以用任何一个或同时用两个系统对程序集进行签名。

(P651)

如果还想对程序集进行强名称签名 (强烈推荐) ,那么必须在 Authenticode 签名之前进行强名称签名。

(P652)

最好避免对 .NET 3.5 或更早的程序集进行 Authenticode 签名。

作为安装 .NET Framework 的一部分,在计算机上创建一个中心仓库,用于存储 .NET 程序集,这就是所谓的全局程序集高速缓存 ( Global Assembly Cache , GAC) 。 GAC 包含 .NET Framework 本身的一个集中副本,并且它也可以用来集中自定义的程序集。

(P653)

对于非常大的程序集, GAC 可以缩短启动时间,因为 CLR 只需要在安装时验证一次 GAC 中程序集的签名,而不是每次加载程序集时都要验证。按百分比来说,如果用 ngen.exe 工具为程序集生成了本机映射 (选择非重叠的基地址) ,就会有这一优势。

GAC 中的程序集总是完全受信任的,即使是从运行在受限的沙箱中调用程序集。

要将程序集安装到 GAC ,第一步是给程序集命名一个强名称。

(P654)

应用程序通常不仅仅包含可执行代码,还包含诸如文本、图像或 XML 文件等内容。这些内容可以表示为程序集中的资源。资源有两个重叠的用例 :

  1. 合并不能进入源代码的数据,例如图像;

  2. 存储在多语言应用程序中可能需要转换的数据;

程序集资源最终是一个带有名称的字节流,可以将程序集看作包含一个按字符串排列的字节数组字典。

Framework 可以通过中间的 .resources 容器添加内容。一些容器包含可能需要转换成不同语言的内容。

(P655)

本地化的 .resources 可打包为在运行时根据用户的操作系统语言被自动挑选的单个卫星程序集。

要使用 Visual Studio 直接嵌入资源 :

  1. 将文件添加到项目;

  2. 将构建操作设置为 “Embedded Resource” ;

资源名称区分大小写,所以 Visual Studio 中包含资源的项目子文件夹名称也区分大小写。

(P656)

要获得一个资源,可以在包含该资源的程序集上调用 GetManifestResourceStream ,返回一个流,然后可以将其读作任何其他名字。

GetManifestResourceNames 返回程序集中所有资源的名称。

.resources 文件包含的是潜在地可本地化的内容。 .resources 文件最终是程序集中的一个嵌入式资源,就像任何其他类型的文件一样。区别在于必须 :

  1. 首先将内容打包到 .resources 文件中;

  2. 通过 ResourceManager 或 pack URI 而不是 GetManifestResourceStream 访问它的内容;

.resources 文件的结构形式是二进制的,所以不是可读的;因此,必须依赖于 Framework 或 Visual Studio 提供的工具来处理它们。

处理字符串或简单数据类的标准方法是使用 .resx 格式,该格式可以通过 Visual Studio 或 resgen 工具转换成 .resources 文件。

.resx 格式也适合于针对 Windows Forms 或 ASP.NET 应用程序的图像。

在 WPF 应用程序中,必须对需要由 URI 引用的图像或类似的内容使用 Visual Studio 的 “Resource” 构建操作。无论是否需要本地化,这一点都是适用的。

(P657)

.resx 文件是一种用于生成 .resources 文件的设计时格式。

.resx 文件使用 XML 通过 名 / 值 对进行构造。

要在 Visual Studio 中创建 .resx 文件,可以添加一个 “Resource File” 类的项目条目。其他工作都是自动完成的 :

  1. 创建正确的头部;

  2. 设计器提供用于添加字符串、图像、文件和其他类型的数据;

  3. .resx 文件自动转转成 .resources 格式,并在编译时嵌入到程序集中;

  4. 编写一个类用于以后访问数据;

资源设计器将图像添加为类型化的 Image 对象 (System.Drawing.dll) ,而不是作为字节数组,这使得它们不适用于 WPF 应用程序。

(P659)

可以简单地通过添加新卫星程序集而增强语言支持,无需更改主程序集。

卫星程序集不能包含可执行代码,只能包含资源。

卫星程序集部署在程序集文件夹的子目录中。

(P661)

文化分成文化和子文化。一种文化代表一种特定的语言;一种子文化代表该语言的一个地区变种。

在 .NET 中用 System.Globalization.CultureInfo 类表示文化,可以检查应用程序的当前文化。

CurrentCulture 反映 Windows 控制面板的区域设置,而 CurrentUICulture 反映操作系统的语言。

一个典型的应用程序包含一个可执行的主程序集和一组引用的库程序集。

程序集解析是指定位所引用程序集的过程。

程序集解析发生在编译时和运行时。

(P662)

在自定义程序集加载和解析方面, Metro 应用只有很少的支持。特别是,它从不支持从任意文件位置加载程序集,而且没有 AssemblyResolve 事件。

所有类都在程序集范围内。

程序集就像类的地址。

程序集组成类的运行时身份的重要部分。

程序集也是类到它的代码和元数据的句柄。

AssemblyResolve 事件允许干预和手动加载 CLR 找不到的程序集。如果处理该事件,可以在各个位置散发引用的程序集,并加载它们。

在 AssemblyResolve 事件处理程序中,通过调用 Assembly 类中三个静态方法 ( Load 、LoadFrom 或 LoadFile ) 中的一个,找到并加载程序集。这些方法返回对新加载的程序集的引用,然后再返回给调用者。

(P663)

ResolveEventArgs 事件比较特殊,因为它具有返回类。如果有多个处理程序,那么第一个返回非空 Assembly 的程序优先。

Assembly 类中的三个 Load 方法在 AssemblyResolve 处理程序内部和外部都很有用。在事件处理程序外部时,它们可以加载和执行编译时没有引用的程序集。可能会加载程序集的一个示例情况是在执行插件时。

在调用 Load 、 LoadFrom 或 LoadFile 之前慎重考虑 : 这些方法将程序集永久地加载到当前应用程序域,即使不对产生的 Assembly 对象执行任何操作。加载程序集具有一些副作用 : 它会锁定程序集文件,还会影响后续的类解析。

卸载程序集的唯一方式是卸载整个应用程序域 (另一个避免锁定程序集的方法是对检测路径的程序集执行阴影拷贝 (shadow copying)) 。

如果只想检查一个程序集,不想执行它的任何代码,那么可以加载到只反射上下文中。

要从完全限定名称 (不带位置) 加载程序集,可调用 Assembly.Load 。这指示 CLR 使用普遍自动解析系统寻找程序集。 CLR 本身使用 Load 寻找所引用的程序集。

要从文件名加载程序集,可调用 LoadFrom 或 LoadFile 。

要从 URI 加载程序集,可调用 LoadFrom 。

要从字节数组加载程序集,可调用 Load 。

通过调用 AppDomain 的 GetAssemblies 方法,可以看到哪些程序集当前被加载到内存中。

LoadFrom 和 LoadFile 都可以从文件名加载程序集。它们有两点区别。首先,如果有一个相同身份的程序集从另一个位置加载到了内存中,那么 LoadFrom 提供前一副本。

LoadFile 提供新副本。

但是,如果从同一位置加载了两次,那么两种方法都提供前一次已缓存的副本。

相反,从同一字节数组两次加载一个程序集,会提供两个不同的 Assembly 对象。

(P664)

在内存中,来自 2 个相同程序的类型是兼容的,这是避免加载重复程序集的主要原因,也是尽量使用 LoadFrom 而不使用 LoadFile 的原因。

LoadFrom 和 LoadFile 的另一个区别是, LoadFrom 会告诉 CLR 前向引用的位置,而 LoadFile 则不会。

如果直接在代码中引用一个类型,那么就称为静态引用 (statically referencing) 该类型。编译器会将该类型的引用添加到正在编译的程序集中,以及包含该类型的程序集名称 (但是不包含如何在运行时寻找该类型的信息) 。

在解析静态引用时, CLR 会先检查 GAC ,然后检查检测路径 (通常是应用的基目录) ,最后触发 AssemblyResolve 事件。但是,在这些操作之前,它会先检查程序集是否已经加载。然而,它只考虑以下情况的程序集 :

  1. 已经从一个路径加载,否则就会出现在自己的路径上 (检测路径) ;

  2. 已经从 AssemblyResolve 事件的响应中加载;

在调用 LoadFrom / LoadFile 时必须非常小心,要先检查程序集是否已经存在于应用的基目录 (除非确实想加载同一个程序集的多个版本) 。

(P665)

如果在 AssemblyResolve 事件响应中加载,则不存在这个问题 (无论是使用 LoadFrom 、 LoadFile 或后面将会介绍的从字节数组加载), 因为事件只触发检测路径之外的程序集。

无论使用 LoadFrom 还是 LoadFile , CLR 都一定会先在 GAC 中查找所请求的程序集。

使用 ReflectionOnlyLoadFrom (它会将程序加载到只有反映的环境中), 可以跳过 GAC 。

程序集的 Location 属性通常会返回其在文件系统的物理位置 (如果有) 。

而 CodeBase 属性则以 URI 形式映射这个位置。

如果要寻找程序集在磁盘的位置,只使用 Location 是不可靠的。更好的方法是同时检查两个属性。

【第19章】

(P670)

在运行时检查元数据和编译代码的操作称为 “反射” 。

System.Type 的实例代表了类型的元数据。因为 Type 的应用领域非常广泛,所以它存在于 System 命名空间中,而非 System.Reflection 命名空间中。

通过调用对象上的 GetType 或者使用 C# 的 typeof 运算符,可以获得 System.Type 实例。

(P671)

还可以通过名称获取类型。如果引用了该类型的程序集。

如果没有程序集对象,可以通过其程序集限定名称获取类型 (该类型的全称会带有程序集完整的限定名称)。

一旦拥有了 System.Type 对象,就可以使用它的属性访问类型的名称、程序集、基础类型、可见性等。

一个 System.Type 实例就是打开类型 (及其定义的程序集) 的全部元数据的一个入口。

System.Type 是个抽象的概念,因此实际上 typeof 运算符获得的是 Type 子类。对于 mscorlib 来说, CLR 使用的这些子类都是内部的,称为 RuntimeType 。

Metro 应用模板隐藏了大多数类型成员,转而将它们封装在 TypeInfo 类中。调用 GetTypeInfo ,就可以得到这个类。

完整的 .NET 框架也包含 TypeInfo ,所以能在 Metro 中正常运行的代码也可以在标准库 .NET 应用中运行,但是只适用于 Framework 4.5 (旧版本不支持) 。

(P672)

TypeInfo 还包含其他一些反射成员的属性和方法。

Metro 应用只实现了有限的反射机制。特别是它们无法访问非公共成员类型,也无法使用 Reflection.Emit 。

可以将 typeof 和 GetType 与数组类型一起使用。还可以通过调用元素类型上的 MakeArrayType 获取数组类型。

可以向 MakeArray 传递整型参数,以创建多维矩形数组。

GetElementType 返回数组的元素类型。

GetArrayRank 返回矩形数组的维数。

要重新获得嵌套类型,可调用包含类型的 GetNestedTypes 。

在使用嵌套类型时需要特别注意的是 CLR 会认为嵌套类型拥有特定 “嵌套” 可访问等级。

类型具有 Namespace 、 Name 和 FullName 特性。在大多数情况中, FullName 是前两者的组合。

Type 还具有 AssemblyQualifiedName 特性,使用它可以返回带有逗号和其程序集完整名称的 FullName 值。同样可以将该字符串传递给 Type.GetType ,然后会在默认的加载环境中单独获取类型。

(P673)

对于嵌套类型来说,包含类型仅在 FullName 中出现。

  • 表示将包含类型与嵌套的命名空间区分开。

泛型类型名称带有‘后缀,还带有类型参数的编号。如果泛型类型被绑定,那么该法则同时应用于 Name 和 FullName 。

然而,如果该泛型类型是封闭式的, FullName (仅仅) 获得基本的额外附加信息。

数组通过在 typeof 表达式中使用的相同后缀表示。

指针类型也与数组类似。

描述 ref 和 out 参数的类型带有 & 后缀。

(P674)

类型可以公开 BaseType 特性。

GetInterfaces 方法会返回类型实现的接口。

反射为 C# 的静态 is 运算符提供了两种等价的动态运算符 :

  1. IsInstanceOfType —— 可以接收类型和实例;

  2. IsAssignableFrom —— 可以接收两个类型;

可以使用两种方法通过对象的类型动态地实例化对象 :

  1. 调用静态 Activator.CreateInstance 方法;

  2. 调用 ConstructorInfo 对象上的 Invoke , ConstructorInfo 对象是通过调用类型 (高级环境) 上的 GetConstructor 获得的;

Activator.CreateInstance 可以接收已传递到构造方法的 Type 和可选的参数。

(P675)

使用 CreateInstance 可以设定许多其他选项,如用于加载类型的程序集、目标应用程序域和是否与非全局构造方法绑定。如果运行时无法找到适当的构造方法,那么会抛出 MissingMethodException 。

当参数值无法在重载的构造方法之间消除时,必须调用 ConstructorInfo 上的 Invoke 。

当类型不明确时,应该将一个 null 参数传递给 Activator.CreateInstance 。在这种情况需要使用 ConstructorInfo 进行替换。

在构造对象时进行动态实例化会增加几微妙的时间。相对而言这是一个较长的时间,因为 CLR 实例化对象的速度非常快 (在小型类上简单的 new 操作不足十纳秒) 。

要根据元素类型动态实例化数组,应首先调用 MakeArrayType 。

(P676)

Type 可以代表封闭式或未绑定的泛型类型。

在编译时,封闭式泛型类型可以实例化,而未绑定的类型不能实例化。

MakeGenericType 方法可以将未绑定的泛型类型转换为封闭式泛型类型。只需传递需要的类型参数就可以实现。

使用 GetGenericTypeDefinition 方法可以实现相反的操作。

当 Type 为泛型时, IsGenericType 会返回 true ,而当泛型类型为未绑定时, IsGenericTypeDefinition 会返回 true 。

GetGenericArguments 可以为封闭式泛型类型返回类型参数。

对于未绑定的泛型类型来说, GetGenericArguments 会返回在泛型类型定义中指定为占位符类型的伪类型。

在运行时,所有泛型类型不是未绑定的就是封闭式的。

在 typeof(Foo<>) 这类表达式中泛型类型是未绑定的 (相对来说这种情况比较常见);在其他情况中,泛型类型是封闭式的。

在运行时不存在开放式泛型类型 : 所有开放式类型都会被编译器关闭。

(P677)

使用 GetMembers 方法可以返回类型的成员。

TypeInfo 提供了另一个 (更简单的) 成员反射协议。这个 API 对于目标平台为 Framework 4.5 的应用是可选的,而 Metro 应用则是强制选择的,因为 Metro 应用没有与 GetMethods 方法等价的方法。

TypeInfo 并没有像 GetMethods 这样可以返回数组的方法,而只有返回 IEnumerable 的属性,它们一般用于运行 LINQ 查询。其中使用最广泛的是 DeclaredMembers 。

如果在调用时没有使用参数, GetMembers 会返回类型 (及其基本类型) 的所有公共成员。

GetMember 通过名称检索特定成员,但是因为成员可能会被重新加载, GetMember 仍旧会返回一个数组。

(P678)

MemberInfo 也具有 MemberTypes 类型的 MemberType 特性。

下面列出的是该特性的典型值 : All 、 Custom 、 Field 、 NestedType 、 TypeInfo 、 Constructor 、 Event 、 Method 、 Property ;

当调用 GetMembers 时,可以传递一个 MemberTypes 实例,以限定它返回的成员类型。还可以通过调用 GetMethods 、 GetFields 、 GetProperties 、 GetEvents 、 GetConstructors 或 GetNestedTypes ,限定返回的结果。这些方法还有专门用于特定成员的版本。

对类型的成员进行检索时应尽可能地具体,因而如果要在以后添加成员,就无需拆分代码。如果要通过名称检索方法,指定所有参数类型可以确保出现方法重载时,代码仍旧可以运行。

MemberInfo 对象具有 Name 特性和以下两个 Type 特性 :

  1. DeclaringType —— 返回定义该成员的类型;

  2. ReflectedType —— 根据所调用种类的 GetMembers 返回类型;

当根据基础类型定义的成员进行调用时,会出现两种不同情况 : DeclaringType 会返回基础类型;而 ReflectedType 会返回子类型。

(P679)

MemberInfo 还定义了用于返回自定义属性的方法。

MemberInfo 本身在成员中不重要,因为它是类型的概要基础。

可以根据 MemberInfo 的 MemberType 特性,将 MemberInfo 投射到其子类型上。如果通过 GetMethod 、 GetField 、 GetProperty 、 GetEvent 、 GetConstructor 或 GetNestedType (或者它们的复数版本) 获取成员,就不必进行投射。

(P680)

每个 MemberInfo 子类都具有大量特性和方法,以便公开成员元数据的可见性、修饰符、泛型类型参数、参数、返回类型和自定义属性。

有些 C# 构造 (即索引器、枚举、运算符和终止器) 在涉及 CLR 时就被设计出来了。尤其应该注意以下几点 :

  1. C# 索引器可以转换为接收一个或多个参数的特性,而且可以标识为类型的 [DefaultMembber] ;

  2. C# 枚举可以通过每个成员的静态域转换为 System.Enum 的子类型;

  3. C# 运算符可以转换为被特殊命名的静态方法,而且带有 “op_” 前缀;

  4. C# 析构函数可以转换为覆盖 Finalize 的方法;

另一种复杂的情况是特性或事件实际上由两部分组成 :

  1. 描述特性或事件的元数据 (由 PropertyInfo 或 EventInfo 封装) ;

  2. 一个或两个反向方法 (backing Method) ;

在 C# 程序中,反向方法被封装在特性或事件定义中。但是当将它们编译为 IL 时,反向方法会被表示为原始方法,而且可以像其他方法那样调用。这意味着 GetMethods 会返回与原始方法并列的特性和事件反向方法。

(P681)

既可以为未绑定的泛型类型获取成员元数据,也可以为封闭式泛型类型获取成员元数据。

从未绑定的和封闭式泛型类型返回的 MemberInfo 对象总是独特的,即使对于签名中不带泛型类型参数的成员也是如此。

未绑定泛型类型的成员不能被动态调用。

(P682)

一旦拥有了 MemberInfo 对象,就可以动态地调用它或者 获取 / 设置 它的值。这种操作称为动态绑定或后期绑定,因为要在运行时选择调用成员,而不是在编译时选择调用成员。

使用 GetValue 和 SetValue 可以获取和设置 PropertyInfo 或 FieldInfo 的值。

要动态调用方法 (如在 MethodInfo 上调用 Invoke) ,应为该方法提供一组参数。如果参数类型错误,那么在运行时就会出现异常。在进行动态调用时,会失去编译时的类型安全,但是仍旧可以拥有运行时的类型安全 (就像使用 dynamic 关键字一样) 。

(P688)

通过调用 Assembly 对象上的 GetType 或 GetTypes ,可以动态反射程序集。

GetTypes 仅会返回顶级类型和非嵌套类型。

(P694)

System.Reflection.Emit 命名空间含有用于在运行时创建元数据和 IL 的类。

(P697)

IL 中没有 while 、 do 和 for 循环;这些循环是通过标签、相等 goto 和条件 goto 语句实现的。

(P698)

new 等价于 IL 中的 Newobj 操作码。

【第20章】

(P718)

C# 依靠动态语言运行时 (DLR) 执行动态绑定。

Framework 4.0 是第一个带有 DLR 的 Framework 版本。

(P719)

每种对动态绑定提供支持的语言都会提供专门的绑定器,以帮助 DLR 以专门方式为该语言解释表达式。

(P724)

C# 的静态类型化严格说是一把双刃剑。一方面,它在编译时保证程序的正确性。另一方面,它偶尔会导致编码困难或无法使用代码进行表述,在这种情况中必须使用反射,动态绑定比反射更清晰、更快速。

(P726)

对象可以通过实现 IDynamicMetaObjectProvider 提供其绑定语义 (或者通过子类化 DynamicObject 更容易地提供其绑定语义, DynamicObject 提供了对该接口的默认实现) 。

(P729)

真正的动态语言 (如 IronPython 和 IronRuby) 确实允许执行随机字符串。而且该功能对一些任务 (如编写脚本、动态配置和实现动态规则引擎) 很有用。

【第21章】

(P731)

.NET 中的权限提供了一个独立于操作系统的安全层。其功能有两部分 :

  1. 沙箱 —— 限制不能完全可信的 .NET 程序集可以执行的操作类型;

  2. 授权 —— 限制谁可以做什么;

通过 .NET 中支持的加密功能可以存储或交换机密、防偷听、检测信息篡改、为存储密码生成单向哈希表和创建数字签名。

Framework 对沙箱和授权都使用权限。权限根据条件阻止代码的执行。沙箱使用代码访问权限;授权使用身份和角色权限。

代码访问安全最常通过 CLR 或托管环境 (如 ASP.NET 或 Internet Explorer) 对你进行限制,而授权通常由你实现,以防止未授权的调用程序访问你的程序。

(P732)

身份和角色安全主要用于编写中间层应用程序和网页应用服务。通常可以对一组角色进行决定,然后对于提供的每个方法,可以要求调用程序为特定角色。

(P738)

为了帮助避免特权提升***,默认情况下 CLR 不允许部分可信的程序集调用完全可信的程序集。

(P753)

System.Security.Cryptography 中的大多数类型位于 mscorlib.dll 和 System.dll 中。 ProtectedData 是一个例外,它位于 System.Security.dll 中。

(P754)

散列法提供了一种加密方式。这种加密方式非常适用于存储数据库中的密码,因为不需要 (或不想要) 看到解密的版本。要进行验证,仅需散列用户输入的信息,然后将其与数据库中存储的信息相比较即可。

不论源数据的长度有多少,散列编码永远为较小的固定大小。这使其在比较文件或检查数据流 (与校验和不同) 时发挥重要作用。源数据中更改任何位置的单个位都会使得散列编码发生巨大的变化。

要进行散列操作,可调用 HashAlgorithm 某个子类 (如 SHA256 或 MD5) 上的 ComputeHash 。

ComputeHash 方法还可以接收字节数组,这对散列法密码非常方便。

Encoding 对象上的 GetBytes 方法将一个字符串转换为一个字节数组; GetString 方法将该数组重新转换为字符串。然而, Encoding 对象无法将加密的或散列的字节数组转换为字符串,因为编码数据通常会破坏文本编码规则。可以使用下列 Convert.ToBase64String 方法和 Convert.FromBase64String 方法代替。这些方法可以使用字节数组和合法 (与 XML 友好) 的字符串相互转换。

MD5 和 SHA256 是 HashAlgorithm 的两个子类型,它们是由 .NET Framework 提供的。下面按照安全等级的升序 (和以字节为单位的散列长度) 列出了主要算法 :

MD5(16) -> SHA1(20) -> SHA256(32) -> SHA384(48) -> SHA512(64)

算法的长度越短,其执行速度就越快。

MD5 的执行速度比 SHA512 的执行速度快 20 多倍,而且非常适合计算文件的校验和。

使用 MD5 每秒钟可以加密数百兆字节,然后将结果存储到 Guid 中 (Guid 的长度恰好为 16 字节,而且作为一个值类型它比字节数组更易于处理) 。然而,较短的散列会增加破解密码的可能性 (两个不同的文件生成相同的散列) 。

在加密密码或其他区分安全等级的数据时,至少应该使用 SHA256 。人们认为在这些情况中使用 MD5 、 SHA1 是不安全的, MD5 和 SHA1 仅适用于防止意外破解,而无法防止有预谋的篡改。

SHA384 的执行速度并不快于 SHA512 的执行速度,如果需要获取比 SHA256 更高的安全性,可以使用 SHA512 。

较长的 SHA 算法适用于密码加密,但是它们需要增强密码策略的强度以减弱字典的威胁 (字典是指者通过对字典中的每个词应用散列算法,创建密码查询表的策略) 。

(P755)

Rfc2898DeriveBytes 和 PasswordDeriveBytes 类可以准确地执行这类增加密码长度的任务。

Framework 还提供了 160 位的 RIPEMD 散列算法,其安全性比 SHA1 稍好。但是,它会受到 .NET 低效实现的影响,这使得其执行速度比 SHA512 的执行速度更慢。

对称加密在加密和解密时使用相同的密钥。 Framework 提供了 4 种对称加密算法,这些算法中 Rijndael 是最方便的。 Rijndael 既快速又安全,而且拥有两个实现 :

  1. Rijndael 类,从 Framework 1.0 之后的版本可以使用它;

  2. Aes 类,它是在 Framework 3.5 中引入的;

除了 Aes 不允许通过更改块尺寸消弱密码外,这两个类几乎相同。

Aes 是 CLR 安全团队推荐使用的类。

(P756)

各个类使用不同的密码系统。 Aes 使用数据密码系统,通过 encryptor 和 decryptor 转换应用密码算法;

CryptoStream 使用数据流加密算法,用于数据流加密。可以使用不同的对称算法替换 Aes ,而仍旧需要使用 CryptoStream 。

CryptoStream 是双向的,因此可以根据是选择 CryptoStreamMode.Read 还是 CryptoStreamMode.Write ,读取数据流或向数据流中写入信息。加密机和解密机都是对读和写的理解,这生成了 4 种组合,这些选择可能使人感到茫然!将读取创建为 “拉” 模型和将写入创建为 “推” 模型可以帮助理解。如果仍旧有疑问,可以将加密的写入和解密的读取作为起点;这通常是最常见的方式。

使用 System.Cryptography 中的 RandomNumberGenerator 可以生成随机密钥或 IV 。实际上它生成的数字是无法预测的或具有密码强度的 (System.Random 类没有提供相同的保证) 。

使用 MemoryStream 完全可以在内存中进行加密和解密。

(P757)

CryptoStream 是一个链接器,它可以将其他流链接起来。

(P759)

公共密钥加密是非对称的,因此加密和解密使用不同的密钥。

(P760)

.NET Framework 提供了许多非对称算法,其中 RSA 是最流行的算法。

【第22章】

(P763)

同步 (Synchronization) 是指协调并发操作,实现可预测的结果。如果有多个线程访问相同的数据,那么同步就非常重要;这个应用领域很容易出现问题。

(P764)

排他锁结构有三种 : lock 语句、 Mutex 和 SpinLock 。 lock 是最方便和最常用的结构 :

  1. Mutex 可以跨越多个进程 (计算机范围的锁) ;

  2. SpinLock 实现了微优化,可以减少高度并发场景的上下文切换;

(P765)

事实上, C# 的 lock 语句是 Monitor.Enter 和 Monitor.Exit 方法调用及 try / finally 语句块的简写语法。

如果未先调用同一个对象的 Monitor.Enter ,而直接调用 Monitor.Exit ,就会抛出异常。

(P766)

为访问任意可写共享域的代码添加锁。即使是最简单的情况 (如某个域的赋值操作) ,也必须考虑同步问题。

(P768)

如果 lock 语句块中抛出异常,则可能破坏通过锁实现的原子操作。

线程可以用嵌套 (重入) 的方式重复锁住同一个对象。

在这些情况中,只有当最外层 lock 语句退出时,或者执行相同数量的 Monitor.Exit 语句,对象才会解除锁。

(P769)

如果两个线程互相等待对方所占用的资源,就会形成死锁,使得双方都无法继续执行。

死锁是多线程中最难解决的问题 —— 特别是其中涉及许多相关对象时。基本上,最难的问题是无法确定调用获取了哪些锁。

(P770)

锁的执行速度很快 : 在目前的计算机上,如果未出现争夺者,那么一般可以在 80 纳秒内获得和释放一个锁;如果出现争夺者,那么相应的上下文切换会将过载增加到毫秒级,但是这个时间远远小于线程的实际调度时间。

Mutex 类似于 C# 的锁,但是它可以支持多个进程。换而言之, Mutex 可用于计算机范围或应用程序范围。

获得和释放一个无争夺的 Mutex 只需要几毫秒 —— 时间比锁操作慢 50 倍。

使用一个 Mutex 类,就可以调用 WaitOne 方法获得锁,或者调用 ReleaseMutex 释放锁。关闭或去掉一个 Mutex 会自动释放互斥锁。与 lock 语句一样, Mutex 只能在它所在的线程上释放。

(P771)

线程安全性主要是通过锁和减少线程交互可能性而实现。

(P789)

从 Framework 4.0 开始,我们可以使用 Lazy 类实现延后初始化。

(P793)

Suspend 和 Resume 可以冻结和解冻另一个线程。虽然在概念上与阻塞不同 (可以通过它的 ThreadState 查询) ,但是冻结的线程就像进入了阻塞状态。与 Interrupt 一样, Suspend / Resume 也缺少有效的用例,并且也可能存在危险;如果暂停一个获得了锁的线程,那么其他线程就无法获得这个锁 (包括自己的锁) ,这使得程序很容易发生死锁。因此, Framework 2.0 废弃了 Suspend 和 Resume 。

(P794)

.NET Framework 提供了四种定时器,以下两种是通用的多线程定时器 :

  1. System.Threading.Timer ;

  2. System.Timers.Timer ;

其他两种是特殊用途的单线程定时器 :

  1. System.Windows.Forms.Timer (Windows Forms 定时器) ;

  2. System.Windows.Threading.DispatcherTimer (WPF 定时器) ;

多线程定时器更加强大、精确和灵活,而在运行需要更新 Windows Forms 控件或 WPF 元素的简单任务时,单线程定时器更加安全和方便。

System.Threading.Timer 是最简单的多线程定时器,它只有一个构造方法和两个方法。

(P795)

.NET Framework 提供另一个与 System.Timers 命名空间中名称相同的定时器类。它简单地封装了 System.Threading.Timer ,在使用完全相同的底层引擎时更加方便。

(P796)

单线程定时器不能在各自环境之外使用。

【第23章】

(P797)

Parallel 类和任务并行结构统称为任务并行库 (Task Parallel Library , TPL) ;

(P798)

通过编程方式利用多内核或多处理器称为并行编程,它是多线程更宽泛概念的一个子集。

(P799)

PFX (Parallel Framework , 并行框架) 主要用于并行编程 : 利用多内核处理器加快计算密集型代码的执行速度。

PLINQ 将自动并行化本地的 LINQ 查询。 PLINQ 的优势是易于使用,因为它把工作划分和结果整理的任务转给了 Framework 。

要使用 PLINQ ,只要在输入序列上调用 AsParallel() 方法,然后继续执行 LINQ 查询。

(P800)

AsParallel 是 System.Linq.ParallelEnumerable 中的一个扩展方法。它基于 ParallelQuery 封装输入序列,使随后调用的 LINQ 查询运算符绑定到 Parallel-Enumerbale 中定义的另一组方法。这为每个标准查询运算符提供了并行实现。基本上,它们的工作原理是将输入序列划分为在不同线程上执行的小块,然后将结果整理到一个输出序列中以供使用。

对于接受两个输入序列的查询运算符 (Join 、 GroupJoin 、 Concat 、 Union 、 Intersect 、 Except 和 Zip) ,必须对这两个输入序列应用 AsParallel() 方法,否则将抛出异常。但不需要在查询进行时一直对它应用 AsParallel ,因为 PLINQ 的查询运算符输出另一个 ParallelQuery 序列。事实上,再次调用 AsParallel 会降低效率,因为它会强制合并和重新划分查询。

PLINQ 仅用于本地集合 : 它不能与 LINQ to SQL 或 Entity Framework 一起使用,因为在这种情况下, LINQ 会转换为 SQL ,然后在数据库服务器上执行。然而,可以使用 PLINQ 基于从数据库查询获得的数据集来执行另外的本地查询。

(P801)

大多数 LINQ to Objects 查询执行速度很快,不仅没有必要并行化,而且划分、整理和协调额外线程的开销实际上会降低执行速度。

和普通的 LINQ 查询一样, PLINQ 查询也是延迟求值的。

(P804)

因为 PLINQ 在并行线程上运行查询,必须注意不能执行非线程安全的操作。

(P806)

PLINQ 的优点之一是,它能够方便地把来自并行工作的结果整理到一个输出序列中。但有时,结束时要做的全部工作就是让序列在每个元素上运行一些函数。

如果这是实情,而且可以忽略元素被处理的顺序,使用 PLINQ 的 ForAll 方法可以提高效率。

ForAll 方法在 ParallelQuery 的每个输出元素上运行一个委托。它正确关联到 PLINQ 的内部,省略了整理和枚举结果的步骤。

(P807)

整理和枚举结果不是复杂的大型操作,因此当存在大量快速执行的输入元素时, ForAll 优化能够获得最佳效果。

PLINQ 有三种用于给线程指派输入元素的划分策略 : 块划分、范围划分、哈希划分;

哈希划分效率相对较低,因为它必须预先计算每个元素的哈希代码,才能在同一线程上处理带有相同哈希代码的元素。如果觉得这样做太慢,唯一的选择就是调用 AsSequential 来禁用并行化。

概括地说,范围划分用于较长的序列,而且当每个元素花费的 CPU 时间大致相等时速度更快。否则,块划分的速度一般更快。

(P816)

Task.Run 可以创建和启动一个 Task 或 Task 。这个方法实际上是 Task.Factory.StartNew 的简写方法,只是后者有更多的重载版本,所以也更加灵活一些。

【第24章】

(P833)

应用域是指运行中的 .NET 程序所在的独立区域。它提供了一个可控内存区域作为程序集和相关配置的容器,同时划定分布式程序的交互区域。

每个 .NET 进程通常拥有一个应用域 : 默认域。默认域在进程开始时由 CLR 自动创建。可以为应用程序建立额外的应用域,并且额外的应用域可以提供隔离,而且与单独的进程相比,降低额外系统开销和交互复杂性。它也可以应用于加载测试、应用程序补丁和运行稳定性错误恢复机制中。

通常情况下,进程的应用域是在用户双击可执行文件或者启动一个系统服务程序的时候,由操作系统建立的。

但是,通过 CLR 的整合,互联网信息服务进程 (IIS) 和数据库服务进程 (SQL) 等也可以拥有应用域。

对于简单应用程序,进程和默认域同时结束运行。但是对于 IIS 和 SQL ,进程控制着 .NET 应用域的生命周期,在合适的时候生成应用域和销毁应用域。

在进程中,可以通过调用静态方法 AppDomain.CreateDomain 和 AppDomain.Unload 创建和销毁应用域。

谨记 : 当由 CLR 在程序开始时创建的应用域即默认域销毁时,应用程序关闭并且销毁该程序其他所有应用域。通过 AppDomain 属性 IsDefaultDomain ,可以确定应用域是否是默认域。

(P834)

ApplicationBase 属性控制应用域的根文件夹,该根文件夹指定了自动检测程序集时的范围。默认域的根文件夹是主要的可执行文件所在的文件夹。对于创建的新应用域,其根文件夹可根据需要任意选取。

(P839)

应用域可以通过命名管道共享数据。

(P840)

管道在其第一次使用的时候被建立。

进程化是指通过委托在其他应用域内实例化对象,这是与其他应用域交互最灵活的方法。

【第25章】

(P844)

P / Invoke 是平台调用服务 (Platform Invocation Services) 的简称,允许访问未托管 DLL 中的函数、构件和回调。

通过在该函数的定义中添加 extern 关键字和 DllImport 属性,可以将该函数定义或一个同名的静态方法,从而在程序中直接调用。

CLR 中包括一个封送器,可以实现 .NET 类型和非托管类型的相互转换。

IntPtr 是一个用来封装非托管句柄的结构,在 32 位平台下,它的位宽是 32 位;在 64 位平台下,它的位宽是 64 位。

(P845)

在 .NET 程序内,仍然有多种类型可以选择。以非托管句柄为例,可以映射为 IntPtr 类型、 int 类型、 uint 类型、 long 类型和 ulong 类型。

大多数情况下非托管句柄封装一个地址或者指针,因此必须转换成一个 IntPtr 类型以匹配 32 位和 64 位的系统。一个典型的示例是 HWND 句柄。

(P846)

如果不能确定怎样调用一个 Win32 方法,通常可以通过搜索方法的名字和 DllImport ,在网络上找到相关的示例。

(P847)

P / Invoke 层作为在托管和非托管代码中一个固有的编程模型,对两者相关的结构映射起到了很大作用。 C# 不但可以调用 C 函数,而且可以作为 C 函数的回调函数,前提是 P / Invoke 层需要映射非托管函数指针到托管代码空间的的合法结构。托管代码中的委托等同于一个指针,因此 P / Invoke 层会将 C# 中的委托与 C 中的指针相互映射。

(P854)

.NET 程序对 COM 对象都有特殊的支持,使得 COM 程序可以在 .NET 程序中调用,反之亦然。 C# 5.0 和 CLR 4.0 增强了在 .NET 中部署和使用 COM 的功能。

(P855)

某种程度上来说, .NET 程序是在 COM 规则上进化而来的 : .NET 平台有助于跨语言开发并且允许二进制组件的更新而不影响依赖于该组件的程序正常运行。

【第26章】

(P861)

正则表达式可以对字符串进行模式化识别。 .NET 中的正则表达式规范是基于编程语言 Perl 5 的,并且支持查找替换功能。

正则表达式一般用于处理下列问题 :

  1. 判定输入字符是否是密码或者手机号;

  2. 将文本数据转换成结构化形式;

  3. 替换文档中固定形式的文本;

一个常用的正则表达式运算符是量词。量词 “?” 表示前面的字符出现一次或者零次。换句话说, “?” 表示前面的字符是可选的。前面的字符可以是单个字符,也可以是放在方括号内的由多个字符构成的复杂结构。

(P862)

Regex.Match 方法可以搜索大型字符串。它返回的对象具有匹配的长度、索引位和匹配的真实值等属性。

可以将 Regex.Match 方法认为是字符串索引方法 IndexOf 的增强版。不同的是 Regex.Match 搜索的是一种模式而非普通字符串。

IsMatch 方法是 Match 的一种捷径,它首先调用 Match 方法,然后判断返回对象的 Success 属性。

默认状态下,正则表达式引擎按照字符串从左到右的顺序进行匹配,所以返回的是左起第一个匹配字符串。可以使用 NextMatch 方法返回更多的匹配值。

Matches 方法通过数组返回所有的匹配值。

另一个常见的正则表达式运算符是交替符,用一个竖线表示 —— “|” 。交替符前后的表达式是可选的。

圆括号将可选的表达式同其他表达式分隔开。

(P863)

Regex 实例是不可更改的。

正则表达式匹配引擎是很快的,就算没有编译,一个简单的匹配也用不了一毫秒。

RegexOptions 标志可以控制正则表达式匹配的行为。

(P864)

当要查找的串中含有元字符,需要在元字符前加反斜杠。

(P865)

\d 表示一个十进制数字,所以 \d 可以匹配任何数字。 \D 表示非数字。

\w 表示一个单词字符,包括字母、数字和下划线。 \W 表示非单词字符,可以用于表示非英语字母。

. 匹配所有字符,除了 \n (但是包括 \r ) 。

如果将 \d 、 \w 、 . 与量词一起使用,可以得到很多的变化。

(P867)

锚点 ^ 和 $ 代表确定的位置,默认表示 :

  1. ^ —— 匹配字符串的开头;

  2. $ —— 匹配字符串的结束;

(P868)

\b 常用来匹配整个单词。

(P870)

Regex.Replace 方法与 string.Replace 的功能类似,不过它使用正则表达式进行查找。

(P871)

静态的 Regex.Split 方法是 string.Split 方法加强版,它使用了正则表达式替换了分隔符的模式。
**

你可能感兴趣的:(.NET,C#,[.Net],-,(C#))