《果壳中的C# C# 5.0 权威指南》 (01-08章) - 学习笔记

《果壳中的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<S> 允许引用转换成 X<B> ,那么称 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>(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<TEventArgs> (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<Tdelegate> 类型的表达式树,该表达式树将 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<T> 时,编译成表达式树的功能;

写匿名方法的方法是 : 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<T> ;

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

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

可枚举对象可以是 :

1. IEnumerable 或 IEnumerable<T> 的实现;

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

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

IEnumerator<T> 和 IEnumerable<T> 在 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<T>

// Enumerator 接口
System.Collections.IEnumerator
System.Collections.Generic.IEnumerator<T>

返回 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<T> 。而 Nullable<T> 是一个轻量的不变结构,它只有两个域,分别是 Value 和 HasValue 。System.Nullable<T> 实质上是很简单的。

public struct Nullable<T> 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 的运算符来处理 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<T> : 如果重载了 (< 、 >) 和 (<= 、 >=),那么还应该实现 IComparable 和 IComparable<T> 。

(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<T> 而不是数组;

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<char> ,所以可以用 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<T> 接口;

Equals 在 System.Object 中定义,所以所有类型都支持这个方法。

Equals 是在运行时根据对象的实际类型解析的。

对于结构体,Equals 会调用每个字段的 Equals 执行结构比较。

(P223)

Equals 很适合用来比较两个未知类型的对象。

object 类提供了一个静态的帮助方法,它的名称是 Equals ,与虚方法相同,但是不会有冲突,因为它接受两个参数。

如果在处理编译时未知类型对象,这是一种能够避免 null 值异常的等值比较算法。

(P224)

静态方法 object.ReferenceEquals 可以实现引用等值比较。

另一种采用引用等值比较的方法是将值转换为 object ,然后再使用 == 运算符。

调用 object.Equals 的结果是强制对值类型执行装箱。这在对性能高度敏感的情况下是不太适合的,因为装箱操作相对于实际比较操作的开销还要高。C# 2.0 引入了一个解决办法,那就是使用 IEquatable<T> 接口。

关键在于实现 IEquatable<T> 所返回的结果与调用 object 的虚方法 Equals 是一样的,但是执行速度会更快。大多数 .NET 基本类型都实现了 IEquatable<T> 。可以在泛型中使用 IEquatable<T> 作为一个约束。

(P225)

默认的等值比较操作有 :

1. 值类型采用的是值相等;

2. 引用类型采用的是引用相等;

此外 :

结构体的 Equals 方法默认采用的是结构值相等。

有时创建一个类型时重载这个行为是很有用的,有以下两种情况我们需要这样做 :

1. 修改相等的语义 —— 当 == 和 Equals 默认行为不符合要求的类型,并且这种行为一般人难以想象时,修改相等的语义是很有用的。

2. 提高结构体的等值比较的执行速度 —— 结构体的默认结构等值比较算法相对较慢。通过重载 Equals 来实现这个过程可以将性能提高 20% 。重载 == 运算符和实现 IEquatable<T> 接口可以实现等值比较的拆箱,并且同样能够将比较速度提高 20% 。

(P226)

重载引用类型的等值语义并不能提高性能。引用等值比较的默认算法已经非常快速,因为它只比较两个 32 位或 64 位引用。

重载等值语义操作步骤总结 :

1. 重载 GetHashCode() 和 Equals() ;

2. (可选) 重载 != 和 == ;

3. (可选) 实现 IEquatable<T> ;

在 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<T> ,其结果应该总是与被重载对象 Equals 方法保持一致,如果自己编写 Equals 方法实现,那么实现 IEquatable<T> 并没有任何的程序开销。

(P229)

除了标准等值协议,C# 和 .NET 还定义了用于确定对象之间相对顺序的协议。基本的协议包括 :

1. IComparable 接口 (IComparable 和 IComparable<T>) ;

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<T> 实现了 IDisposable 。它允许枚举器保存资源引用,并保证这些资源在枚举结束或者中途停止时能够被释放。foreach 语句能够识别这个细节。

(P238)

using 语句保证清理操作的执行。

有时由于下面一个或多个原因而希望实现 IEnumerable 或 IEnumerable<T> :

1. 为了支持 foreach 语句;

2. 为了与任何使用标准集合的组件交互;

3. 作为一个更复杂集合接口实现的一部分;

4. 为了支持集合初始化器;

为了实现 IEnumerable / IEnumerable<T> ,必须提供一个枚举器。可以采用以下三个方法来实现 :

1. 如果这个类 “包装” 了任何一个集合,那么就返回所包装集合的枚举器;

2. 使用 yield return 的迭代器;

3. 实例化 IEnumerator / IEnumerator<T> ;

还可以创建一个现有集合类的子类,Collection<T> 正是基于此目的而设计的。

返回另一个集合的枚举器就是调用内部集合的 GetEnumerator 。然而,这种方法仅仅适合一些最简单的情况,那就是内部集合的元素正好是所需要的类型。

更好的方法是使用 C# 的 yield return 语句编写迭代器。

迭代器是 C# 语言的一个特性,它能够协助完成集合编写,与 foreach 语句协助完成集合遍历的方式是一样的。

迭代器会自动处理 IEnumerable 和 IEnumerator 或者它们的泛型类的实现。

注意, GetEnumerator 实际上不返回一个枚举器,通过解析 yield return 语句,编译器编写一个隐藏的枚举器类,然后重构 GetEnumerator 来实例化和返回这个类。

迭代器很强大,也很简单,并且是 LINQ 实现的基础。

(P240)

因为 IEnumerable<T> 实现了 IEnumerable ,所以必须同时实现泛型和非泛型的 GetEnumerator 。

最后一种编写 GetEnumerator 的方法是编写一个直接实现 IEnumerator 的类。

(P241)

实现 Reset 方法不是必需的,相反,可以抛出一个 NotSupportedException 。

注意,第一次调用 MoveNext 会将位置移到列表的第一个 (而非第二个) 元素。

(P242)

IEnumerable<T> (和 IEnumerable ) —— 支持最少的功能 (只支持枚举) 。

ICollection<T> (和 ICollection ) —— 支持一般的功能 。

IList<T> / IDictionary<K, V> 及其非泛型版本 —— 支持最多的功能 。

大多数情况下不需要实现这些接口,几乎在需要编写一个集合类的任何时候,都可以使用子类 Collection<T> 替代。

泛型和非泛型版本的差别很大,特别是对于 ICollection 。

因为泛型出现在后,而泛型接口是为了后面出现的泛型而开发的。

ICollection<T> 并没有继承 ICollection ;

IList<T> 也没有继承 IList ;

而且 IDictionary<TKey, TValue> 也同样不继承 IDictionary 。

当然,在有利的情况下,集合类本身通常是可以实现某个接口的两个版本的。

.NET Framework 中并没有一种统一使用集合 (collection) 和 列表 (list) 这两个词的方法。我们通常将 集合 (collection) 和 列表 (list) 这两个术语看作在很多方面是同义的,只有在使用具体类型时例外。

ICollection<T> 是对象的可计数集合的标准接口。它提供了很多功能,包括确定集合大小 (Count) 、确定集合中是否存在某个元素 (Contains) 、将集合复制到一个数组 (ToArray) 以及确定集合是否为只读 (IsReadOnly) 。对于可写集合,可能还需要对集合元素执行 Add 、 Remove 和 Clear 操作。而且,由于它继承了 IEnumerable<T> ,所以也支持通过 foreach 语句进行遍历。

(P243)

非泛型的 ICollection 具有与可计数集合类似的功能,但是它不支持修改列表或检查元素成员的功能。

IList<T> 是标准的可按位置索引的接口,除了从 ICollection<T> 和 IEnumerable<T> 继承的功能,它还提供了按位置 (通过一个索引器) 读写元素和按位置 插入 / 删除 元素的功能。

IndexOf 方法可以对列表执行线性搜索,如果未找到指定项,那么返回 -1 。

IList 非泛型版本具有更多的成员方法,因为它继承了少量的 ICollection 成员方法。

(P244)

非泛型 IList 接口的 Add 方法返回一个整数,这是最新添加元素的索引。相反,ICollection<T> 的 Add 方法的返回类型为 void 。

通用的 List<T> 类是 IList<T> 和 IList 的典型表现。C# 数组也同时实现了泛型和非泛型的 IList 。

为了与只读的 Windows Runtime 集合实现互操作,Framework 4.5 引入了一个新的集合接口 IReadOnlyList<T> 。这个接口本身很有用,并且可以看作为 IList<T> 的缩减版本,它只包含列表只读操作所需要的成员。

因为它的类型参数只用在输出位置,所以它被标记为协变式 (covariant) 。

IReadOnlyList<T> 表示一个链表的只读版本,它并不意味着底层实现也是只读的。

IReadOnlyList<T> 与 Windows 运行时类型 IVectorView<T> 相对应。

(P245)

Array 类是所有一维和多维数组的隐式基类,它是实现标准集合接口的最基本类型之一。

Array 类提供了类型统一性,所以常见的方法都适用于所有的数组,而与它们声明或实际的元素类型无关。

由于数组是基本类型,所以 C# 提供了明确的声明和初始化语法。

当使用 C# 语法声明一个数组时,CLR 会在内部将它转化为 Array 的子类 —— 合成一个对应数组维数和元素类型的伪类型。

CLR 也会特别处理数组类型的创建,将它们分配到一块连续的内存空间。因此数组的索引非常高效,但是不允许在创建后修改数组大小。

Array 实现了 IList<T> 的泛型与非泛型的集合接口。

Array 类实例也提供了一个静态的 Resize 方法,但是它实际上是创建一个新数组,然后将每一个元素复制到新数组中。Resize 方法是很低效的,而且程序的数组引用无法修改为新位置。

实现可变大小集合的最好方法是使用 List<T> 类。

(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<T> 的帮助对象;

2. 通过一个 Comparison 委托 : public delegate int Comparison<T> (T x, T y) ;

Comparison 委托采用与 IComparer<T>.CompareTo 相同的语义。

(P251)

作为 Sort 的替代方法,可以使用 LINQ 的 OrderBy 和 ThenBy 运算符。与 Array.Sort 不同的是,LINQ 运算符不会修改原始数组,而是将排序结果保存在一个新的 IEnumerable<T> 序列中。

Array 有 4 个方法可以执行浅拷贝操作 : Clone 、 CopyTo 、 Copy 和 ConstrainedCopy 。前两个方法都是实例方法;后两个方法是静态方法。

Clone —— 方法返回一个全新 (浅拷贝) 的数组;

CopyTo 和 Copy —— 方法复制数组的若干连续元素;

ConstrainedCopy —— 执行一个原子操作 : 如果所有请求的元素都无法成功复制,那么操作会回滚;

Array 还有一个 AsReadOnly 方法,它会返回一个包装器,可以防止元素被重新赋值。

(P252)

System.Linq 命名空间包含另外一些适合用于执行数组转换的扩展方法。这些方法会返回一个 IEnumerable<T> ,它可以通过 Enumerable 的 ToArray 方法转换回一个数组。

在灵活性和性能方面,泛型类更具优势,而它们的非泛型冗余实现则是为了实现向后兼容。

泛型 List<T> 和非泛型 ArrayList 类提供了一种动态调整大小的对象数组实现,它们是集合类中使用最广泛的类。 ArrayList 实现了 IList ,而 List<T> 同时实现了 IList 和 IList<T> 。与数组不同,所有接口都是公开实现的。

在内部,List<T> 和 ArrayList 都维护了一个对象数组,并在超出容量时替换为一个更大的数组。添加元素是很高效的 (因为数组末尾通常还有空闲存储位置) ,但是插入元素的速度会慢一些 (因为插入位置之后的所有元素都必须向后移动才能留出插入空间) 。与数组一样,如果对已排序列表执行 BinarySearch 方法,那么查找是很高效的,但是其他情况效率就不高,因为查找时必须检查每一个元素。

如果 T 是一种值类型,那么 List<T> 的速度会比 ArrayList 快好几倍,因为 List<T> 不需要元素执行装箱和开箱操作。

List<T> 和 ArrayList 具有可以接受已有元素集合的构造函数,它们会将已有集合的每一个元素复制到新的 List<T> 或 ArrayList 中。

(P254)

非泛型 ArrayList 类主要用于向后兼容 Framework 1.x 代码。

ArrayList 的功能与 List<object> 类型相似。当需要一个包含不共享任何相同基类的混合类型元素时,这两种类型是很有用的。在这种情况下,如果需要使用反射机制处理列表,那么选择使用 ArrayList 更具优势。相比于 List<object> ,反射机制更容易处理非泛型的 ArrayList 。

如果定义 System.Linq 命名空间,那么可以通过先调用 Cast 再调用 ToList 的方式将一个 ArrayList 转换为一个泛型 List 。

Cast 和 ToList 是 System.Linq.Enumerable 的扩展方法,是从 .NET Framework 3.5 开始支持的。

LinkedList<T> 是一个泛型的双向链表。双向链表是一系列互相引用的节点,其中每个节点都引用前一个节点、后一个节点及实际存储数据的元素。它的主要优点是元素总是能够高效地插入到链表的任意位置,因为插入节点只需要创建一个新节点,然后修改引用值。然而,查找插入节点的位置可能减慢执行速度,因为链表本身没有直接索引的内在机制;我们必须遍历每一个节点,并且无法执行二叉查找。

(P255)

LinkedList<T> 实现了 IEnumerable<T> 和 ICollection<T> 及其非泛型版本,但是没有实现 IList<T> ,因为它不支持根据索引进行访问。

(P256)

Queue<T> 和 Queue 是一种先进先出 (FIFO) 的数据结构,它们提供了 Enqueue (将一个元素添加到队列末尾) 和 Dequeue (取出并删除队列的第一个元素) 方法。它们还包括一个只返回而不删除队列第一个元素的 Peek 方法,以及一个 Count 属性 (可用来检查出列前的元素个数) 。

虽然队列是可枚举的,但是它们都没有实现 IList<T> / IList ,因为不能够直接通过索引访问它的成员。

队列内部是使用一个可根据需要调整大小的数组来操作的,这与一般的 List 类很类似。队列具有一个直接指向头和尾元素的索引,因此,入列和出列操作是及其快速的 (除非内部的大小需要调整) 。

(P257)

Stack<T> 和 Stack 是后进先出 (LIFO) 的数据结构,它们提供了 Push (添加一个元素到堆栈的顶部) 和 Pop (从堆栈顶部取出并删除一个元素) 方法。它们还提供了一个只读而不删除元素的 Peek 方法,以及 Count 属性和用于导出数据以实现随机访问的 ToArray 方法。

堆栈内部也是使用一个可根据需要调整大小的数组来操作,这一点和 Queue<T> 与 List<T> 类似。

BitArray 是一个保存压缩 bool 值的可动态调整大小的集合。它具有比简单的 bool 数组和 bool 泛型 List 更高的内存使用效率,因为它的每个值只占用一位,而 bool 类型的每个值占用一个字节。

(P258)

HashSet<T> 和 SortedSet<T> 分别是 Framework 3.5 和 4.0 新增加的泛型集合。这两个类都具有以下特点 :

1. 它们的 Contains 方法都使用基于散列的查找而实现快速执行;

2. 它们都不保存重复元素,并且都忽略添加重复值的请求;

3. 无法根据位置访问元素;

SortedSet<T> 按一定顺序保存元素,而 HashSet<T> 则不是。

这些类型的共同点是由接口 ISet<T> 提供的。

HashSet<T> 是通过使用只存储键的散列表实现的;而 SortedSet<T> 则是通过一个 红 / 黑 树实现的。

两个集合都实现了 ICollection<T> 接口。

因为 HashSet<T> 和 SortedSet<T> 实现了 IEnumerable<T> 接口,所以可以将另一种集合作为任意集合操作方法的参数。

SortedSet<T> 的构造函数还接受一个可选的 IComparer<T> 参数 (而非一个等值比较器) 。

(P259)

字典是一种所包含元素均为 键 / 值 对的集合。字典通常都用来执行列表查找和排序。

Framework 通过接口 IDictionary 和 IDictionary<TKey, TValue> 及一组通用的字典类定义了一个标准字典协议。这些类在以下方面有区别 :

1. 元素是否按有序序列存储;

2. 元素是否按位置 (索引) 或按键访问;

3. 类是泛型还是非泛型的;

4. 集合变大时的性能;

(P260)

IDictionary<TKey, TValue> 定义了所有基于 键 / 值 的集合的标准协议。它扩展了 ICollection<T> ,增加了一些基于任意类型的键访问元素的方法和属性。

(P261)

从 Framework 4.5 开始,还出现了一个接口 IReadOnlyDictionary<TKey, TValue> ,它定义了字典成员的只读子集。它与 Windows Runtime 类型 IMapView<K, V> 相对应,当时也是因为相同原因而引入的。

重复的键在所有字典实现中都是禁止的,所以用相同的键调用两次 Add 会抛出一个异常。

直接通过一个 IDictionary<TKey, TValue> 进行枚举会返回一个 KeyValuePair 结构体序列。

非泛型的 IDictionary 接口在原理上与 IDictionary<TKey, TValue> 相同,但是存在以下两个重要的功能区别 :

1. 通过索引器查找一个不存在的键会返回 null (而不是抛出一个异常) ;

2. 使用 Contains 而非 ContainsKey 来检测成员是否存在 ;

枚举一个非泛型 IDictionary 会返回一个 DictionaryEntry 结构体序列。

泛型 Dictionary (和 List<T> 集合一样) 是使用最广泛的集合之一。它使用一个散列表结构来存储键和值,而且快速、高效。

Dictionary<TKey, TValue> 的非泛型版本是 Hashtable ;Framework 中不存在名为 Dictionary 的非泛型类。当我们提到 Dictionary 时,指的是泛型的 Dictionary<TKey, TValue> 类。

Dictionary 同时实现了泛型和非泛型的 IDictionary 接口,而泛型 IDictionary 是公开的接口。

事实上, Dictionary 是泛型 IDictionary 的一个标准实现。

(P262)

Dictionary 和 Hashtable 的缺点是元素是无序的。而且,添加元素时不保存原始顺序。此外,所有字典类型都不允许出现重复值。

(P263)

OrderedDictionary 是一种非泛型字典,它能够保存添加元素的原始顺序。通过使用 OrderedDictionary ,既可以根据索引访问元素,也可以根据键进行访问。

OrderedDictionary 并不是一个有序的字典。

OrderedDictionary 是 Hashtable 和 ArrayList 的组合。

这个类是在 .NET 2.0 中引入的,特殊的是,它没有泛型版本。

ListDictionary 和 HybridDictionary 这两个类都只有非泛型版本。

Framework 只支持两种在内部结构中将内容根据键进行排序的字典 :

1. SortedDictionary<TKey, TValue> ;

2. SortedList <TKey, TValue> (SortedList 是具有相同功能的非泛型版本) ;

(P265)

Collection<T> 类是一个可定制的 List<T> 包装类。

(P267)

CollectionBase 是 Framework 1.0 引入的 Collection<T> 的非泛型版本。它提供了大多数与 Collection<T> 相似的特性,但是使用方式不太灵活。

KeyedCollection<TKey, TItem> 是 Collection<Item> 的子类。它增加也删去了一些功能。它增加的功能是按键访问元素,这与字典很相似,删去的功能是委托自己的内部列表。

KeyedCollection<TKey, TItem> 通常看作是实现了按键进行快速查找的 Collection<TItem> 。

(P269)

KeyedCollection 的非泛型版本称为 DictionaryBase 。

DictionaryBase 存在的目的就是为了向后兼容。

ReadOnlyCollection<T> 是一个包装器,或者称为委托,它提供了集合的一种只读视图。它的用途是允许一个类公开地显示集合的只读访问,但是同时这个类仍然可以在内部进行修改。

【第08章】

(P277)

LINQ 是 Language Integrated Query 的简写,它可以被视为一组语言和框架特性的集合,我们可以使用 LINQ 对本地对象和远程数据源进行结构化的类型安全的查询操作。

在 C# 3.0 和 Framework 3.5 中引入了 LINQ 。

LINQ 可用于查询任何实现了 IEnumerable<T> 接口的集合类型。

LINQ 具有编译时的类型检查及动态查询组合这两大优点。

LINQ 中所有核心类型都包含在 System.Linq 和 System.Linq.Expressions 这两个命名空间中。

LINQ 数据源的基本组成部分是序列和元素。在这里,序列是指任何实现了 IEnumerable<T> 接口的对象,其中的每一项则称为一个元素。

查询运算符是 LINQ 中用于转换序列的方法。通常,查询运算符可接收一个输入序列,并将其转换为一个输出序列。在 System.Linq 命名空间的 Enumerable 类中定义了约 40 种查询运算符,这些运算符都是以静态扩展方法的形式来实现的,称为标准查询运算符。

我们把对本地序列进行的查询操作称为本地查询或者是 LINQ 到对象查询。

LINQ 还支持对那些从远程数据源中动态获取的序列进行查询,这些序列需要实现 IQueryable<T> 接口,而在 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<TSource, TKey> 将每个输入元素关联到一个排序键 TKey ,TKey 的类型也是由 Lambda 表达式中推测出来的,但它的类与同输入类型、输出类型是没有关系的,三者是独立的,类型可以相同也可以不同。

(P284)

实际上我们可以使用传统的方式直接调用 Enumerable 中的各种方法来实现查询运算符的功能,此时在查询过程可以不使用 Lambda 表达式。这种直接调用的方式在对本地集合进行查询时非常好用,尤其是在 LINQ to XML 这种操作中应用最为方便。

传统调用方式并不适合对 IQueryable<T> 类型集合的查询,最典型的就是对数据库的查询,因为在对 IQueryable<T> 类型数据进行查询时,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<T> 接口中的方法完成的,具体的方法实现是在 Queryable 类中。在这种查询中,LINQ 语句不会被编译成 .NET Framework 中间语言 (IL),而会在运行时被解释成查询表达式树来执行。

(P304)

实际上,可以使用 Enumerable 中的方法来查询 IQueryable<T>  类型的数据源,但会遇到一个问题,那就是查询的时候,远端的数据源必须被加载到本地内存中,然后以本地数据源的方式进行处理。可以想象,这种查询的效率非常低,每次都需要读取大量的数据,在本地进行筛选。这正是创建解释型查询的原因。

在 .NET Framework 中有两个类都实现了 IQueryable<T> 接口,这两个类用于实现两种不同的查询 :

1. LINQ to SQL;

2. Entity Framework (EF);

这两种 LINQ-to-db 的查询技术实际上非常相似。

在对本地数据源的查询中,也可以使用 IQueryable<T> 接口中的方法进行查询,只要在本地集合的最后使用一个 AsQueryable 方法即可。

IQueryable<T> 实际上是对 IEnumerable<T> 方法的扩展。

(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<T> 类还提供了 InsertOnSubmit 和 DeleteOnSubmit 方法用于插入和删除数据表中的记录;而 EF 的 ObjectSet<T> 类提供了 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<T> (L2S) - ObjectSet<T> (EF);

4. 将实体中的属性值的变化 (添加、删除等) 更新回数据库 : SubmitChanges (L2S) - SaveChanges (EF);

5. 使用 conetext 更新的方式向数据库中添加新的记录 : InsertOnSubmit (L2S) - AddObject (EF);

6. 使用 context 更新的方式删除记录 : DeleteOnSubmit (L2S) - DeleteObject (EF);

7. 关联表中用于存放多条关联记录的属性 : EntitySet<T> (L2S) - EntityCollection<T> (EF);

8. 关联表中用于存放单条关联记录的属性 : EntityRef<T> (L2S) - EntityReference<T> (EF);

9. 加载关联属性的默认方式 : Lazy (L2S) - Explicit (EF);

10. 构建立即加载的查询方式 : DataLoadOptions (L2S) - Include() (EF);

(P325)

一个查询表达式树是由一个微型的 DOM (Document Object Model ,文档对象模型) 来描述的。这个 DOM 中每个节点都代表了 System.Linq.Expressions 命名空间中的一个类型。

(P326)

Expression<T> 的基类是 LambdaExpression ,LambdaExpression 是 Lambda 表达式树中所有节点的基类型,所有的节点类型都可以转换成这种基类型,因此保证了表达式树中节点的类型一致性。

Lambda 表达式需要接收参数,而普通的表达式则没有参数。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页