《果壳中的C# C# 5.0 权威指南》
========== ========== ==========
[作者] (美) Joseph Albahari (美) Ben Albahari
[译者] (中) 陈昇 管学理 曾少宁 杨庆川
[出版] 中国水利水电出版社
[版次] 2013年08月 第1版
[印次] 2013年08月 第1次 印刷
[定价] 118.00元
========== ========== ==========
【第09章】
(P329)
标准查询运算符可以分为三类 :
1. 输入是集合,输出是集合;
2. 输入是集合,输出是单个元素或者标量值;
3. 没有输入,输出是集合 (生成方法) ;
(P330)
[集合] --> [集合]
1. 筛选运算符 —— 返回原始序列的一个子集。使用的运算符有 : Where 、 Take 、 TakeWhile 、 Skip 、 SkipWhile 、 Distinct ;
2. 映射运算符 —— 这种运算符可以按照 Lambda 表达式指定的形式,将每个输入元素转换成输出元素。 SelectMany 用于查询嵌套的集合;在 LINQ to SQL 和 EF 中 Select 和 SelectMany 运算符可以执行内连接、左外连接、交叉连接以及非等连接等各种连接查询。使用的运算符有 : Select 、 SelectMany ;
3. 连接运算符 —— 用于将两个集合连接之后,取得符合条件的元素。连接运算符支持内连接和左外连接,非常适合对本地集合的查询。使用运算符有 : Join 、 GroupJoin 、 Zip ;
4. 排序运算符 —— 返回一个经过重新排序的集合,使用的运算符有 : OrderBy 、 ThenBy 、 Reverse ;
(P331)
5. 分组运算符 —— 将一个集合按照某种条件分成几个不同的子集。使用的运算符有 : GroupBy ;
6. 集合运算符 —— 主要用于对两个相同类型集合的操作,可以返回两个集合中共有的元素、不同的元素或者两个集合的所有元素。使用的运算符有 : Concat 、 Unoin 、 Intersect 、 Except ;
7. 转换方法 Import —— 这种方法包括 OfType 、 Cast ;
8. 转换方法 Export —— 将 IEnumerable
[集合] --> [单个元素或标量值]
1. 元素运算符 —— 从集合中取出单个特定的元素,使用的运算符有 : First 、 FirstOrDefault 、 Last 、 LastOrDefault 、 Single 、 SingleOrDefault 、 ElementAt 、 ElementAtOrDefault 、 DefaultIfEmpty ;
2. 聚合方法 —— 对集合中的元素进行某种计算,然后返回一个标量值 (通常是一个数字) 。使用的运算符有 : Aggregate 、 Average 、 Count 、 LongCount 、 Sum 、 Max 、 Min ;
3. 数量词 —— 一种返回 true 或者 false 的聚合方法,使用的运算符有 : All 、 Any 、 Contains 、 SequenceEqual ;
(P332)
[空] --> [集合]
第三种查询运算符不需要输入但可以输出一个集合。
生成方法 —— 生成一个简单的集合,使用的方法有 : Empty 、 Range 、 Repeat ;
(P333)
经过各种方法的筛选,最终得到的序列中的元素只能比原始序列少或者相等,绝不可能比原始序列还多。在筛选过程中,集合中的元素类型及元素值是不会改变的,和输入时始终保持一致。
如果和 let 语句配合使用的话,Where 语句可以在一个查询中出现多次。
(P334)
标准的 C# 变量作用域规则同样适用于 LINQ 查询。也就是说,在使用一个查询变量前,必须先声明,否则不能使用。
Where 判断选择性地接受一个 int 型的第二参数。这个参数用于指定输入序列中特定位置上的元素,在查询中可以使用这个数值进行元素的筛选。
下面几个关键字如果用在 string 类型的查询中将会被转换成 SQL 中的 LIKE 关键字 : Contains 、 StartsWith 、 EndsWith 。
Contains 关键字仅用于本地集合的比较。如果想要比较两个不同列的数据,则需要使用 SqlMethods.Like 方法。
SqlMethods.Like 也可以进行更复杂的比较操作。
在 LINQ to SQL 和 EF 中,可以使用 COntains 方法来查询一个本地集合。
如果本地集合是一个对象集合或其他非数值类型的集合,LINQ to SQL 或者 EF ,也可能把 Contains 关键字翻译成一个 EXISTS 子查询。
(P335)
Take 返回集合的前 n 个元素,并且放弃其余元素;Skip 则是跳过前 n 个元素,并且返回其余元素。
在 SQL Server 2005 中,LINQ to SQL 和 EF 中的 Take 和 Skip 运算符会被翻译成 ROW_NUMBER 方法,而在更早的 SQL Server 数据库版本中则会被翻译成 Top n 查询。
TakeWhile 运算符会遍历输入集合,然后输出每个元素,直到给定的判断为 false 时停止输出,并忽略剩余的元素。
SkipWhile 运算符会遍历输入集合,忽略判断条件为真之前的每个元素,直到给定的判断为 false 时输出剩余的元素。
在 SQL 中没有与 TakeWhile 和 SkipWhile 对应的查询方式,如果在 LINQ-to-db 查询中使用,将会导致一个运行时错误。
(P336)
Distinct 的作用是返回一个没有重复元素的序列,它会删除输入序列中的重复元素。在这里,判断两个元素是否重复的规则是可以自定义的,如果没有自定义,那么就使用默认的判断规则。
因为 string 实现了 IEnumerable
在查询一个数据库时, Select 和 SelectMany 是最常用的连接操作方法;对于本地查询来说,使用 Join 和 Group 的效率最好。
在使用 Select 时,通常不会减少序列中的元素数量。每个元素可以被转换成需要的形式,并且这个形式需要通过 Lambda 表达式来定义。
(P337)
在条件查询中,一般不需要对查询结果进行映射,之所以要使用 select 运算符,是为了满足 LINQ 查询必须以 select 或者 group 语句结尾的语法要求。
Select 表达式还接受一个整型的可选参数,这个参数实际上是一个索引,使用它可以得到输入序列中元素的位置。需要注意的是,这种参数只能在本地查询中使用。
可以在 Select 语句中再嵌套 Select 子句来构成嵌套查询,这种嵌套查询的结果是一个多层次的对象集合。
(P338)
内部的子查询总是针对外部查询的某个元素进行。
Select 内部的子查询可以将一个多层次的对象映射成另一个多层次的对象,也可以将一组关联的单层次对象映射成一个多层次的对象模型。
在对本地集合的查询中,如果 Select 语句中包含 Select 子查询,那么整个查询是双重的延迟加载。
子查询的映射在 LINQ to SQL 和 EF 中都可以实现,并且可以用来实现 SQL 的连接功能。
(P339)
我们将查询结果映射到匿名类中,这种映射方式适用于查询过程中暂存中间结果集的情况,但是当需要将结果返回给客户端使用的时候,这种映射方式就不能满足需求了,因为匿名类型只能在一个方法内作为本地变量存在。
(P341)
SelectMany 可以将两个集合组成一个更大的集合。
(P342)
在分层次的数据查询中,使用 SelectMany 和 Select 得到的结果是相同的,但是在查询单层次的数据源 (如数组) 的时候,Select 要完成同样的任务,就需要使用嵌套循环了。
SelectMany 的好处就是在于,无论输入集合是什么类型的,它输出的集合肯定是一个数组类型的二维集合,结果集的数据不会有层次关系。
在查询表达式语法中,from 运算符有两个作用,在查询一开始的 from 的作用都是引入查询集合和范围变量;其他任何位置再出现 from 子句,编译器都会将其翻译成 SelectMany 。
(P343)
在需要用到外部变量的情况下,选择使用查询表达式语法是最佳选择。因为在这种情况中,这种语法不仅便于书写,而且表达方式也更接近查询逻辑。
(P344)
在 LINQ to SQL 和 EF 中, SelectMany 可以实现交叉连接、不等连接、内连接以及左外连接。
(P345)
在标准 SQL 中,所有的连接都要通过 join 关键字实现。
在 Entity Framework 的实体类中,并不会直接存储一个外键值,而是存储外键所关联对象的集合,所以当需要使用外键所关联的数据时,直接使用实体类属性中附带的数据集合即可,不用像 LINQ to SQL 查询中那样手动地进行连接来得到外键集合中的数据。
对于本地集合的查询中,为了提高执行效率,应该尽量先筛选,再连接。
如果有需要的话,可以引入新的表来进行连接,查询时的连接并不限于两个表之间,多个表也可以进行。在 LINQ 中,可以通过添加一个 from 子句来实现。
(P347)
正确的做法是在 DefaultIfEmpty 运算符之前使用 Where 语句。
Join 和 GroupJoin 的作用是连接两个集合进行查询,然后返回一个查询结果集。他们的不同点在于,Join 返回的是非嵌套结构的数据集合,而 GroupJoin 返回的则是嵌套结构的数据集合。
Join 和 GroupJoin 的长处在于对本地集合的查询,也就是对内存中数据的查询效率比较高。它们的缺点是目前只支持内连接和左外连接,并且连接条件必须是相等连接。需要用到交叉连接或者非等值连接时,就只能选择 Select 或者 SelectMany 运算符。在 LINQ to SQL 或者 EF 查询中, Join 和 GroupJoin 运算符在功能上与 Select 和 SelectMany 是没有什么区别的。
(P352)
当 into 关键字出现在 join 后面的时候,编译器会将 into 关键字翻译成 GroupJoin 来执行。而当 into 出现在 Select 或者 Group 子句之后时,则翻译成扩展现有的查询。虽然都是 into 关键字,但是出现在不同的地方,差别非常大。有一点它们是相同的,into 关键字总是引入一个新的变量。
GroupJoin 的返回结果实际上是集合的集合,也就是一个集合中的元素还是集合。
(P355)
Zip 是在 .NET Framework 4.0 中新加入的一个运算符,它可以同时枚举两个集合中的元素 (就像拉链的两边一样) ,返回的集合是经过处理的元素对。
两个集合中不能配对的元素会直接被忽略。需要注意的是,Zip 运算符只能用于本地集合的查询,它不支持对数据库的查询。
经过排序的集合中的元素值和未排序之前是相同的,只是元素的顺序不同。
(P356)
OrderBy 可以按照指定的方式对集合中的元素进行排序,具体的排序方式可以在 KeySelector 表达式中定义。
如果通过 OrderBy 按照指定顺序进行排序后,集合中的元素相对顺序仍无法确定时,可以使用 ThenBy 。
ThenBy 关键字的作用是在前一次排序的基础上再进行一次排序。在一个查询中,可以使用任意多个 ThenBy 关键字。
(P357)
LINQ 中还提供了 OrderByDescending 和 ThenByDescending 关键字,这两个关键字也是用于完成对集合的排序功能,它们的功能和 OrderBy / ThenBy 相同,用法也一样,只是它们排序后的集合中的元素是按指定字段的降序排序。
在对本地集合的查询中,LINQ 会根据默认的 IComparable 接口中的算法对集合中的元素进行排序。如果不想使用默认的排序方式,可以自己实现一个 IComparable 对象,然后将这个对象传递给查询 LINQ 。
在查询表达式语法中我们没有办法将一个 IComparable 对象传递给查询语句,也就不能进行自定义的查询。
在使用了排序操作的查询中,排序运算符会将集合转换成 IEnumerable
(P358)
在对远程数据源的查询中,需要用 AsQueryable 代替 AsEnumerable 。
(P359)
GroupBy 可以将一个非嵌套的集合按某种条件分组,然后将得到的分组结果以组为单位封装到一个集合中。
Enumerable.GroupBy 的内部实现是,首先将集合中的所有元素按照键值的关系存储到一个临时的字典类型的集合中。然后再将这个临时集合中的所有分组返回给调用者。这里一个分组就是一个键和它所对应的一个小集合。
默认情况下,分组之后的元素不会对原始元素做任何处理,如果需要在分组过程中对元素做某些处理的话,可以给元素选择器指定一个参数。
(P360)
GroupBy 只对集合进行分组,并不做任何排序操作,如果想要对集合进行排序的话,需要使用额外的 OrderBy 关键字。
在查询表达式语法中,GroupBy 可以使用下面这个格式来创建 : group 元素表达式 by 键表达式 。
和其他的查询一样,当查询语句中出现了 select 或者 group 的时候,整个查询就结束了,如果不想让查询就此结束,那么就需要扩展整个查询,可以使用 into 关键字。
在 group by 查询中,经常需要扩展查询语句,因为需要对分组后的集合进一步进行处理。
在 LINQ 中, group by 后面跟着 where 查询相当于 SQL 中的 HAVING 关键字。这个 where 所作用的对象是整个集合或者集合中的每个分组,而不是单个元素。
分组操作同样适用于对数据库的查询。如果是在 EF 中,在使用了关联属性的情况下,分组操作并不像在 SQL 中那样常用。
(P361)
LINQ 中的分组功能对 SQL 中的 “GROUP BY” 进行了很大的扩展,可以认为 LINQ 中的分组是 SQL 中分组功能的一个超集。
和传统 SQL 查询不同点是,在 LINQ 中不需要对分组或者排序子句中的变量进行映射。
当需要使用集合中多个键来进行分组时,可以使用匿名类型将这几个键封装到一起。
(P362)
Concat 运算符的作用是合并两个集合,合并方式是将第一个集合中所有元素放置到结果集中,然后再将第二个集合中的元素放在第一个结果集的后面,然后返回结果集。Union 执行的也是这种合并操作,但是它最后会将结果集中重复的元素去除,以保证结果集中每个元素都是唯一的。
当对两个不同类型但基类型却相同的序列执行合并时,需要显式地指定这两个集合的类型以及合并之后的集合类型。
Intersect 运算符用于取出两个集合中元素的交集。Except 用于取出只出现在第一个集合中的元素,如果某个元素在两个集合中都存在,那么这个元素就不会包含在结果中。
Enumerable.Except 的内部实现方式是,首先将第一个集合中的所有元素加载到一个字典集合中,然后再对比第二个集合中的元素,如果字典中的某个元素在第二个集合中出现了,那么就将这个元素从字典中移除。
(P363)
从根本上讲,LINQ 处理的是 IEnumerable
OfType 和 Cast 可以将非 IEnumerable 类型的集合转换成 IEnumerable
Cast 和 OfType 运算符的唯一不同就是它们遇到不相容类型时的处理方式 : Cast 会抛出异常,而 OfType 则会忽略这个类型不相容的元素。
元素相容的规则与 C# 的 is 运算符完全相同,因此只能考虑引用转换和拆箱转换。
Cast 运算符的内部实现与 OfType 完全相同,只是省略了类型检查那行代码。
OfType 和 Cast 的另一个重要功能是 : 按类型从集合中取出元素。
(P365)
ToArray 和 ToList 可以分别将集合转换成数组和泛型集合。这两个运算符也会强制 LINQ 查询语句立即执行,也就是说当整个查询是延迟加载的时候,一旦遇到 ToArray 或者 ToList ,整个语句会被立即执行。
ToDictionary 方法也会强制查询语句立即执行,然后将查询结果放在一个 Dictionary 类型的集合中。 ToDictionary 方法中的键选择器必须为每个元素提供一个唯一的键,也就是说不同元素的键是不能重复的,否则在查询的时候系统会抛出异常。而 Tolookup 方法的要求则不同,它允许多个元素共用相同的键。
AsEnumerable 将一个其他类型的集合转换成 IEnumerable
AsQueryable 方法则会将一个其他类型的集合转换成 IQueryable
(P366)
所有以 "OrDefault" 结尾的方法有一个共同点,那就是当集合为空或者集合中没有符合要求的元素时,这些方法不抛出异常,而是返回一个默认类型的值 default(TSource) 。
对于引用类型的元素来说 default(TSource) 是 null ,而对于值类型的元素来说,这个默认值通常是 0 。
为了避免出现异常,在使用 Single 运算符时必须保证集合中有且仅有一个元素;而 SingleOrDefault 运算符则要求集合中有一个或零个元素。
Single 是所有元素运算符中要求最多的,而 FirstOrDefault 和 LastOrDefault 则对集合中的元素没有什么要求。
(P367)
在 LINQ to SQL 和 EF 中, Single 运算符通常应用于使用主键到数据库中查找特定的单个元素。
ElementAt 运算符可以根据指定的下标取出集合中的元素。
Enumerable.ElementAt 的实现方式是,如果它所查询的集合实现了 IList
DefaultIfEmpty 可以将一个空的集合转换成 null 或者 default() 类型。这个运算符一般用于定义外连接查询。
(P368)
Count 运算符的作用是返回集合中元素的个数。
Enumerable.Count 方法的内部实现方式如下 : 首先判断输入集合有没有实现 ICollection
还可以为 Count 这个方法添加一个筛选条件。
LongCount 运算符的作用和 Count 是相同的,只是它的返回值类型是 int64 ,也就是它能用于大数据量的统计, int64 能统计大概 20 亿个元素的集合。
Min 和 Max 返回集合中最小和最大的元素。
如果集合没有实现 IComparable
选择器表达式不仅定义了元素的比较方式,还定义了最后的结果集的类型。
(P369)
Sum 和 Average 的返回值类型是有限的,它们内置了以下几种固定的返回值类型 : int 、 long 、 float 、 double 、 decimal 以及这几种类型的可空类型。这里返回值都是值类型,也就是,Sum 和 Average 的预期结果都是数字。而 Min 和 Max 则会返回所有实现了 IComparable
更进一步讲, Average 值返回两种类型 : decimal 和 double 。
Average 为了避免查询过程中数值的精度损失,会自动将返回值类型的精度升高一级。
(P370)
Aggregate 运算符我们可以自定义聚合方法,这个运算符只能用于本地集合的查询中,不支持 LINQ to SQL 和 EF 。这个运算符的具体功能要根据它在特定情况下的定义来看。
Aggregate 运算符的第一个参数是一个种子,用于指示统计结果的初始值是多少;第二个参数是一个表达式,用于更新统计结果,并将统计结果赋值给新的变量;第三个参数是可选的,用于将统计结果映射成期望的形式。
Aggregate 运算符最大的问题是,它实现的功能通过 foreach 语句也可以实现,而且 foreach 语句的语法更清晰明了。 Aggregate 的主要用处在于处理比较大或者比较复杂的聚合操作。
(P372)
Contains 关键字接收一个 TSource 类型的参数;而 Any 的参数则定义了筛选条件,这个参数是可选的。
Any 关键字对集合中元素的要求低一点,只要集合中有一个元素符合要求,就返回 true 。
Any 包含了 Contains 关键字的所有功能。
如果在使用 Any 关键字的时候不带参数,那么只要集合中有一个元素符合要求,就返回 true 。
Any 关键字在子查询中使用特别广泛,尤其是在对数据库的查询中。
当集合中的元素都符合给定的条件时, All 运算符返回 true 。
SequenceEqual 用于比较两个集合中的元素是否相同,如果相同则返回 true 。它的筛选条件要求元素个数相同、元素内容相同而且元素在集合中的顺序也必须是相同的。
(P373)
Empty 、 Repeat 和 Range 都是静态的非扩展方法,它们只能用于本地集合中。
Empty 用于创建一个空的集合,它需要接收一个用于标识集合类型的参数。
和 “??” 运算符配合使用的话,Empty 运算符可以实现 DefaultEmpty 的功能。
Range 和 Repeat 运算符只能使用在整型集合中。
Range 接收两个参数,分别用于指示起始元素的下标和查询元素的个数。
Repeat 接收两个参数,第一个参数是要创建的元素,第二个参数用于指示重复元素的个数。
【第10章】
(P375)
在 .NET Framework 中提供了很多用于处理 XML 数据的 API 。从 .NET Framework 3.5 之后,LINQ to XML 成为处理通用 XML 文档的首选工具。它提供了一个轻量的集成了 LINQ 友好的 XML 文档对象模型,当然还有相应的查询运算符。在大多数情况下,它完全可以替代之前 W3C 标准的 DOM 模型 (又称为 XmlDocument) 。
LINQ to XML 中 DOM 的设计非常完善且高效。即使没有 LINQ ,单纯的 LINQ to XML 中 DOM 对底层 XmlReader 和 XmlWriter 类也进行了很好的封装,可以通过它来更简单地使用这两个类中的方法。
LINQ to XML 中所有的类型定义都包含在 System.Xml.Linq 命名空间中。
所有 XML 文件一样,在文件开始都是声明部分,然后是根元素。
属性由两部分组成 : 属性名和属性值。
(P376)
声明、元素、属性、值和文本内容这些结构都可以用类来表示。如果这种类有很多属性来存储子内容,我们可以用一个对象树来完全描述文档。这个树状结构就是文档对象模型 (Document Object Model) ,简称 DOM 。
LINQ to XML 由两部分组成 :
1. 一个 XML DOM ,我们称之为 X-DOM ;
2. 约 10 个用于查询的运算符;
可以想象, X-DOM 是由诸如 XDocument 、 XElement 、 XAttribute 等类组成的。有意思的是, X-DOM 类并没有和 LINQ 绑定在一起,也就是说,即使不使用 LINQ 查询,也可以加载、更新或存储 X-DOM 。
X-DOM 是集成了 LINQ 的模型 :
1. X-DOM 中的一些方法可以返回 IEnumerable 类型的集合,使 LINQ 查询变得非常方便;
2. X-DOM 的构造方法更加灵活,可以通过 LINQ 将数据直接映射成 X-DOM 树;
XObject 是整个继承结构的根, XElement 和 XDocument 则是平行结构的根。
XObject 是所有 X-DOM 内容的抽象基类。在这个类型中定义了一个指向 Parent 元素的链接,这样就可以确定节点之间的层次关系。另外这个类中还有一个 XDocument 类型的对象可供使用。
除了属性之外, XNode 是其他大部分 X-DOM 内容的基类。 XNode 的一个重要特性是它可以被有顺序地存放在一个混合类型的 XNodes 集合中。
XAttribute 对象的存储方式 —— 多个 XAttribute 对象必须成对存放。
(P377)
虽然 XNode 可以访问它的父节点 XElement ,但是它却对自己的子节点一无所知,因为管理子节点的工作是由子类 XContainer 来做的。 XContainer 中定义了一系列成员和方法来管理它的子类,并且是 XElement 和 XDocument 的抽象基类。
除了 Name 和 Value 之外, XElement 还定义了其他的成员来管理自己的属性,在绝大多数情况下, XElement 会包含一个 XText 类型的子节点, XElement 的 Value 属性同时包含了存取这个 XText 节点的 get 和 set 操作,这样可以更方便地设置节点值。由于 Value 属性的存在,我们可以不必直接使用 XText 对象,这使得对节点的赋值操作变得非常简单。
(P378)
XML 树的根节点是 XDocument 对象。更准确地说,它封装了根 XElement ,添加了 XDeclaration 以及一些根节点需要执行的指令。与 W3C 标准的 DOM 有所不同,即使没有创建 XDocument 也可以加载、操作和保存 X-DOM 。这种对 XDocument 的不依赖性使得我们可以很容易将一个节点子树移到另一个 X-DOM 层次结构中。
XElement 和 XDocument 都提供了静态 Load 和 Parse 方法,使用这两个方法,开发者可以根据已有的数据创建 X-DOM :
1. Load 可以根据文件、 URI 、 Stream 、 TextReader 或者 XmlReader 等构建 X-DOM ;
2. Parse 可以根据字符串构建 X-DOM ;
(P379)
在节点上调用 ToString 方法可将这个节点中的内容转换成 XML 字符串,默认情况下,转换后的 XML 字符串是经过格式化的,即使用换行和空格将 XML 字符串按层次结构逐行输出,且使用正确的缩进格式。如果不想让 ToString 方法格式化 XML ,那么可以指定 SaveOptions.DisableFormatting 参数。
XElement 和 XDocument 还分别提供了 Save 方法,使用这个方法可将 X-DOM 写入文件、 Stream 、 TextWriter 或者 XmlWriter 中。如果选择将 X-DOM 写入到一个文件中,则会自动写入 XML 声明部分。另外, XNode 类还提供了一个 WriteTo 方法,这个方法只能向 XmlWriter 中写入数据。
创建 X-DOM 树常用的方法是手动实例化多个节点,然后通过 XContainer 的 Add 方法将所有节点拼装成 XML 树,而不是通过 Load 或者 Parse 方法。
要构建 XElement 和 XAttribute ,只需提供属性名和属性值。
构建 XElement 时,属性值不是必须的,可以只提供一个元素名并在其后添加内容。
注意,当需要为一个对象添加属性值时,只需设置一个字符串即可,不用显式创建并添加 XText 子节点, X-DOM 的内部机制会自动完成这个操作,这使得活加属性值变得更加容易。
(P380)
X-DOM 还支持另一种实例化方式 : 函数型构建 (源于函数式编程) 。
这种构建方式有两个优点 : 第一,代码可以体现出 XML 的结构;第二,这种表达式可以包含在 LINQ 查询的 select 子句中。
之所以以函数型构建的方式定义 XML 文件,是因为 XElement (和 XDocument) 的构造方法都可重载,以接受 params 对象数组 : public XElement(XName name, params object[] content) 。
XContainer 类的 Add 方法同样也接收这种类型的参数 : public void Add(params object[] content) 。
所以,我们可以在构建或添加 X-DOM 时指定任意数目、任意类型的子对象。这是因为任何内容都是合法的。
XContainer 类内部的解析方式 :
1. 如果传入的对象是 null ,那么就忽略这个节点;
2. 如果传入对象是以 XNode 或者 XStreamingElement 作为基类,那么就将这个对象添加为 Node 对象,放到 Nodes 集合中;
3. 如果传入对象是 XAttribute ,那么就将这个对象作为 Attribute 集合来处理;
4. 如果对象是 string ,那么这个对象会被封装成一个 XText 节点,然后添加到 Nodes 集合中;
5. 如果对象实现了 IEnumerable 接口,则对其进行枚举,每个元素都按照上面的规则来处理;
6. 如果某个类型不符合上述任一条件,那么这个对象会被转换成 string ,然后被封装在 XText 节点上,并添加到 Nodes 集合中;
上述所有情况最终都是 : Nodes 或 Attributes 。另外,所有对象都是有效的,因为最终肯定可以调用它的 ToString 方法并将其作为 XText 节点来处理。
实际上, X-DOM 内部在处理 string 类型的对象时,会自动执行一些优化操作,也就是简单地将文本内容存放在字符串中。直到 XContainer 上调用 Nodes 方法时,才会生成实际的 XText 节点。
(P382)
与在 XML 中一样, X-DOM 中的元素和属性名是区分大小写的。
使用 FirstNode 与 LastNode 可以直接访问第一个或最后一个子节点;Nodes 返回所有的子节点并形成一个序列。这三个函数只用于直系的子节点。
(P383)
Elements() 方法返回类型为 XElement 的子节点。
Elements() 方法还可以只返回指定名字的元素。
(P384)
Element() 方法返回匹配给定名称的第一个元素。Element 对于简单的导航是非常有用的。
Element 的作用相当于调用 Elements() ,然后再应用 LINQ 的 FirstOrDefault 查询运算符给定一个名称作为匹配断言。如果没有找到所请求的元素,则 Element 返回 null 。
XContainer 还定义了 Descendants 和 DescendantNodes 方法,它们递归地返回子元素或子节点。
Descendants 接受一个可选的元素名。
(P385)
所有的 XNodes 都包含一个 Parent 属性,另外还有一个 AncestorXXX 方法用来找到特定的父节点。一个父节点永远是一个 XElement 。
Ancestors 返回一个序列,其第一个元素是 Parent ,下一个元素则是 Parent.Parent ,依次类推,直到根元素。
还可以使用 LINQ 查询 AncestorsAndSelf().Last() 来取得根元素。
另外一种方法是调用 Document.Root ,但只有存在 XDocument 时才能执行。
使用 PreviousNode 和 NextNode (以及 FirstNode / LastNode) 方法查找节点时,相当于从一个链表中遍历所有节点。事实上 XML 中节点的存储结构确实是链表。
(P386)
XNode 存储在一个单向链表中,所以 PreviousNode 并不是当前元素的前序元素。
Attributes 方法接受一个名称并返回包含 0 或 1 个元素的序列;在 XML 中,元素不能包含重复的属性名。
可以使用下面这几种方式来更新 XML 中的元素和属性 :
1. 调用 SetValue 方法或者重新给 Value 属性赋值;
2. 调用 SetElementValue 或 SetAttributeValue 方法;
3. 调用某个 RemoveXXX 方法;
4. 调用某个 AddXXX 或 ReplaceXXX 方法指定更新的内容;
也可以为 XElement 对象重新设置 Name 属性。
使用 SetValue 方法可以使用简单的值替换元素或者属性中原来的值。通过 Value 属性赋值会达到相同的效果,但只能使用 string 类型的数据。
调用 SetValue 方法 (或者为 Value 重新赋值) 的结果就是它替换了所有的子节点。
(P387)
最好的两个方法是 : SetElementValue 和 SetAttributeValue 。它们提供了一种非常便捷的方式来实例化 XElement 或 XAttribute 对象,然后调用父节点的 Add 方法,将新节点加入到父节点下面,从而替换相同名称的任何现有元素或属性。
Add 方法将一个子节点添加到一个元素或文档中。AddFirst 也一样,但它将节点插入集合的开头而不是结尾。
我们也可以通过调用 RemoveNodes 或 RemoveAttributes 将所有的子节点或属性全部删除。 RemoveAll 相当于同时调用了这两个方法。
ReplaceXXX 方法等价于调用 Removing ,然后再调用 Adding 。它们拥有输入参数的快照,因此 e.ReplaceNodes(e.Nodes) 可以正常进行。
AddBeforeSelf 、 AddAfterSelf 、 Remove 和 ReplaceWith 方法不能操作一个节点的子节点。它们只能操作当前节点所在的集合。这就要求当前节点都有父元素,否则在使用这些方法时就会抛出异常。此时 AddBeforeSelf 和 AddAfterSelf 方法非常有用,这两个方法可以将一个新节点插入到 XML 中的任意位置。
(P388)
Remove 方法可以将当前节点从它的父节点中移除。ReplaceWith 方法实现同样的操作,只是它在移除旧节点之后还会在同一位置插入其他内容。
通过 System.Xml.Linq 中的扩展方法,我们可以使用 Remove 方法整组地移除节点或者属性。
(P389)
Remove 方法的内部实现机制是这样的 : 首先将所有匹配的元素读取到一个临时列表中,然后枚举该临时列表并执行删除操作。这避免了在删除的同时进行查询操作所引起的错误。
XElement 和 XAttribute 都有一个 string 类型的 Value 属性,如果一个元素有 XText 类型的子节点,那么 XElement 的 Value 属性就相当于访问此节点的快捷方式,对于 XAttribute 的 Value 属性就是指属性值。
有两种方式可以设置 Value 属性值 : 调用 SetValue 方法或者直接给 Value 属性赋值。 SetValue 方法要复杂一些,因为它不仅可以接收 string 类型的参数,也可以设置其他简单的数据类型。
(P390)
由于有了 Value 的值,你可能会好奇什么时候才需要直接和 XText 节点打交道?答案是 : 当拥有混合内容时。
(P391)
向 XElement 添加简单的内容时, X-DOM 会将新添加的内容附加到现有的 XText 节点后面,而不会新建一个 XText 节点。
如果显式地指定创建新的 XText 节点,最终会得到多个子节点。
XDocument 封装了根节点 XElement ,可以添加 XDeclaration 、处理指令、说明文档类型以及根级别的注释。
XDocument 是可选的,并且能够被忽略或者省略,这点与 W3C DOM 不同。
XDocument 提供了和 XElement 相同的构造方法。另外由于它也继承了 XContainer 类,所以也支持 AddXXX 、 RemoveXXX 和 ReplaceXXX 等方法。但与 XElement 不同,一个 XDocument 节点可添加的内容是有限的 :
1. 一个 XElement 对象 (根节点) ;
2. 一个 XDeclaration 对象;
3. 一个 XDocumentType 对象 (引用一个 DTD) ;
4. 任意数目的 XProcessingInstruction 对象;
5. 任意数目的 XComment 对象;
(P392)
对于 XDocument 来说,只有根 XElement 对象是必须的。 XDeclaration 是可选的,如果省略,在序列化的过程中会应用默认设置。
(P393)
XDocument 有一个 Root 属性,这个属性是取得当前 XDocument 对象单个 XElement 的快捷方式。其反向的链接是由 XObject 的 Document 属性提供的,并且可以应用于树中的所有对象。
XDocument 对象的子节点是没有 Parent 信息的。
XDeclaration 并不是 XNode 类型的,因此它不会出现在文档的 Nodes 集合中,而注释、处理指令和根元素等都会出现在 Nodes 集合中。
XDeclaration 对象专门存放在一个 Declaration 属性中。
XML 声明是为了保证整个文件被 XML 阅读器正确解析并理解。
XElement 和 XDocument 都遵循下面这些 XML 声明的规则 :
1. 在一个文件名上调用 Save 方法时,总是自动写入 XML 声明;
2. 在 XmlWriter 对象上调用 Save 方法时,除非 XmlWriter 特别指出,都则都会写入 XML 声明;
3. ToString 方法从来都不返回 XML 声明;
如果不想让 XmlWriter 创建 XML 声明,可以在构建 XmlWriter 对象时,通过设置 XmlWriterSettings 对象的 OmitXmlDeclaration 和 ConformanceLevel 属性来实现。
是否有 XDeclaration 对象对是否写入 XML 声明没有任何影响。 XDeclaration 的目的是提示进行 XML 序列化进程,方式有两种 :
1. 使用的文本编码标准;
2. 定义 XML 声明中 encoding 和 standalone 两个属性的值 (如果写入声明) ;
XDeclaration 的构造方法接受三个参数,分别用于设置 version 、 encoding 和 standalone 属性。
(P394)
XML 编写器会忽略所指定的 XML 版本信息,始终写入 “1.0” 。
需要注意的是,XML 声明中指定的必须是诸如 “utf-16” 这样的 IETF 编码方式。
XML 命名空间有两个功能。首先,与 C# 的命名空间一样,它们可以帮助避免命名冲突。当要合并来自两个不同 XML 文件的数据时,这可能会成为一个问题。其次,命名空间赋予了名称一个绝对的意义。
(P395)
xmlns 是一个特殊的保留属性,以上用法使它执行下面两种功能 :
1. 它为有疑问的元素指定了一个命名空间;
2. 它为所有后代元素指定了一个默认的命名空间;
有前缀的元素不会为它的后代元素定义默认的命名空间。
(P396)
使用 URI (自定义的 URI) 作为命名空间是一种通用的做法,这可以有效地保证命名空间的唯一性。
对于属性来说,最好不使用命名空间,因为属性往往是对本地元素起作用。
有多种方式可以指定 XML 命名空间。第一种方式是在本地名字前面使用大括号来指定。第二种方式 (也是更好的一种方式) 是通过 XNamespace 和 XName 为 XML 设置命名空间。
(P397)
XName 还重载了 + 运算符,这样无需使用大括号即可直接将命名空间和元素组合在一起。
在 X-DOM 中有很多构造方法和方法都能接受元素名或者属性名作为参数,但它们实际上接受 XName 对象,而不是字符串。到目前为止我们都是在用字符串作参数,之所以可以这么用,是因为字符串可以被隐式转换成 XName 对象。
除非需要输出 XML ,否则 X-DOM 会忽略默认命名空间的概念。这意味着,如果要构建子 XElement ,必须显式地指定命名空间,因为子元素不会从父元素继承命名空间。
(P398)
在使用命名空间时,一个很容易犯的错误是在查找 XML 的元素时没有指定它所属的命名空间。
如果在构建 X-DOM 树时没有指定命名空间,可以在随后的代码中为每个元素分配一个命名空间。
【第11章】
(P407)
System.Xml ,命名空间由以下命名空间和核心类组成 :
System.Xml.* ——
1. XmlReader 和 XmlWriter : 高性能、只向前地读写 XML 流;
2. XmlDocument : 代表基于 W3C 标准的文档对象模型 (DOM) 的 XML 文档;
System.Xml.XPath —— 为 XPath (一种基于字符串的查询 XML 的语言) 提供基础结构和 API (XPathNavigator 类) ;
System.Xml.XmlSchema —— 为 (W3C) XSD 提供基础机构和 API ;
System.Xml.Xsl —— 为使用 (W3C) XSLT 对 XML 进行解析提供基础结构和 API ;
System.Xml.Serialization —— 提供类和 XML 之间的序列化;
System.Xml.XLinq —— 先进的、简化的、 LINQ 版本的 XmlDocument 。
W3C 是 World Web Consortium (万维网联盟) 的缩写,定义了 XML 标准。
静态类 XmlConvert 是解析和格式化 XML 字符串的类。
XmlReader 是一个高性能的类,能够以低级别、只向前的方式读取 XML 流。
(P408)
通过调用静态方法 XmlReader.Create 来实例化一个 XmlReader 对象,可以向这个方法传递一个 Stream 、 TextReader 或者 URI 字符串。
因为 XmlReader 可以读取一些可能速度较慢的数据源 (Stream 和 URI) ,所以它为大多数方法提供了异步版本,这样我们可以方便编写非阻塞代码。
XML 流以 XML 节点为单位。读取器按文本顺序 (深度优先) 来遍历 XML 流, Depth 属性返回游标的当前深度。
从 XmlReader 读取节点的最基本的方法是调用 Read 方法。它指向 XML 流的下一个节点,相当于 IEnumerator 的 MoveNext 方法。第一次调用 Read 会把游标放置在第一个节点,当 Read 方法返回 false 时,说明游标已经到达最后一个节点 在这个时候 XmlReader 应该被关闭。
(P409)
属性没有包含在基于 Read 的遍历中。
XmlReader 提供了 Name 和 Value 这两个 string 类型的属性来访问节点的内容。根据节点类型,内容可能定义在 Name 或 Value 上,或者两者都有。
(P410)
验证失败会导致 XmlReader 抛出 XmlException ,这个异常包含错误发生的行号 (LineNumber) 和位置 (LinePosition) 。当 XML 文件很大时记录这些信息会比较关键。
(P413)
XmlReader 提供了一个索引器以直接 (随机) 地通过名字或位置来访问一个节点的属性,使用索引器等同于调用 GetAttributes 方法。
(P415)
XmlWriter 是一个 XML 流的只向前的编写器。 XmlWriter 的设计和 XmlReader 是对称的。
和 XmlReader 一样,可以通过调用静态方法 Create 来构建一个 XmlWriter 。
(P416)
除非使用 XmlWriterSettings ,并设置其 OmitXmlDeclaration 为 true 或者 ConfermanceLevel 为 Fragment ,否则 XmlWriter 会自动地在顶部写上声明。并且后者允许写多个根节点,如果不设置的话会抛出异常。
WriteValue 方法写一个文本节点。它不仅接受 string 类型的参数,还可以接受像 bool 、 DateTime 类型的参数,实际在内部调用了 XmlConvert 来实现符合 XML 字符串解析。
WriteString 和调用 WriteValue 传递一个 string 参数实现的操作是等价的。
在写完开始节点后可以立即写属性。
(P417)
WriteRaw 直接向输出流注入一个字符串。也可以通过接受 XmlReader 的 WriteNode 方法,把 XmlReader 中的所有内容写入输出流。
XmlWriter 使代码非常简洁,如果相同的命名空间在父元素上已声明,它会自动地省略子元素上命名空间的声明。
(P420)
可以在使用 XmlReader 或 XmlWriter 使代码复杂时使用 X-DOM ,使用 X-DOM 是处理内部元素的最佳方式,这样就可以兼并 X-DOM 的易用性和 XmlReader 、 XmlWriter 低内存消耗的特点。
(P421)
XmlDocument 是一个 XML 文档的内存表示,这个类型的对象模型和方法与 W3C 所定义的模式一致。如果你熟悉其他符合 W3C 的 XML DOM 技术,就会同样熟悉 XmlDocument 类。但是如果和 X-DOM 相比的话, W3C 模型就显得过于复杂。
(P422)
可以实例化一个 XmlDocument ,然后调用 Load 或 LoadXml 来从一个已知的源加载一个 XmlDocument :
1. Load 接受一个文件名、 流 (Stream) 、 文本读取器 (TextReader) 或者 XML 读取器 (XmlReader) ;
2. LoadXml 接受一个 XML 字符串;
相对应的,通过调用 Save 方法,传递文件名, Stream 、 TextReader 或者 XmlWriter 参数来保存一个文档。
通过定义在 XNode 上的 ChildNodes 属性可以深入到此节点的下层树型结构,它返回一个可索引的集合。
而使用 ParentNode 属性,可以返回其父节点。
XmlNode 定义了一个 Attributes 属性用来通过名字或命名空间或顺序位置来访问属性。
(P423)
InnerText 属性代表所有子文本节点的联合。
设置 InnerText 属性会用一个文本节点替换所有子节点,所以在设置这个属性时要谨慎以防止不小心覆盖了所有子节点。
InnerXml 属性表示当前节点中的 XML 片段。
如果节点类型不能有子节点, InnerXml 会抛出一个异常。
XmlDocument 创建和添加新节点 :
1. 调用 XmlDocument 其中一个 CreateXXX 方法;
2. 在父节点上调用 AppendChild 、 PrependChild 、 InsertBefore 或者 InsertAfter 来添加新节点到树上;
要创建节点,首先要有一个 XmlDocument ,不能像 X-DOM 那样简单地实例化一个 XmlElement 。节点需要 “寄生” 在一个 XmlDocument 宿主上。
(P424)
可以以任何属顺序来构建这棵树,即便重新排列添加子节点后的语句顺序,对此也没有影响。
也可以调用 RemoveChild 、 ReplaceChild 或者 RemoveAll 来移除节点。
使用 CreateElement 和 CreateAttribute 的重载方法可以指定命名空间和前缀。
CreateXXX (string name);
CreateXXX (string name, string namespaceURI);
CreateXXX (string prefix, string localName, string namespaceURI);
参数 name 既可以是本地名称 (没有前缀) ,也可以是带前缀的名称。
参数 namespaceURI 用在当且仅当声明 (而不是仅在引用) 一个命名空间时。
XPath 是 XML 查询的 W3C 标准。在 .NET Framework 中, XPath 可以查询一个 XmlDocument ,就像用 LINQ 查询 X-DOM 。然而 XPath 应用更广泛,它也在其他 XML 技术中被使用,例如 XML Schema 、 XLST 和 XAML 。
XPath 查询按照 XPath 2.0 数据模型 (XPath Data Model) 来表示。 DOM 和 XPath 数据模型都表示一个 XML 文档树。区别是 XPath 数据模型纯粹以数据为中心,采取了 XML 文本的格式。例如在 XPath 数据模型中,CDATA 部分不是必需的,因为 CDATA 存在的唯一原因是可以在文本中包含 XML 的一些标识符。
(P425)
可以使用下面的方式在代码中实现 XPath 查询 :
1. 在一个 XmlDocument 或 XmlNode 上调用 SelectXXX 方法;
2. 从一个 XmlDocument 或者 XPathDocument 上生成一个 XPathNavigator ;
3. 在 XNode 上调用一个 XPathXXX 扩展方法;
SelectXXX 方法接受一个 XPath 查询字符串。
(P426)
XPathNavigator 是 XML 文档的 XPath 数据模型上的一个游标,他被加载并提供了一些基本方法可以在文档树上移动光标。
XPathNavigator 的 Select* 方法可以使用一个 XPath 字符串来表达更复杂的导航或查询以返回多个节点。
可以从一个 XmlDocument 、 XPathDocument 或者另一个 XPathNavigator 上来生成 XPathNavigator 实例。
(P427)
在 XPath 数据模型中,一个节点的值是文本元素的连接,等同于 XmlDocument 的 InnerText 属性。
SelectSingleNode 方法返回一个 XPathNavigator 。 Select 方法返回一个 XPathNodeInterator 以在多个 XPathNavigator 上进行简便地遍历。
为了更快地查询,可以把 XPath 编译成一个 XPathExpression ,然后传递给 Select* 方法。
(P428)
XmlDocument 和 XPathNavigator 的 Select* 方法有对应的重载函数来接受一个 XmlNamespaceManager 。
XPathDocument 是符合 W3C XPath 数据模型的只读的 XML 文档。使用 XPathDocument 后跟一个 XPathNavigator 要比一个单纯的 XmlDocument 快,但是不能对底层的文档进行更改。
(P429)
XSD 文档本身就是用 XML 来写的,并且 XSD 文档也是用 XSD 来介绍的。
可以在读或处理 XML 文件或文档时用一个或多个模式来验证它,这样做有以下几个理由 :
1. 可以避免更少的错误检查和异常处理;
2. 模式检验可以查出注意不到的错误;
3. 错误信息比较详细重要;
为进行验证,可以把模式加入到 XmlReader 、 XmlDocument 或者 X-DOM 对象中,然后像通常那样读取或加载 XML 文档。模式验证会在内容被读的时候自动进行,所以输入流没有被读取两次。
(P430)
在 System.Xml 命名空间下包含一个 XmlValidatingReader 类,这个类存于 .NET Framework 2.0 之前的版本中,用来进行模式验证,现在已经不再使用。
(P431)
XSLT (Entensible Stylesheet Language Transformations ,扩展样式表转换语言) 是一种 XML 语言,它介绍了如何把一种 XML 语言转化为另一种。这种转化的典型就是把一个 (描述数据的) XML 文档转化为一个 (描述格式化文档的) XHTML 文档。
【第12章】
(P432)
有些对象要求显式地卸载代码来释放资源,如打开的文件、锁、执行中的系统句柄和非托管对象。在 .NET 的术语中,这叫做销毁 (Disposal) ,它由 IDisposable 接口来实现。
那些占用托管内存的未使用对象必须在某些时候被回收,这个功能被称为垃圾回收,它由 CLR 执行。
销毁不同于垃圾回收的是,销毁通常是显式调用,而垃圾回收则完全自动进行。换言之,程序员要关心释放文件句柄、锁和操作系统资源等,而 CLR 则关心释放内存。
C# 的 using 语句从语法上提供了对实现 IDisposable 接口的对象调用 Dispose 方法的捷径,它还使用了 try / finally 块。
(P433)
finally 语句块保证 Dispose 方法一定被调用,即使是抛出异常或代码提前离开这个语句块。
在简单的情况下,编写自定义的可销毁类型只需要实现 IDisposable 接口并编写 Dispose 方法。
在销毁的逻辑中,.NET Framework 遵循了一系列实际存在的规则。这些规则并不是硬编码在 .NET Framework 或 C# 语言中;它们的目的是为使用者定义一致的协议。它们是 :
1. 一旦被销毁,对象无法恢复。对象也不能重新被激活,调用它的方法或属性将抛出 ObjectDisposedException 异常;
2. 重复调用对象的 Dispose 方法不会产生异常;
3. 如果可销毁对象 x 包含或 “封装” 或 “占有” 可释放资源对象 y , x 的 Dispose 方法自动调用 y 的 Dispose 方法 —— 除非接收到其他指令;
除了 Dispose 方法,一些类还定义了 Close 方法。 .NET Framework 对 Close 方法的语义并不是完全一致,尽管几乎所有的情况都是下面的一种 :
1. 从功能上等同于 Dispose 方法;
2. 从功能上是 Dispose 方法的子集;
(P434)
一些类定义了 Stop 方法,它们可以像 Dispose 方法一样释放非托管资源,但不同于 Dispose 方法的是,它允许重新开始。
在 WinRT 中, Close 可以认为与 Dispose 相同。事实上,运行时会将 Close 方法映射到 Dispose 方法上,使它们的类型同样可以在 using 语句中使用。
包含非托管资源句柄的对象几乎总是要求销毁,目的是为了释放这些句柄。
如果一个类型是可销毁的,它经常 (而非总是) 直接或间接地引用非托管句柄。
有 3 种情况不能释放 :
1. 当通过静态字段或属性获得共享对象时;
2. 当对象的 Dispose 方法执行不需要的操作时;
3. 当对象的方法在设计时不是必须的,而且释放那个对象将增加程序的复杂性时;
(P436)
StreamWriter 必须公开另一个方法 (Flush 方法) 来保证使用者不调用 Dispose 方法也能执行必要的清理工作。
Dispose 方法本身并没有释放内存,只有垃圾回收时才释放内存。
无论对象是否要求使用 Dispose 方法来自定义清理逻辑,某些情况下在堆上被占用的内存必须被释放。 CLR 通过垃圾回收器完全自动地处理这方面工作。永远不能自动释放托管内存。
(P437)
垃圾回收并不是在对象没有引用之后立即执行。
垃圾回收器在每次回收时并没有回收所有的垃圾。相反的,内存管理器将对象分为不同的代,垃圾回收器收集新代 (最近分配的对象) 的垃圾比旧代 (长时间存活的对象) 的垃圾更频繁。
垃圾回收器试图在垃圾回收所花费的时间和应用程序内存使用 (工作区) 上保持平衡。因此,应用程序会使用比实际需要更多的内存,特别是构造大的临时数组。
根保持对象存活。如果对象没有直接或间接地由根引用,那么它将被垃圾回收器选中。
根有以下三种 :
1. 局部变量或执行方法中的参数 (或在调用它的栈的方法中);
2. 静态变量;
3. 准备运行终止器的对象;
(P438)
Windows Runtime 依靠 COM 的引用计数机制来释放内存,而非依靠自动化的垃圾回收器。
在对象从内存中被释放之前,它的终止器将运行 (如果它有终止器的话) 。终止器像构造方法一样声明,但是它有 ~ 符号作前缀。
虽然与构造函数的声明相似,但是析构器无法声明为 public 或 static ,不能有参数,而且不能调用基类。
(P439)
终止器很有用,但是它有一些附带条件 :
1. 终止器使分配和内存回收变得缓慢 (垃圾回收器将对执行的终止器保持追踪) ;
2. 终止器延长了对象和任意引用对象的生命周期 (它们必须等待下一次垃圾回收来实际删除) ;
3. 无法预测终止器以什么顺序调用一系列的对象;
4. 对对象的终止器何时被调用只有有限的控制;
5. 如果终止器的代码被阻碍,其他对象也不能被终结;
6. 如果应用程序没有被完全地卸载,终止器也许会被规避;
总之,终止器尽管在有些时候你确实需要它,通常你不想使用它,除非绝对必要。如果确实要使用它,需要 100% 确定理解它所做的一切。
实现终止器的准则 :
1. 保证终止器执行得很快;
2. 永远不要在终止器中中断;
3. 不要引用其他可终结对象;
4. 不要抛出异常;
终止器的一个很好的用途是当忘记对可销毁对象调用 Dispose 方法的时候提供一个备份;对象迟一点被销毁通常比没有被销毁好。
(P440)
无参数的版本没有被声明成虚方法 (virtual) ,它只是简单地用 true 作为参数调用的增强版本。
增强版本包含实际的销毁逻辑,它是受保护的 (protected) 和虚拟的 (virtual) ,这为子类添加它们自己的销毁逻辑提供了安全的方法。
请注意我们在没有参数的 Dispose 方法中调用了 GC.SuppressFinalize 方法,这防止当垃圾回收器在之后捕捉这个对象时终止器也同时运行的情况。从技术上讲这并不必要,因为 Dispose 方法能够接受重复调用。但是,这样可以提高效率,因为允许对象 (和它引用的对象) 在一个周期中被回收。
(P441)
复活对象的终止器不会第二次执行,除非调用 GC.ReRegisterForFinalize 方法。
(P442)
请注意在终止器方法中只调用一次 ReRegisterForFinalize 方法。如果调用了两次,对象将会被注册两次并且经历两次终结过程。
CLR 使用分代式 “标记-紧缩型” 垃圾回收器来执行存储在托管堆上对象的自动内存管理。垃圾回收器被认为是追踪型垃圾回收器,因为它不会干涉每次对对象的访问,而是立刻激活并追踪存储在托管堆上对象的记录,以此来决定哪些对象被认为是垃圾并被回收。
垃圾回收器通过执行内存分配 (通过 new 关键字) 开始一次垃圾回收,在内存分配或者某个内存起始点被分配之后,或者在其他减少应用程序内存的时候。这个过程也可以通过调用 System.GC.Collect 方法手动开始。在垃圾回收时,所有的线程也许都会被冻结。
垃圾回收器从根对象引用开始,按对象记录前进,标记它所有接触的对象为可到达的。一旦这个过程结束,所有没有被标记的对象被认为是无用的,将会被垃圾回收器回收。
没有终止器的无用对象将立刻被删除;有终止器的对象将在垃圾回收结束之后在终止器中排队进行处理。这些对象将在下一次对这代对象的垃圾回收过程中被选中回收 (除非复活) 。
然后将剩余的 “活动” 对象移到堆的开头 (紧缩) ,释放出更多的对象空间。这种压缩操作有两个目的 : 避免出现内存片段,允许垃圾回收器在分配新对象时始终在堆的末尾分配内存。这可避免为可能非常耗时的任务维护剩余内存片段的列表。
如果在垃圾回收之后没有足够的内存来分配新的对象,操作系统将无法分配更多的内存,这时将抛出 OutOfMemoryException 异常。
垃圾回收包含多种优化技术来减少垃圾回收的时间。
(P443)
最重要的优化是垃圾回收是分代的。
基本上讲,垃圾回收器将托管堆分为三代。刚刚被分配的对象在 Gen 0 里,在一轮回收幸存下来的对象在 Gen 1 里,其他所有对象都在 Gen 2 里。
CLR 将 Gen 0 部分保持在相对较小的空间内 (在 32 位工作站 CLR 上最大是 16MB ,典型的大小是几百 KB 到几 MB) 。当 Gen 0 部分被填满之后,垃圾回收器引发 Gen 0 的回收,这经常发生。垃圾回收器对 Gen 1 执行相似的内存限制 (Gen 1 扮演着 Gen 2 的缓存角色) ,因此 Gen 1 的回收也相对地快速和频繁。然后,包括 Gen 2 的完全回收花费更长的时间,发生得不那么频繁。
存活周期短的对象非常有效地被垃圾回收器使用。
(P444)
对大于某一限度 (当前是 85000 字节) 的对象,垃圾回收器使用特殊的堆即 “大对象堆” 。这避免了过多的 Gen 0 回收,分配一系列 16MB 的对象也许会在每次分配之后引起一次 Gen 0 的回收。
大对象堆并不是分代的 : 所有对象都按 Gen 2 来处理。
垃圾回收器在回收的时候必定会冻结 (阻止) 执行线程一段时间,这包括 Gen 0 和 Gen 1 回收发生的整个时间。
可以在任何时间通过调用 GC.Collect 方法强制垃圾回收。调用 GC.Collect 方法而没有参数将发起完全回收。如果传入一个整数值,只有整数值的那一代将被回收,因此 GC.Collect(0) 只执行一次快速的 Gen 0 回收。
(P445)
总的来说,通过允许垃圾回收器来决定何时回收来获得最好的性能。强制回收不必要地将 Gen 0 对象提升到 Gen 1 中,这将降低性能,也将影响垃圾回收器的自我调节能力,即垃圾回收器动态调整每一代回收的开始时间,以保证在应用程序执行的时候性能最大化。
(P446)
在 WPF 的主题中,数据绑定是另一个导致内存泄露的常见情况。
忘记计时器也能造成内存泄露。
(P447)
一个很好的准则是如果类中的任何字段被赋值给实现 IDisposable 接口的对象,类也应该实现 IDisposable 接口。
【第13章】
(P452)
可以使用预处理器指令有条件地编译 C# 中的任何代码段。预处理器指令是以 C# 符号开头特殊的编译器指令。不同于其他 C# 结构体的是,它必须出现在单独的一行。条件编译的预处理指令有 #if 、 #else 、 #endif 和 # elif 。
#if 指令表示编译器将忽略一段代码,除非定义了特定的符号。可以用 #define 指令或编译开关来定义一个符号。 #define 指令应用于特定的文件;编译开关应用于整个程序集。
# define 指令必须在文件顶端。
(P453)
#else 语句和 C# 的 else 语句很类似, #elif 等同于 #if 其后的 #else 。
|| 、 && 和 ! 运算符用于执行或、与和非运算。
要在程序集范围内定义符号,可在编译时指定 /define 开关。
Visual Studio 在 “项目属性” 中提供了输入条件编译符号的选项。
如果在程序集级别定义了符号,之后想在某些特定文件中取消定义,可使用 #undef 指令。
(P454)
[Conditional] 的另一个好处是条件性检测在调用方法被编译时执行,而不是在调用的方法被编译时。
Conditional 属性在运行时被忽略,因为它仅仅是给编译器的指令而已。
如果需要在运行时动态地启用或禁用某种功能, Conditional 属性将毫无用处,而是必须使用基于变量的方法。
(P455)
Debug 和 Trace 是提供基本日志和断言功能的静态类。这两个类很类似,主要的不同是它们的特定用途。 Debug 类用于调试版本; Trace 类用于调试和发布版本。
所有 Debug 类的方法都用 [Conditional("DEBUG")] 定义;
所有 Trace 类的方法都用 [Conditional("TRACE)] 定义;
这意味着所有调用标记为 DEBUG 或 TRACE 的方法都会被编译器忽略,除非定义了 DEBUG 或 TRACE 符号。默认情况下, Visual Studio 在项目的调试配置中定义了 DEBUG 和 TRACE 符号,同时只在发布配置中定义了 TRACE 符号。
Debug 和 Trace 类都提供了 Write 、 WriteLine 和 WriteIf 方法。默认情况下,这些方法向调试器的输出窗口发送消息。
Trace 类也提供了 TraceInformation 、 TraceWarning 和 TraceError 方法。这些方法和 Write 方法在行为上的不同取决于 TraceListeners 类。
Debug 和 Trace 类都提供了 Fail 和 Assert 方法。
Fail 方法给每一个在 Debug 或 Trace 类的 Listeners 集合中的 TraceListener 发送消息,默认在调试输出窗口和对话框中显示消息。
Assert 方法在布尔参数为 false 时仅仅调用 Fail 方法,这叫做使用断言。指定错误消息也是可选的。
Write 、 Fail 和 Assert 方法也被重载来接受字符串类型的额外信息,这在处理输出时很有用。
(P456)
Debug 和 Trace 类都有 Listeners 属性,包含了 TraceListener 实例的静态集合。它们负责处理由 Write 、 Fail 和 Trace 方法发起的内容。
(P457)
对于 Windows 事件日志,通过 Wirte 、 Fail 或 Assert 方法输出的消息在 Windows 事件查看器中总是显示为 “消息” 。但是,通过 TraceWarning 和 TraceError 方法输出的消息,则显示为 “警告” 或 “错误” 。
Trace 和 Debug 类提供了静态的 Close 和 Flush 方法来调用所有监听器的 Close 和 Flush 方法 (依次调用它所属的编写器和流的 Close 或 Flush 方法) 。 Close 方法隐式地调用 Flush 方法,关闭文件句柄,防止数据进一步被写入。
作为一般的规则,要在应用程序结束前调用 Close 方法,随时调用 Flush 方法来保证当前的消息数据被写入。这适用于使用流或基于文件的监听器。
(P458)
Trace 和 Debug 类也提供了 AutoFlush 属性,如果它为 true ,则在每条消息之后强制执行 Flush 方法。
如果使用任何文件或基于流的监听器,将 AutoFlush 设为 true 是很好的方法。否则,如果任何未处理的异常或关键的错误发生,最后 4KB 的诊断信息也许会丢失。
Framework 4.0 提供了叫做 “代码契约” 的新特性,用统一的系统代替了这些方法。这种系统不但支持简单的断言,也支持更加强大的基于契约的断言。
代码契约由 Eiffel 编程语言中的契约式设计原则而来,函数之间通过相互有义务和好处的系统进行交互。本质上讲,客户端 (调用方) 必须满足函数指定的先决条件和保证当函数返回时客户端能够依赖的后置条件。
代码契约的类型存在于 System.Diagnostics.Contracts 命名空间中。
先决条件由 Contract.Requires 定义,它在方法开始时被验证。后置条件由 Contract.Ensures 定义,它并不在它出现的地方被验证,而是当方法结束时被验证。
(P459)
先决条件和后置条件必须出现在方法的开始。优点是如果没有在按顺序编写的方法中实现契约,错误就会被检测出来。
代码契约的另一个限制是不能用它们来执行安全性检查,因为它们在运行时被规避 (通过处理 ContractFailed 事件) 。
代码契约由先决条件、后置条件、断言和对象不变式组成。这些都是可发现的断言。不同之处是它们何时被验证 :
1. 先决条件在函数开始时被验证;
2. 后置条件在函数结束之前被验证;
3. 断言在它出现的地方被验证;
4. 对象不变式在每个类中的公有函数之后被验证;
(P460)
代码契约完全通过调用 Contract 类中的 (静态) 方法来定义,这与契约语言无关。
契约不仅在方法中出现,也可以在其他函数中出现,例如构造方法、属性、索引器和运算符。
(P465)
无论重写的方法是否调用了基方法,二进制重写器能保证基方法的先决条件总是在子类中被执行。
(P467)
以下两个原因使 Contract.Assert 比 Debug.Assert 更受欢迎 :
1. 通过代码契约提供的失败处理机制能获得更多的灵活性;
2. 静态检测工具能尝试验证 Contract.Asserts ;
(P473)
DbgCLR 是 Visual Studio 中的调试器,和 .NET Framework SDK 一起免费下载,它是当没有 IDE 时最简单的调试选择,尽管必须下载整个 SDK 。
(P474)
Process.GetProcessXXX 方法通过名称或进程 ID 检索指定进程,或检索所有运行在当前或指定名称计算机中的进程,包括所有托管和非托管的进程。每一个 Process 实例都有很多属性映射到各种统计数据上,例如名称、 ID 、优先级、内存和处理器利用率、窗口句柄等。
Process.GetCurrentProcess 方法返回当前的进程。如果创建了额外的应用程序域,它们将共享同一个进程。
可以通过调用 Kill 方法来终止一个进程。
(P475)
也可以用 Process.Threads 属性遍历其他进程的所有线程。然而,获得的对象并不是 System.Threading.Thread 对象,而是 ProcessThread 对象,它用于管理而不是同步任务。
ProcessThread 对象提供了潜在线程的诊断信息,并允许控制它的一些属性,例如优先级和处理器亲和度。
(P476)
Exception 已经有 StackTrace 属性,但是这个属性返回的是简单的字符串而不是 StackTrace 对象。
如果注册了 EventLogTraceListener 类,之前使用的 Debug 和 Trace 类可以写入 Windows 事件日志。但是,可以使用 EventLog 类直接写入 Windows 事件日志而不使用 Trace 或 Debug 类。也可以使用这个类来读取和监视事件数据。
写入事件日志对 Windows 服务应用程序来说很有意义,因为如果出错了,不能弹出用户界面来提供给用户一些包含诊断信息的特殊文件。也因为 Windows 服务通常都写入 Windows 事件日志,如果服务出现问题, Windows 事件日志几乎是管理员首先要查看的地方。
(P477)
有三种标准的 Windows 事件日志,按名称分类 :
1. 应用程序;
2. 系统;
3. 安全;
应用程序日志是大多数应用程序通常写入的地方。
要写入 Windows 事件日志 :
1. 选择三种事件日志中的一种 (通常是应用程序日志) ;
2. 决定源名称,必要时创建;
3. 用日志名称、源名称和消息数据来调用 EventLog.WriteEntry 方法;
源名称使应用程序更容易分类。必须在使用它之前注册源名称,使用 CreateEventSource 方法可以实现这个功能,之后可以调用 WriteEntry 方法。
EventLogEntryType 可以是 Information 、 Warning 、 Error 、 SuccessAudit 或 FailureAudit 。
每一个在 Windows 事件查看器中都显示不同的图标。
CreateEventSource 也允许指定计算机名 : 这可以写入其他计算机的事件日志,如果有足够的权限。
要读取事件日志,用想访问的日志名来实例化 EventLog 类,并选择性地使用日志存在的其他计算机名。每一个日志项目能够通过 Entries 集合属性来读取。
(P478)
可以通过静态方法 EventLog.GetEventLogs 来遍历当前 (或其他) 计算机上的所有日志 (这需要管理员权限) 。通常这至少会打印应用程序日志、安全日志和系统日志。
通过 EntryWritten 事件,一条项目被写入到 Windows 事件日志时,将获得通知。对工作在本机的事件日志,无论什么应用程序记录日志都会被触发。
要开启日志监视 :
1. 实例化 EventLog 并设置它的 EnableRaisingEvents 属性为 true ;
2. 处理 EntryWritten 事件;
(P483)
Stopwatch 类提供了一种方便的机制来衡量执行时间。Stopwatch 使用了操作系统和硬件提供的最高分辨率机制,通常少于 1ms (对比一下, DateTime.Now 和 Environment.TickCount 有大约 15ms 的分辨率) 。
要使用 Stopwatch 调用 StartNew() 方法,它实例化 Stopwatch 对象并开始计时 (换句话说,可以手动实例化并在之后调用 Start 方法) 。 Elapsed 返回表示过去的时间间隔的 TimeSpan 对象。
Stopwatch 也公开了 ElapsedTicks 属性,它返回表示过去时间的 long 类型的数字。要将时间转换成秒,请除以 Stopwatch.Frequency 。 Stopwatch 也有 ElapsedMilliseconds 属性,这通常是最方便的。
调用 Stop 方法将终止 Elapsed 和 ElapsedTicks 。运行的 Stopwatch 并不会引起任何后台活动,因此调用 Stop 方法是可选的。
【第14章】
(P484)
程序并发执行代码的通用机制是多线程 (multithreading) 。 CLR 和操作系统都支持多线程,它是一种基础并发概念。因此,最基本的要求是理解线程的基本概念,特别是线程的共享状态。
(P485)
线程 (thread) 是一个独立处理的执行路径。
每一个线程都运行在一个操作系统进程中,这个进程是程序执行的独立环境。在单线程 (single-threaded) 程序中,在进程的独立环境中只有一个线程运行,所以该线程具有独立使用进程资源的权利。
在多线程 (multi-threaded) 程序中,在进程中有多个线程运行,它们共享同一个执行环境 (特别是内存) 。这在一定程度上反映了多线程处理的作用 : 例如,一个线程在后台获取数据,同时另一个线程显示所获得的数据,这些数据就是所谓的共享状态 (shared state) 。
Windows Metro 配置文件不允许直接创建和启动线程;相反,必须通过任务来操作线程。任务增加了间接创建线程的方法,这种方法增加了学习复杂性,所以最好从控制台应用程序开始,熟悉它们的使用方法,然后再直接创建线程。
客户端程序 (Console 、 WPF 、 Metro 或 Windows 窗体) 都从操作系统自动创建一个线程 (主线程) 开始。除非创建更多的线程 (直接或间接) ,否则这就是单线程应用程序的运行环境。
实例化一个 Thread 对象,然后调用它的 Start 方法,就可以创建和启动一个新的线程。
最简单的 Thread 构造方法接受一个 ThreadStart 代理 : 一个无参数方法,表示执行开始位置。
在单核计算机上,操作系统会给每一个线程分配一些 “时间片” (Windows 一般为 20 毫秒) ,用于模拟并发性,因此这段代码会出现连续的 x 和 y 。在 多核 / 多处理器 主机上执行时,虽然这个例子仍然会出现重复的 x 和 y (受控制台处理并发请求的机制影响) ,但是线程却能够真正实现并行执行 (分别由计算机上其他激活处理器完成) 。
(P486)
线程被认为是优先占用 (preempted) 它在执行过程与其他线程代码交叉执行的位置。这个术语通常可以解释出现的问题。
在线程启动之后,线程的 IsAlive 属性就会变成 true ,直到线程停止。当 Thread 的构造函数接收的代理执行完毕时,线程就会停止。在停止之后,线程无法再次启发。
每个线程都有一个 Name 属性,它用于调试程序。它在 Visual Studio 中特别有用,因为线程的名称会显示在 Threads 窗口和 Debug Location 工具栏上。线程名称只能设置一次;修改线程名称会抛出异常。
静态属性 Thread.CurrentThread 可以返回当前执行的线程。
在等待另一个线程结束时,可以调用另一个线程的 Join 方法。
Thread.Sleep 会将当前线程暂停执行一定的时间。
(P487)
调用 Thread.Sleep(0) ,会马上放弃线程的当前时间片,自动将 CPU 交给其他线程。
Thread.Yield() 方法也有相同的效果,但是它只会将资源交给在同一个处理器上运行的线程。
有时候,在生产代码中使用 Sleep(0) 或 Yield ,可以优化性能。它还是一种很好的诊断工具,可以帮助开发者发现线程安全问题 : 如果在代码任意位置插入 Thread.Yield() 会破坏程序,那么代码肯定存在 Bug 。
在等待线程 Sleep 或 Join 的过程中,还可以阻塞线程。
线程阻塞是指线程由于特定原因暂停执行,如 Sleeping 或执行 Join 后等待另一个线程停止。阻塞的线程会立刻交出 (yield) 它的处理器时间片,然后从这时开始不再消耗处理器时间,直至阻塞条件结束。使用线程的 ThreadState 属性,可以测试线程的阻塞状态。
ThreadState 是一个标记枚举量,它由三 “层” 二进制位数据组成。
ThreadState 属性可用于诊断程序,但是不适用于实现同步,因为线程状态可能在测试 ThreadState 和获取这个信息的时间段内发生变化。
当线程阻塞或未阻塞时,操作系统会执行环境切换 (context switch) 。这个操作会稍微增加负载,幅度一般在 1~2 毫秒左右。
如果一个操作将大部分时间用于等待一个条件的发生,那么它就称为 I / O 密集 (I / O - bound) 操作。
I / O 密集操作一般都会涉及输入或输出,但是这不是硬性要求 : Thread.Sleep 也是一种 I / O 密集操作。
如果一个操作将大部分时间用于执行 CPU 密集操作,那么它就称为计算密集 (compute-bound) 操作。
I / O 密集操作可以以两种方式执行 : 同步等待当前线程的操作完成 (如 Console.ReadLine 、Thread.Sleep 或 Thread.Join) ,或者异步执行,然后在将来操作完成时触发一个回调函数。
异步等待的 I / O 密集操作会将大部分时间花费在线程阻塞上。它们也可能在一个定期循环中自旋。
(P488)
自旋与阻塞有一些细微差别。首先,非常短暂的自旋可能非常适用于设置很快能满足的条件 (也许是几毫秒之内) ,因为它可以避免过载和环境切换延迟。
CLR 会给每一个线程分配独立的内存堆,从而保证本地变量的隔离。
如果线程拥有同一个对象实例的通用引用,那么这些线程就共享相同的数据。
(P489)
编译器会将 Lambda 表达式或匿名代理捕获的局部变量转换为域,所以它们也可以共享。
静态域是在线程之间共享数据的另一种方法。
(P490)
当两个线程同时争夺一个锁时 (它可以是任意引用类型的对象,这里是 _locker) ,其中一个线程会等待 (或阻塞) ,直到锁释放。这个例子保证一次只有一个线程能够进入它的代码块,因此 “Done” 只打印一次。在复杂的多线程环境中,采用这种方式来保护的代码就是具有线程安全性 (thread-safe) 。
锁并不是解决线程安全的万能法宝 —— 人们很容易在访问域时忘记锁,而且锁本身也存在一些问题 (如死锁) 。
(P491)
ParameterizedThreadStart 的局限性在于 : 它只接受一个参数。而且因为参数属于类型 object ,所以它通常需要进行强制转换。
Lambda 表达式是向线程传递数据的最方便且最强大的方法。
(P492)
在线程创建时任何生效的 try / catch / finally 语句块开始执行后都与线程无关。
(P493)
在运行环境中,应用程序的所有线程入口方法都需要添加一个异常处理方法 —— 就和主线程一样 (通常位于更高一级的执行堆栈中) 。
默认情况下,显示创建的线程都是前台线程 (foreground thread) 。无论是否还有后台线程 (background thread) 运行,只要有一个前台线程仍在运行,整个应用程序就会保持运行状态。当所有前台线程结束时,应用程序就会停止,而且所有仍在运行的后台线程也会随之中止。
线程的 前台 / 后台 状态与线程的优先级 (执行时间分配) 无关。
使用线程的 IsBackground 属性,可以查询或修改线程的后台状态。
(P494)
线程的 Priority 属性可以确定它与其他激活线程在操作系统中的相对执行时间长短。
如果同时激活多个线程,优先级就会变得很重要。提高一个线程的优先级时,要注意不要过度抢占其他线程的执行时间。如果希望一个线程拥有比其他进程的线程更高级的优先级,那么还必须使用 System.Diagnostics 的 Process 类,提高进程本身的优先级。
这种方法非常适合于一些工作量较少但要求较低延迟时间 (能够快速响应) 的 UI 进程中。在计算密集特别是带有用户界面的应用程序中,提高进程优先级可能会抢占其他进程的执行时间,从而影响整个计算机的运行速度。
有时候,一个线程需要等待来自其他线程的通知,这就是所谓的发送信号 (singaling) 。最简单的发送信号结构是 ManualResetEvent 。在一个 ManualResetEvent 上调用 WaitOne ,可以阻塞当前线程,使之一直等待另一个线程通过调用 Set “打开” 信号。
(P495)
在调用 Set 之后,信号仍然保持打开;调用 Reset ,就可以再次将它关闭。 ManualResetEvent 是 CLR 提供的多个信号发送结构之一。
(P496)
System.ComponentModel 命名空间中有一个抽象类 SynchronizationContext ,它实现了编程编列一般化。
WPF 、 Metro 和 Windows 窗体都定义和实例化了 SynchronizationContext 的子类,当运行在 UI 线程上时,它可以通过静态属性 SynchronizationContext.Current 获得。捕获这个属性,将来就可以在工作者线程上提交数据到 UI 控件。
(P497)
SynchronizationContext 还有一个专门用在 ASP.NET 的子类,它这时作为一个更微妙的角色,保证按照异步操作方式处理页面处理事件,并且保留 HttpContext 。
在 Dispatcher 或 Control 上调用 Post 与调用 BeginInvoke 的效果相同;另外 Send 方法与 Invoke 的效果相同。
Framework 2.0 引入了 BackgroundWorker 类,它使用 SynchronizationContext 类简化富客户端应用程序的工作者线程。BackgroundWorker 增加了相同的 Tasks 和异步功能,它也使用 SynchronizationContext 。
无论何时启动一个线程,都需要一定时间 (几百毫秒) 用于创建新的局部变量堆。线程池 (thread pool) 预先创建了一组可回收线程,因此可以缩短这段过载时间。要实现高效的并行编程和细致的并发性,必须使用线程池;它可用于运行一些短暂操作,而不会受到线程启动过载的影响。
在使用线程池中的线程 (池化线程) 时,还需要考虑下面这些问题 :
1. 由于不能设置池化线程的 Name ,因此会增加代码调试难度;
2. 池化线程通常都是后台线程;
3. 池化线程阻塞会影响性能;
池化线程的优先级可以随意修改 —— 在释放回线程池时,优先级会恢复为普通级别。
使用属性 Thread.CurrentThread.IsThreadPoolThread ,可以确定当前是否运行在一个池化线程上。
在池化线程上运行代码的最简单方法是使用 Task.Run 。
(P498)
由于 Framework 4.0 之前不支持任务,所以可以改为调用 ThreadPool.QueueUserWorkItem 。
(P498)
使用线程池的情况有 :
1. WCF 、 远程处理 (Remoting) 、 ASP.NET 和 ASMX Web Services 应用服务器;
2. System.Timers.Timer 和 System.Threading.Timer;
3. 并行编程结构;
4. BackgroundWorker 类 (现在是多余的) ;
5. 异步代理 (现在是多余的) ;
线程池还有另一个功能,即保证计算密集作业的临时过载不会引起 CPU 超负荷 (oversubscription) 。
超负荷是指激活的线程数量多于 CPU 内核数量,因此操作系统必须按时间片执行线程调度。超负荷会影响性能,因为划分时间片需要大量的上下文切换开销,并且可能使 CPU 缓存失效,而这是现代处理器实高性能的必要条件。
CLR 能够将任务进行排序,并且控制任务启动数量,从而避免线程池超负荷。它首先运行与硬件内核数量一样多的并发任务,然后通过爬山算法调整并发数量,在一个方向上不停调整工作负荷。如果吞吐量提升,那么它会在这个方向上继续调整 (否则切换到另一个方向) 。这样就保证能够发现最优性能曲线 —— 即使是计算机上同时发生的活动。
如果满足以下两个条件,则适合使用 CLR 的策略 :
1. 大多数工作项目的运行时间都非常短 (小于 250ms ,最理想情况是小于 100ms) ,这样 CLR 就有大量的机会可以测量和调整;
2. 线程池不会出现大量将大部分时间都浪费在阻塞上的作业;
阻塞是很麻烦的,因为它会让 CLR 错误地认为它占用了大量的 CPU 。 CLR 能够检测并补偿 (往池中注入更多的线程) ,但是这可能使线程池受到超负荷的影响。此外,这样也会增加延迟,因为 CLR 会限制注入新线程的速度,特别是应用程序生命周期的前期 (在客户端操作系统上更严重,因为它有严格的低资源消耗要求) 。
如果想要提高 CPU 的利用率,那么一定要保持线程池的整洁性。
线程是创建并发的底层工具,因此它具有一定的局限性。特别是 :
1. 虽然很容易向启动的线程传入数据,但是并没有简单的方法可以从联合 (Join) 线程得到 “返回值” 。因此,必须创建一些共享域。当操作抛出一个异常时,捕捉和处理异常也是非常麻烦的;
2. 当线程完成之后,无法再次启动该线程;相反,只能够联合 (Join) 它 (在进程中阻塞当前线程) 。
(P499)
这些局限性会影响并发性的实现;换而言之,不容易通过组合较小的并发操作实现较大的并发操作 (这对于异步编程而言非常重要) 。因此,这会增加对手工同步处理 (加锁、发送信号) 的依赖,而且很容易出现问题。
直接使用线程会对性能产生影响。而且,如果需要运行大量并发 I / O 密集操作,那么基于线程的方法仅仅在线程过载方面就会消耗大量的内存。
Task 类可以解决所有这些问题。与线程相比, Task 是一个更高级的抽象概念,它表示一个通过或不通过线程实现的并发操作。任务是可组合的 (compositional) —— 使用延续 (continuation) 将它们串联在一起。它们可以使用线程池减少启动延迟,而且它们可以通过 TaskCompletionSource 使用回调方法,避免多个线程同时等待 I / O 密集操作。
Task 类型是 Framework 4.0 引入的,作为并行编程库的组成部分。然后,它们后来 (通过使用等待者 awaiter) 进行了很多改进,从而在常见并发场景中发挥越来越大的作用,并且也是 C# 5.0 异步功能的基础类型。
从 Framework 4.5 开始,启动一个由后台线程实现的 Task ,最简单的方法是使用静态方法 Task.Run (Task 类似于 System.Threading.Tasks 命名空间) 。调用时只需要传入一个 Action 代理。
Task.Run 是 Framework 4.5 新引入的方法。在 Framework 4.0 中,调用 Task.Factory.StartNew ,可以实现相同的效果。前者相当于是后者的快捷方式。
Task 默认使用池化线程,它们都是后台线程。这意味着当主线程结束时,所有任务也会随之停止。因此,要在控制台应用程序中运行这些例子,必须在启动任务之后阻塞主线程。例如,挂起 (Waiting) 该任务,或者调用 Console.ReadLine 。
(P500)
Task.Run 会返回一个 Task 对象,它可用于监控任务执行过程,这一点与 Thread 对象不同。
注意这里没有调用 Start ,因为 Task.Run 创建的是 “热” 任务;相反,如果想要创建 “冷” 任务,则必须使用 Task 的构造函数,但是这种用法在实践中很少使用。
任务的 Status 属性可用于跟踪任务的执行状态。
调用任务的 Wait 方法,可以阻塞任务,直至任务完成,其效果等同于调用线程的 Join 。
可以在 Wait 中指定一个超时时间和一个取消令牌 (用于提前中止停止等待状态) 。
在默认情况下, CLR 会运行在池化线程上,这种线程非常适合执行短计算密集作业。如果要执行长阻塞操作,则可以按以下方式避免使用池化线程。
在池化线程上运行一个长任务问题并不大;但是如果要同时运行多个长任务 (特别是会阻塞的任务) ,则会对性能产生影响。在这种情况下,通常更好的方法是使用 TrackCreationOptions.LongRunning :
1. 如果是运行 I / O 密集任务,则可以使用 TaskCompletionSource 和异步操作 (asynchronous functions) ,通过回调函数 (延续) 实现并发性,而不通过线程实现;
2. 如果是运行计算密集任务,则可以使用一个 生产者 / 消费者 队列,控制这些任务的并发数量,避免出现线程和进程阻塞的问题;
Task 有一个泛型子类 Task
然后,查询 Result 属性,就可以获得结果。如果任务还没有完成,那么访问这个属性会阻塞当前线程,直至任务完成。
(P501)
Task
有趣的是,当 Task 和 Task
与线程不同,任务可以随时抛出异常。所以,如果任务中的代码抛出一个未处理异常 (换而言之,任务出错) , 那么这个异常会自动传递到调用 Wait() 的任务上或者访问 Task
使用的 Task 的 IsFaulted 和 IsCanceled 属性,就可以不重新抛出异常而检测出错的任务。如果这两个属性都返回 false ,则表示没有错误发生;如果 IsCanceld 为 true ,则任务抛出了 OperationCanceledOperation ;如果 IsFaulted 为 true , 则任务抛出了另一种异常,而 Exception 属性包含了该错误。
如果使用了自主的 “设置后忘记的” 任务 (不通过 Wait() 或 Result 控制的任务,或者实现相同效果的延续) ,那么最好在任务代码中显式声明异常处理,避免出现静默错误,就像线程的异常处理一样。
自主任务上的未处理异常称为未监控异常 (unobserved exception) ,在 CLR 4.0 中,它们实际上会中止程序 (当任务跳出运行范围并被垃圾回收器回收时, CLR 会在终结线程上重新抛出异常) 。这种方式有利于提醒一些悄悄发生的问题;然而,错误发生时间可能并不准确,因为垃圾回收器可能会明显滞后于发生问题的任务。因此,在发现这种行为具有复杂的不同步性模式时 , CLR 4.5 删除了这个特性。
如果异常仅仅表示无法获得一些不重要的结果,那么忽略异常是最好的处理方式。
如果异常反映了程序的重大缺陷,那么忽略异常是很有问题。这其中的原因有两个 :
1. 这个缺陷可能使程序处于无效状态;
2. 这个缺陷可能导致更多的异常发生,而且无法记录初始错误也会增加诊断难度;
使用静态事件 TaskScheduler.UnobservedTaskException ,可以在全局范围订阅未监控的异常;处理这个事件,然后记录发生的错误,是一个很好的异常处理方法。
未监控异常有一些有趣的细微差别 :
1. 如果在超时周期之后发生错误,那么等待超时的任务将生成一个未监控异常;
2. 在错误发生之后检查任务的 Exception 属性,会使异常变成 “已监控异常” ;
延续 (continuation) 会告诉任务在完成后继续执行下面的操作。延续通常由一个回调方法实现,它会在操作完成之后执行一次。给一个任务附加延续的方法有两种。第一种方法是 Framework 4.5 新增加的,它非常重要,因为 C# 5.0 的异步功能使用了这种方法。
调用 GetAwaiter 会返回一个等待者 (awaiter) 对象,它的方法会让先导 (antecedent) 任务 (primeNumberTask) 在完成 (或出错) 之后执行一个代理。已经完成的任务也可以附加一个延续,这时延续就马上执行。
等待者 (awaiter) 可以是任意对象,但是它必须包含前面所示两个方法 (OnCompleted 和 GetResult) 和一个 Boolean 类型属性 IsCompleted 的对象,它不需要实现包含所有这些成员的特定接口或继承特定基类 (但是 OnCompleted 属性接口 INotifyCompletion) 。
(P503)
如果先导任务出现错误,那么当延续代码调用 awaiter.GetResult() 时就会重新抛出异常。我们不需要调用 GetResult ,而是直接访问先导任务的 Result 属性。调用 GetResult 的好处是,当先导任务出现错误时,异常可以直接抛出,而不会封装在 AggregateException 之中,从而可以实现更简单且更清晰的异常捕捉代码。
对于非泛型任务,GetResult() 会返回空值 (void) ,然后它的实用函数会单独重新抛出异常。
如果出现同步上下文,那么会自动捕捉它,然后将延续提交到这个上下文中。这对于富客户端应用程序而言非常实用,因为会将延续弹回 UI 线程。然而,在编写库时,通常不采用这种方法,因为开销相对较大的 UI 线程只会在离开库时运行一次,而不会在方法调用期间运行。
如果不出现同步上下文或者使用 ConfigureAwait(false) ,那么通常延续会运行在先导任务所在的线程上,从而避免不必要的过载。
ContinueWith 本身会返回一个 Task ,它非常适用于添加更多的延续。然而,如果任务出现错误,我们必须直接处理 AggregateException ,然后编写额外代码,将延续编列到 UI 应用程序中。而在非 UI 下文中,如果想要让延续运行在同一个线程上,则必须指定 TaskContinuationOptions.ExecuteSynchronously ;否则它会弹回线程池。 ContinueWith 特别适用于并行编程场景。
TaskCompletionSource 可以创建一个任务,它不包含任何必须在后面启动和结束的操作。它的实现原理是提供一个可以手工操作的 “附属” 任务 —— 用于指定操作完成或出错的时间。这种方法非常适合于 I / O 密集作业 : 可以利用所有任务的优点 (它们能够生成返回值、异常和延续) ,但不会在操作执行期间阻塞线程。
TaskCompletionSource 用法很简单、直接初始化就可以。它包含一个 Task 属性,它返回一个可以等待和附加延续的任务 —— 和其他任务一样。然而,这个任务完全通过下面的方法由 TaskCompletionSource 对象进行控制。
(P504)
调用这些方法,就可以给任务发送信号,将任务修改为完成、异常或取消状态。这些方法都只能调用一次 : 如果多次调用 SetResult 、 SetException 或 SetCanceled ,它们就会抛出异常,而 Try * 等方法则会返回 false 。
TaskCompletionSource 的真正作用是创建一个不绑定线程的任务。
(P505)
Delay 方法非常实用,因此它成为 Task 类的一个静态方法。
Task.Delay 是 Thread.Sleep 的异步版本。
(P506)
同步操作 (synchronous operation) 在返回调用者之前才完成它的工作。
在大多数情况下,异步操作 (asynchronous operation) 则在返回调用者之后才完成它的工作。
异步方法使用频率较小,并且需要初始化并发编程,因为它的作业会继续与调用者并行处理。
异步方法一般会快速 (或立刻) 返回给调用者;因此,它们也称为非阻塞方法。
到目前为止,我们学习的异步方法都可以认为是通用方法 :
1. Thread.Start ;
2. Task.Run ;
3. 给任务附加延续的方法;
异步编程的原则是以异步方式编写运行时间很长 (或可能很长) 的函数。这与编写长运行时间函数的传统同步方法相反,它会在一个新线程或任务上调用这些函数,从而实现所需要的并发性。
异步方法的不同点是它会在长运行时间函数之中而非在函数之外初始化并发性。这样做有两个优点 :
1. I / O 密集并发性的实现不需要绑定线程,因此可以提高可伸缩性和效率;
2. 富客户端应用程序可以减少工作者线程的代码,因此可以简化线程的安全实现;
在传统的同步调用图中,如果图中出现一个运行时间很长的操作,我们就必须将整个调用图转移到一个工作者线程中,以保证 UI 的高速响应。因此,我们最终会得到一个跨越许多方法的并发操作 (过程级并发性) ,而且这时需要考虑图中每一个方法的线程安全性。
使用异步调用图,就可以在真正需要时才启动线程,因此可以降低调用图中线程的使用频率 (或者在特定操作中完全不需要使用线程,如 I / O 密集操作) 。其他方法则可以在 UI 线程上运行,从而可以大大简化线程安全性的实现。
(P507)
Metro 和 Silverlight .NET 鼓励使用异步编程,甚至一些运行时间较长的方法完全不会出现同步执行版本。相反,它们使用一些可以返回任务 (或者可以通过扩展方法转换为任务的对象) 的异步方法。
任务非常适合异步编程,因为它们支持异步编程所需要的延续。编写 Delay 时使用了 TaskCompletionSource ,它是一种实现 “底层” I / O 密集异步方法的标准方法。
(P509)
如果不想增加程序复杂性,那么必须使用 async 和 await 关键字实现异步性。
(P510)
C# 5.0 引入了 async 和 await 关键字。这两个关键字可用于编写异步代码,它具有与同步代码相当的结构和简单性,并且摒弃了异步编程的复杂结构。
为了完成编译,我们必须在包含的方法上添加 async 修饰符。
(P511)
修饰符 async 会指示编译器将 await 视为一个关键字,而非在方法中随意添加的修饰符 (这样可以保证 C# 5.0 之前编写并使用 await 作为修饰符的代码不会出现编译错误) 。
async 修饰符只能应用到返回 void 、 Task 或 Task
添加 async 修饰符的方法就是所谓的异步函数,因为它们通常本身也是异步的。
await 表达式的最大特点在于它们可以出现在代码的任意位置。具体地, await 表达式可以出现在异步方法中除 catch 或 finally 语句块、 lock 表达式、 unsafe 上下文或可执行入口 (Main 方法) 之外的任意位置。
(P513)
直接并发的代码要避免访问共享状态或 UI 控件。
(P514)
在 C# 5.0 之前,异步编程很难实现,原因不仅仅在缺少语言支持,还因为 .NET 框架是通过 EAP 和 APM 等模式实现异步功能,而非通过任务返回方法。
(P515)
在调用图上层启动工作者线程是很冒险的做法。
如果使用异步函数,则可以将返回类型 void 修改为 Task ,使方法本身适合采用异步实现 (即可等待的) ,其他方面都不需要修改。
注意,方法体内不需要显式返回一个任务。编译器会负责生成任务,它会在方法完成或者出现未处理异常时发出信号。这样就很容易创建异步调用链。
编译器会扩展异步函数,它会将任务返回给使用 TaskCompletionSource 的代码,用于创建任务,然后再发送信号或异常中止。
(P516)
当一个返回任务的异步方法结束时,执行过程会返回等待它的程序 (通过一个延续) 。
如果方法体返回 TResult ,则可以返回一个 Task
(P517)
使用 C# 异步函数进行程序设计的基本原则 :
1. 以同步方式编写方法;
2. 使用异步方法调用替换同步方法,然后等待它们;
3. 除了 “最顶级的” 方法 (一般是 UI 控件的事件处理器) ,将异步方法的返回类型修改为 Task 或 Task
编译能够为异步函数创建任务,意味着在很大程度上,我们只需要在创建 I / O 密集并发性的底层方法中显式创建一个 TaskCompletionSource 实例。 (而对于创建计算密集并发性的方法,则可以使用 Task.Run 创建函数) 。
(P519)
只要添加 async 关键字、 未命名 (unnamed) 方法 (lambda 表达式和匿名方法) 也一样可以采用异步方式执行。
在 WinRT 中,与 Task 等价的是 IAsyncAction ,而与 Task
这两个类都可以通过 System.Runtime.WindowsRuntime.dll 程序集的 AsTask 扩展方法转换为 Task 或 Task
tion
(P520)
由于 COM 类型系统的限制, IAsyncOperation
AsTask 方法也有重载方法,可以接受一个取消令牌和一个对象 IProgress
AsyncVoidMethodBuilder 会捕捉未处理异常 (在无返回值的异步函数中) ,然后将它们提交到同步上下文中 (如果有) ,以保证触发全局异常处理事件。
(P521)
注意,在 await 之前或之后抛出异常并没有任何区别。
(P526)
Framework 4.5 提供了大量返回任务的异步方法,它们都可用于代替 await (主要与 I / O 相关) 。很多方法 (至少有一部分) 采用了一种基于任务的异步模式 (Task-based Asynchronous Pattern , TAP) ,这是到目前为止最合理的形式。一个 TAP 方法必须 :
1. 返回一个 “热” (正在运行的) Task 或 Task
2. 拥有 “Async” 后缀 (除了一些特殊情况) ;
3. 如果支持取消 和 / 或 进度报告,重载后可接受取消令牌 和 / 或 IProgress
4. 快速返回调用者 (具有一小段初始同步阶段) ;
5. 在 I / O 密集操作中不占线程 ;
TAP 方法很容易通过 C# 异步函数实现。
(P527)
使用统一协议调用异步函数 (它们都一致返回任务) 的一个优点是,可以使用和编写任务组合器 (Task Combinator) —— 一些适用于组合各种用途的任务的函数。
CLR 包含两个任务组合器 : Task.WhenAny 和 Task.WhenAll 。
Task.WhenAny 返回这样一个任务 : 当任务组中任意一个任务完成,它也就完成。
(P528)
Task.WhenAll 返回这样一个任务 : 当传入的所有任务都完成时,它才完成。
(P530)
最老的模式是 APM (Asynchronous Programming Model) ,它使用一对以 “Begin” 和 “End” 开头的方法,以及一个接口 IAsyncResult 。
基于事件的异步模式 (Event-based Asynchronous Pattern, EAP) 在 Framework 2.0 时引入,它是代替 APM 的更简单方法,特别是在 UI 场景中。然而,他只能通过有限的类型实现。
EAP 只是一个模式,它并没有任何辅助类型。
实现 EAP 需要编写大量的模板代码,因此这个模式的代码相当复杂。
(P532)
位于 System.ComponentModel 的 BackgroundWorker 是 EAP 的通用实现。它允许富客户端应用启动一个工作者线程,然后执行完成和报告百分比进度,而不需要显式捕捉同步上下文。
RunWorkerAsync 启动操作,然后触发一个池化工作者线程的 DoWork 事件。它还会捕捉同步上下文,而且当操作完成或出错时, RunWorkerCompleted 事件就会通过同步上下文触发 (像延续一样) 。
BackgroundWorker 可以创建过程级并发性,其中 DoWork 事件完全运行在工作者线程上。如果需要在该事件处理器上更新 UI 控件 (而非提交完成百分比进度) ,则必须使用 Dispatcher.BeginInvoke 或类似的方法。
【第15章】
(P533)
System.IO 命名空间中的类型,即底层 I / O 功能的基础。
.NET Framework 也支持一些更高级的 I / O 功能,形式包括 SQL 连接和命令 、 LINQ to SQL 和 LINQ to XML 、 WCF 、 Web Services 和 Remoting 。
.NET 流体系结构主要包括以下概念 : 后备存储流、装饰器流和流适配器。
后备存储是支持输入和输出的终端,例如文件或网络连接。准确地说,它可以是下面的一种或两种 :
1. 支持顺序读取字节的源;
2. 支持顺序写入字节的目标;
但是,除非对程序员公开,否则后备存储是无用的。
Stream 正是实现这个功能的标准 .NET 类;它支持标准的读、写和寻址方法。与数组不同,流不是直接将所有数据保存到内存中,而是按序列方式处理数据 —— 一次一个字节或一个可管理大小的块。因此,无论后备存储的大小如何,流都只占用很少的内存。
流分成两类 :
后备存储流 —— 它们是与特定类型后备存储硬连接的, 例如 FileStream 或 NetworkStream ;
装饰器流 —— 它们使用另一种流,以某种方式转换数据,例如 DeflateStream 或 CryptoStream ;
(P534)
装饰器流具有以下体系结构优势 :
1. 它们能够释放用于实现自我压缩和加密的后备存储流;
2. 在装饰后,流不受接口变化的影响;
3. 装饰器支持实时连接;
4. 装饰器支持相互连接 (例如,压缩器后紧跟一个加密器) ;
后备存储流和装饰器流都只支持字节。虽然这种方式既灵活又高效,但是应用程序通常采用更高级的方式,例如文本或 XML 。通过在一个类中创建专门支持特定格式的类型化方法,并在这个类中封装一个流,适配器弥补了这个缺陷。
适配器会封装一个流,这与装饰器类似。然而,与装饰器不同的是,适配器本身不是一个流;它一般会完全隐藏面向字节的方法。
总之,后备存储流负责处理原始数据;装饰器流支持透明的二进制转换。
适配器支持一些处理更高级类型的类型化方法。
为了构成一个关系链,我们只需要将一个对象传递给另一个对象的构造函数。
抽象的 Stream 类是所有流的基类。它定义了三种基础操作的方法和属性 : 读取、写入和查找;以及一些管理任务,例如关闭、清除和配置超时。
(P536)
要实现异步读或写,只需要调用 ReadAsync / WriteAsync ,替代 Read / Write ,然后等待表达式。
使用异步方法,不需要捆绑线程就可以轻松编写适应慢速流 (可能是网络流) 的响应式和可扩展应用。
一个流可能支持只读、只写、读写。如果 CanWrite 返回 false ,那么流就是只读的;如果 CanRead 返回 false ,那么流就是只写的。
Read 可以将流中的一个数据块读取到数组中。它返回接收到的一些字节,字节数一定小于或等于 count 参数。如果它小于 count ,那么表示已经到达流的结尾,或者流是以小块方式提供数据的 (通常是网络流) 。无论是哪一种情况,数组的剩余字节都是不可写的,它们之前的值都是保留的。
(P537)
ReadByte 方法简单一些 : 它每次只读取一个字节,在流结束时返回 -1 。ReadByte 实际上返回的是一个 int ,而不是 byte ,因为后者不能为 -1 。
Write 和 WriteByte 方法都支持将数据发送到流中。当它们无法发送指定的字节时,就会抛出一个异常。
在 Read 和 Write 方法中,参数 offset 指的是缓冲数组中开始读或写的索引位置,而不是流中的位置。
如果 CanSeek 返回 true ,那么表示流是可查找的。在一个可查找的流中 (例如文件流) ,我们可以通过调用 SetLength 查询或修改它的 Length ,也可以随时修改正在读写的 Position 。 Position 属性是与流的开始位置相关的;然而,Seek 方法则支持移动到流的当前位置或结束位置。
修改 FileStream 的 Position 属性一般需要几毫秒时间。如果要在循环中执行几百万次位置修改,那么 Framework 4.0 中新的 MemoryMappedFile 类可能比 FileStream 更适合。
如果流不支持查找 (例如加密流) ,那么确定其长度的唯一方法是遍历整个流。而且,如果需要重新读取之前的位置,必须先关闭这个流,然后再重新从头开始读取。
流在使用完毕之后必须清理,以释放底层资源,例如文件和套接字句柄。一个保证关闭的简单方法是在块中初始化流。通常,流采用以下标准的清理语法 :
1. Dispose 和 Close 的功能相同;
2. 重复清除或关闭流不会产生错误;
(P538)
关闭一个装饰流会同时关闭装饰器及其后备存储流。在装饰器系列中,关闭最外层的装饰器 (系列的头部) 会关闭整个系列。
有一些流 (例如文件流) 会将数据缓冲到后备存储并从中取回数据,减少回程,从而提升性能。这意味着写入到流中的数据不会直接存储到后备存储器;而是等到缓冲区填满时再写入。Flush 方法可以强制将所有内部缓冲的数据写入。当流关闭时,Flush 会自动被调用。
如果 CanTimeout 返回 true ,那么流支持读写超时设定。网络流支持超时设定;文件流和内存流则不支持。对于支持超时设定的流,ReadTimeout 和 WriteTimeout 属性可用来确定以毫秒为单位的预期超时时间,其中 0 表示不设定超时。 Read 和 Write 方法会在超时发生时抛出一个异常。
通过 Stream 的静态 Null 域,能够获得一个 “空流” 。
(P539)
FileStream 不适用于 Metro 应用。相反,要转而使用 Windows.Storage 的 Windows Runtime 类型。
实例化 FileStream 的最简单方法是使用 File 类的以下静态方法之一 :
1. File.OpenRead() // 只读;
2. File.OpenWrite() //只写;
3. File.Create() // 读-写;
如果文件已经存在,那么 OpenWrite 和 Create 的行为是不同的。 Create 会截去全部已有的内容; OpenWrite 则会原封不动地保留流中从位置 0 开始的已有内容。如果写入的字节小于文件已有字节,那么 OpenWrite 所产生的流会同时保存新旧内容。
我们还可以直接实例化一个 FileStream 。它的构造函数支持所有特性,允许指定文件名或底层文件句柄、文件创建和访问模式、共享选项、缓冲和安全性。
下面的静态方法能够一次性将整个文件读取到内存中 :
1. File.ReadAllText (返回一个字符串);
2. File.ReadAllLines (返回一个字符串数组);
3. File.ReadAllBytes (返回一个字节数组);
下面的静态方法能够一次性写入一个完整的文件 :
1. File.WriteAllText ;
2. File.WriteAllLines ;
3. File.WriteAllBytes ;
4. File.AppendAllText (适用于给日志文件附加内容) ;
从 Framework 4.0 开始,增加了一个静态方法 File.ReadLines 。这个方法与 ReadAllLines 类似,唯一不同的是它返回一个延后判断的 IEnumerable
(P540)
文件名可以是绝对路径,也可以是当前目录的相对路径。我们可以通过静态的 Environment.CurrentDirectory 属性来访问或修改当前目录。
当程序启动时,当前目录不一定是程序执行文件所在的路径。因此,一定不要使用当前目录来定位与可执行文件一起打包的额外运行时文件。
AppDomain.CurrentDomain.BaseDirectory 会返回应用程序根目录,正常情况下它就是程序可执行文件所在的文件夹。使用 Path.Combine 可以指定相对于这个目录的文件名。
我们还可以通过 UNC 路径读写一个网络文件。
FileStream 的所有构造函数接受文件名需要一个 FileMode 枚举参数。
用 File.Create 和 FileMode.Create 处理隐藏文件会抛出一个异常。必须先删除隐藏文件再重新创建。
只使用文件名和 FieMode 创建一个 FileStream 会得到 (只有一种异常) 一个可读写的流。如果传入一个 FileAccess 参数,就可以要求降低读写模式。
(P541)
FileMode.Append 是最奇怪的一个方法 : 使用这个模式会得到只写流。相反,要附加读写支持,我们使用 FileMode.Open 或 FileMode.OpenOrCreate ,然后再查找流的结尾。
创建 FileStream 时可以选择的其他参数 :
1. 一个 FileShare 枚举值,描述了在完成文件处理之前,可以给同一个文件的其他进程授予的访问权限 (None 、 Read[default] 、 ReadWrite 或者 Write) ;
2. 以字节为单位的内部缓冲区大小 (当前的默认值是 4KB) ;
3. 一个标记,表示是否由操作系统管理异步 I / O ;
4. 一个 FileSecurity 对象,描述给新文件分配什么用户和角色权限;
5. 一个 FileOptions 标记枚举值,包括请求操作系统加密 (Encrypted) 、在临时文件关闭时自动删除 (DeleteOnClose) 和优化提示 (RandomAccess 和 SequentialScan) 。此外,还有一个 WriteThrough 标记要求操作系统禁用写后缓存,适用于事务文件或日志。
使用 FileShare.ReadWrite 打开一个文件允许其他进程或用户同时读写同一个文件。为了避免混乱,我们可以使用以下方法在读或写文件之前锁定文件的特定部分 :
public virtual void Lock(long position, long length);
public virtual voio UnLock(long position, long length);
如果所请求的文件段的部分或全部已经被锁定,那么 Lock 会抛一个异常。
MemoryStream 使用一个数组作为后备存储。这在一定程度是与使用流的目的相违背的,因为整个后备存储都必须一次性驻留在内存中。然而, MemoryStream 仍然有一定的用途,一个示例是随机访问一个不可查找的流。
(P542)
调用 ToArray 可以将一个 MemoryStream 转换为一个字节数组。GetBuffer 方法也可以实现相同操作,而且效率更高,它将返回一个底层存储数组的直接引用。但是,这个数组通常会比流的实际长度长一些。
MemoryStream 的关闭和清除不是必需的。如果关闭了一个 MemoryStream ,我们就无法再读或写这个流,但是我们仍然可以调用 ToArray 来获得底层数据。消除实际上不会对内存流执行任何操作。
PipeStream 是在 Framework 3.5 引入的。它支持一种简单的方法,其中一个进程可以通过 Windows 管道协议与另一个进程进行通信。
管道的类型有两种 :
1. 匿名管道 —— 支持在同一个 computer.id 的父子进程之间单向通信;
2. 命名管道 —— 支持同一台计算机或 Windows 网络中不同计算机的任意进程之间进行通信;
管道很适合用于在同一台计算机上进行进程间通信 (IPC) : 它不依赖于任何网络传输,性能更高,也不会有防火墙问题。
管道是基于流实现的,所以一个进程会等待接收字节,而另一个进程则负责发送字节。另一种进程通信方法可以通过共享内存块实现。
PipeStream 是一个抽象类,它有 4 个实现子类。其中两个支持匿名管道和两个支持命名管道 :
1. 匿名管道 —— AnonymousPipeServerStream 和 AnonymousPipeClientStream ;
2. 命名管道 —— NamedPipeServerStream 和 NamedPipeClientStream ;
命名管道使用更简单。
(P543)
管道是一个底层概念,它支持发送和接收字节 (或消息,即字节组) 。
WCF 和 Remoting API 支持使用 IPC 通道进行通信的更高级消息框架。
通过命名管道,各方将使用一个同名管道进行通信。这个协议定义了两个不同的角色 : 客户端和服务器。客户端与服务器之间的通信采用以下方式 :
1. 服务器初始化一个 NamedPipeServerStream , 然后调用 WaitForConnection ;
2. 客户端初始化一个 NamedPipeClientStream , 然后调用 Connect (使用一个可选的超时时间) ;
命名管道流默认是双向通信的,所以任何一方都可以读或写它们的流。这意味着客户端和服务器都必须同意使用一种协议来协调它们的操作,所以双方是不能同时发送或接收消息的。
通信双方还需要统一每次传输的数据长度。
为了支持传输更长的消息,管道提供了一种消息传输模式。如果启用这个模式,调用 Read 的一方可以通过检查 IsMessageComplete 属性来确定消息是否完成传输。
(P544)
只需要等待 Read 返回 0 ,我们就可以确定一个 PipeStream 是否完成消息的读取。这是因为,与其他大多数流不同,管道流和网络流并没有确定的结尾。相反,它们会在消息传输期间临时中断。
匿名管道支持在父子进程之间进行单向通信。匿名管道不使用系统级名称,而是通过一个私有句柄进行调整。
与命名管道一样,匿名管道也区分客户端和服务器角色。然而,通信系统有一些不同,它采用以下方法 :
1. 服务器初始化一个 AnonymousPipeServerStream ,提交一个 In 或 Out 的 PipeDirection ;
2. 服务器调用 GetClientHandleAsString 获取管道的标识,然后传递回客户端 (一般作为启动子进程的一个参数) ;
3. 子进程初始化一个 AnonymousPipeClientStream ,指定相反的 PipeDirection ;
4. 服务器调用 DisposeLocalCopyOfClientHandle ,释放第 2 步产生的本地句柄;
5. 父子进程通过 读 / 写 流来进行通信;
因为匿名管道是单向的,所以服务器必须为双向通信创建两个管道。
(P545)
与命名管道一样,客户端和服务器必须协调它们的发送和接收,并且统一每一次传输的数据长度。但是,匿名管道不支持消息模式,所以必须实现自己的消息长度认同协议。一种方法是在每次传输的前 4 个字节中发送一个整数值,定义后续消息的长度。
BitConverter 类具有一些用于转换整数和 4 字节数组的方法。
BufferedStream 可以装饰或包装另一个具有缓冲功能的流,它是 .NET Framework 的诸多核心装饰流类型之一。
(P546)
缓冲能够减少后备存储的方法,从而提高性能。
组合使用 BufferedStream 和 FileStream 的好处并不明显,因为 FileStream 已经有内置的缓冲了。它的唯一用途可能就是扩大一个已有 FileStream 的缓冲区。
关闭一个 BufferedStream 会自动关闭底层的后备存储流。
Stream 只支持字节处理;要读写一些数据类型,例如字符串、整数或 XML 元素,我们必须插入适配器。下面是 Framework 支持的适配器 :
1. 文本适配器 (处理字符串和字符数据) —— TextReader 、 TextWriter 、 StreamReader 、 StreamWriter 、 StringReader 、 StreamWriter ;
2. 二进制适配器 (处理基本数据类型,例如 int 、 bool 、 string 和 float) —— BinaryReader 、 BinaryWriter ;
3. XML 适配器 —— XmlReader 、 XmlWriter ;
(P547)
TextReader 和 TextWriter 都是专门处理字符和字符串的适配器的抽象基类。它们在框架中都是两个通用的实现 :
1. StreamReader / StreamWriter —— 使用 Stream 存储它的原始数据,将流的字节转换成字符或字符串;
2. StringReader / StringWriter —— 使用内存字符串实现 TextReader / TextWriter ;
不需要将位置前移,Peek 就可以返回流中的下一个字符。
如果到达流的末尾,那么 Peek 与不带参数的 Read 都会返回 -1 ;否则,它们会返回一个能够强制转换为 char 的整数。
接收一个char[] 缓冲区参数的 Read 重载函数功能与 ReadBlock 方法相似。
Windows 的新换行字符是模仿机械打字机的 : 回车符后面加上一个换行符。 C# 字符串表示是 “\r\n” 。如果顺序调换,结果可能是两行,也可能一行也没有。
WriteLine 会给指定文本附加 CR + LF 。我们可以使用 NewLine 属性修改这些字符,这对于支持 UNIX 文件格式的互操作性很有用。
和 Stream 一样,TextReader 和 TextWriter 为它们的 读 / 写 方法提供了基于任务的异步版本。
因为文本适配器通常与文件有关,所以 File 类也有一些静态方法支持快捷处理,例如 CreateText 、 AppendText 和 OpenText 。
(P549)
TextReader 和 TextWriter 本身是与流或后备存储无关的抽象类。然而,类型 StreamReader 和 StreamWriter 都与底层的字节流相关,所以它们必须进行字符和字节之间的转换。它们是通过 System.Text 命名空间的 Encoding 类进行这些操作的,创建 StreamReader 或 StreamWriter 需要选择一种编码方式。如果不进行选择,那么就使用默认的 UTF-8 编码。
如果明确指定一个编码方式,默认情况下 StreamWriter 会在流开头写入一个前缀,用于指定编码方式。这通常不是一种好做法。
最简单的编码方式是 ASCII ,因为每一个字符都是用一个字节表示的。
ASCII 编码将 Unicode 字符集的前 127 个字符映射为一个字节,其中包括键盘上的所有字符。
默认的 UTF-8 编码方式也能够映射所有分配的 Unicode 字符,但是更复杂一些。它将前 127 个字符编码为一个字节,以兼容 ASCII ;其他字符则编码为一定数量的字节 (通常是两个或三个) 。
UTF-8 在处理西方字母时很高效,因为最常用的字符只需 1 个字节。只需要忽略 127 之后的字节,它就能够轻松向下兼容 ACSII 。缺点是在流中查找是很麻烦的,因为字符的位置与它在流中的字节位置是无关的。
另一种方式是 UTF-16 (在 Encoding 类中仅仅标记为 “Unicode”) 。
技术上, UTF-16 使用 2 个或 4 个字节来表示一个字符 (所分配或保护的 Unicode 字符接近一百万个,所以 2 个字节并不总是足够的) 。然而,因为 C# 的 char 类型本身只有 16 位,所以 UTF-16 编码方式总是使用 2 个字节来表示一个 .NET 的 char 类型。这样就能够很容易转到流中特定的字符索引。
UTF-16 使用 2 个字节前缀来确定字节对采用 “小字节序” 还是 “大字节序” (最低有效字节在前还是最高有效字节在前) 。 Windows 系统采用的默认标准是小字节序。
(P551)
StringReader 和 StringWriter 适配器并不封装流;相反,它们使用一个字符串或 StringBuilder 作为底层数据源。这意味着不需要进行任何的字节转换,事实上,这些类所执行的操作都可以通过字符串或 StringBuilder 与一个索引变量轻松实现。并且它们的优点是与 StreamReader / StreamWriter 使用相同的基类。
BinaryReader 和 BinaryWriter 能够读写基本的数据类型 : bool 、 byte 、 char 、 decimal 、 float 、 double 、 short 、 int 、 long 、 sbyte 、 unshort 、 uint 和 ulong 以及字符串和数组等。
与 StreamReader 和 StreamWriter 不同的是,二进制适配器能够高效地存储基本数据类型,因为它们位于内存中。所以,一个 int 占用 4 个字节;一个 double 占用 8 个字节。字符串是通过文本编码 (与 StreamReader 和 STreamWriter 一样) 写入的,但是带有长度前缀,从而不需要特殊分隔符就能够读取一系列字符串。
(P552)
BinaryReader 也支持读入字节数组。
清理流适配器有 4 种方法 :
1. 只关闭适配器;
2. 先关闭适配器,然后再关闭流;
3. (对于编写器) 先清理适配器,然后再关闭流;
4. (对于读取器) 直接关闭流;
对于适配器和流, Close (关闭) 和 Dispose (清理) 是同义词。
关闭一个适配器会自动关闭底层的流。
因为嵌入语句是从内向外清理的,所以适配器先关闭,然后再关闭流。
一定不要在关闭和清理编写器之前关闭一个流,这样会丢失仍在适配器中缓存的所有数据。
(P553)
我们要调用 Flush 来保证将 StreamWriter 的缓冲区数据写入到底层的流中。
流适配器及其可选的清理语法并没有实现扩展的清理模式,即在终结器中调用 Dispose 。这可以避免垃圾回收器找到弃用的适配器时自动清理这个适配器。
从 Framework 4.5 开始, StreamReader / StreamWriter 有一个新的构造方法,它可以让流在清理之后仍然保持打开。
System.IO.Compression 命名空间提供了两个通用压缩流 : DeflateStream 和 GZipStream 。这两个类都使用与 ZIP 格式类似的流行压缩算法。它们的区别是 : GZipStream 会在开头和结尾写入一个额外的协议 —— 其中包括检测错误的 CRC 。 GZipStream 还遵循一个其他软件可识别的标准。
这两种流都支持读写操作,但是有以下限制条件 :
1. 压缩时总是在写入流;
2. 解压缩时总是在读取流;
DeflateStream 和 GZipStream 都是装饰器;它们负责压缩或解压缩构造方法传入的另一个流。
非重复性二进制文件数据的压缩效果很差 (缺少设计规范性的加密数据的压缩比是最差的), 这种压缩适用于大多数文本文件。
在 DeflateStream 构造方法传入的额外标记,表示在清除底层流时不采用普通的协议。
(P555)
Framework 4.5 引入了一个新特性 : 支持流行的 Zip 文件压缩格式,实现方法是 System.IO.Compression 中 (位于 System.IO.Compression.dll) 新增加的 ZipArchive 和 ZipFile 类。与 DeflateStream 和 GZipStream 相比,这种格式的优点是可以处理多个文件,并且兼容 Windows 资源管理器及其他压缩工具创建的 Zip 文件。
ZipArchive 可以操作流,而 ZipFile 则负责操作更常见的文件。 (ZipFile 是 ZipArchive 的静态帮助类) 。
ZipFile 的 CreateFromDirectory 方法可以将指定目录的所有文件添加到一个 Zip 文件中。
而 ExtractToDirectory 则执行相反操作,可以将一个 Zip 文件解压缩到一个目录中。
在压缩时,可以指定是否优化文件大小或压缩速度,以及是否在存档文件中包含源文件目录名称。
ZipFile 包含一个 Open 方法,它可以 读 / 写 各个文件项目。这个方法会返回一个 ZipArchive 对象 (也可以通过使用一个 Stream 对象创建 ZipArchive 实例而获得) 。当调用 Open 时,必须指定一个文件名,并且指定存档文件操作方式 : Read 、 Create 或 Update 。然后,使用 Entries 属性遍历现有的项目,或者使用 GetEntry 查询某个文件。
ZipArchiveEntry 还有 Delete 方法, ExtractToFile 方法 (实际是 ZipFileExtensions 类的扩展方法) 和 Open 方法 (返回一个 可读 / 可写 的 Stream) 。调用 ZipArchive 的 CreateEntry 或者 CreateEntryFromFile 扩展方法,可以创建新项目。
使用 MemoryStream 创建 ZipArchive ,也可以在内存中实现相同效果。
System.IO 命名空间有一些执行 “实用的” 文件与目录操作的类型。对于大多数特性,我们可以选择两种类型 : 一种采用静态方法,另一种采用实例方法 :
1. 静态类 —— File 和 Directory ;
2. 实例方法类 (使用文件或目录名创建) —— FileInfo 和 DirectoryInfo ;
(P556)
此外,还有一个静态类 Path ,它不操作文件或目录;相反,它具有一些文件名或目录路径的字符处理方法。 Path 也能够帮助处理临时文件。
所有这些类都不适用于 Metro 应用。
File 是一个静态类,它的方法都接受文件名参数。这个文件名可以是相对当前目录的路径,也可以是一个目录的完整路径。
如果目标文件已存在,那么 Move 会抛出一个异常;但是 Replace 不会,这两个方法允许将文件重命名或移动到另一个目录。
如果文件被标记为只读,那么 Delete 会抛出一个 UnauthorizedAccessException ;调用 GetAttribtes 可以预先判断其属性。
(P557)
FileInfo 提供了一个更简单的修改文件只读标记的方法 (IsReadOnly) 。
执行解压缩,可以将 CompressEx 替换成 UncompressEx 。
透明加密和压缩需要特殊的文件系统支持。 NTFS (硬盘中使用最广泛的格式) 支持这些特性; CDFS (在 CD-ROM 中) 和 FAT (在可移动内存卡中) 则不支持。
(P558)
GetAccessControl 和 SetAccessControl 方法支持通过 FileSecurity 对象 (位于命名空间 System.Security .AccessControl) 查询和修改操作系统授予用户和角色的权限。在创建一个新文件时,我们可以给FileStream 的构造函数传入一个 FileSecurity ,以指定它的权限。
(P559)
静态的 Directory 类具有一组与 File 类相似的方法,用于检查目录是否存在 (Exists) 、移动目录 (Move) 、 删除目录 (Delete) 、获取 / 设置 创建时间或最后访问时间,以及 获取 / 设置 安全权限。
使用 File 和 Directory 的静态方法,我们可以方便地执行一个文件或目录操作。如果需要一次性调用多个方法, FileInfo 和 DirectoryInfo 类支持一种简化这种调用的对象模型。
FileInfo 以实例方法的形式支持大部分的 File 静态方法以及一些额外的属性,例如 Extension 、 Length 、 IsReadOnly 和 Directory (返回一个 DirectoryInfo 对象) 。
(P560)
静态的 Path 类定义了一些处理路径和文件名的方法和字段。
(P561)
Combine 是非常有用的,它可用来组合目录和文件名或者两个目录,而不需要先检查名称后面是否有反斜杠。
GetFullPath 可以将一个相对于当前目录的路径转换为一个绝对路径。它接受例如 ..\..\file.txt 这样的值。
GetRandomFileName 会返回一个完全唯一的 8.3 格式文件名,但不会真正创建文件。
GetTempFileName 会使用一个自增计数器生成一个临时文件名,这个计数器每隔 65,000 次重复一遍。然后,它会用这个名称在本地临时目录创建一个 0 字节的文件。
System.Environment 类的 GetFolderPath 方法提供查找特殊文件夹的功能。
Environment.SpecialFolder 是一个枚举类型,它的值包括 Windows 中的所有特殊目录。
(P563)
DriveInfo 类可用来查询计算机的驱动器信息。
(P564)
静态的 GetDrives 方法会返回所有映射的驱动器,包括 CD-ROM 、内存卡和网络连接。
FileSystemWatcher 类可用来监控一个目录 (或者子目录) 的活动。当有文件或子目录被创建、修改、重命名、删除以及属性变化时, FileSystemWatcher 都会触发相应的事件。无论是用户还是进程执行这些操作,这些事件都会触发。
(P565)
因为 FileSystemWatcher 在一个独立线程上接收事件,所以事件处理代码中必须使用异常处理语句,防止错误使应用程序崩溃。
Error 事件不会通知文件系统错误;相反,它表示的是 FileSystemWatcher 的事件缓冲区溢出,因为它已经被 Changed 、 Created 、 Deleted 或 Renamed 占用。我们可以通过 InternalBufferSize 属性修改缓冲区大小。
IncludeSubdirectories 会递归执行。
Metro 应用都不能使用 FileStream 和 Directory / File 类。相反, Windows.Storage 命名空间包含一些具有相同用途的 WinRT 类型,其中两个主要类是 StorageFolder 和 StorageFile 。
StorageFolder 类表示一个目录,调用 StorageFolder 的静态方法 GetFolderFromPathAsync ,指定文件夹的完整路径,就可以获得一个 StorageFolder 对象。
(P566)
StorageFile 是操作文件的基础类。使用静态类 StorageFile.GetFileFromPathAsync ,可以使用完整路径获得一个文件实例;调用 StorageFolder 或 IsStorageFolder 对象的 GetFileAsync 方法,则可以使用相对路径获得一个文件实例。
(P567)
内存映射文件是 Framework 4.0 新增加的。它们有两个主要特性 :
1. 文件数据的高效随机访问;
2. 在同一台计算机的不同进程之间共享内存;
内存映射文件的类型位于 System.IO.MemoryMappedFiles 命名空间。在内部,它们是封装了支持内存映射文件的 Win32 API 。
虽然常规的 FileStream 也支持随机文件 I / O (通过设置流的 Position 属性实现) ,但是它在连续 I / O 方面进行了优化。一般原则大致是 :
1. FileStream 的连续 I / O 速度要比内存映射文件快 10 倍;
2. 内存映射文件的随机 I / O 速度要比 FileStream 快 10 倍;
修改 FileStream 的 Position 属性可能需要耗费几毫秒时间,并在循环中会进一步累加。 FileStream 不适用于多线程访问,因为它在读或写时位置会发生改变。
要创建一个内存映射文件,我们要 :
1. 获取一个普通的 FileStream ;
2. 使用文件流实例化 MemoryMappedFile ;
3. 在内存映射文件对象上调用 CreateViewAccessor ;
最后一步可以得到一个 MemoryMappedViewAccessor 对象,它具有一些随机读写简单类型、结构和数组的方法。
(P568)
内存映射文件也可以作为同一台计算机上不同进程间共享内存的一种手段。一个进程可以调用 MemoryMappedFile.CreateNew 创建一个共享内存块,而另一个进程则可以用相同的名称调用 MemoryMappedFile.OpenExisting 来共享同一个内存块。虽然它仍然是一个内存映射文件,但是已经完全脱离磁盘而进入内存中。
在 MemoryMappedFile 中调用 CreateViewAccessor 可以得到一个视图访问器,它可以用来执行随机位置的 读 / 写。
(P569)
Read * / Write * 方法可以接受数字类型、 bool 、 char 以及包含值类型元素或域的数组和结构体。引用类型 (及包含引用类型的数组或结构体) 是禁止使用的,因为它们无法映射到一个未托管的内存中。
我们还可以通过指针直接访问底层的未托管内存。
指针在处理大结构时的优势是 : 它们可以直接处理原始数据,而不需要使用 Read / Write 在托管内存和未托管内存之间进行数据复制。
每一个 .NET 程序都可以访问该程序独有的本地存储区域,即独立存储 (isolated storage) 。如果程序无法访问标准文件系统,那么很适合使用独立存储。使用受限 “互联网” 权限的 Silverlight 应用和 ClickOnce 应用就属于这种情况。
(P570)
在安全性方面,隔离存储区的作用更多的是阻止其他的应用程序进入,而不是阻止其中的应用程序出去。隔离存储区的数据受到严格保护,不会受到其他运行在最严格权限集之下的 .NET 应用程序的入侵。
在沙箱中运行的应用程序可以通过权限设置获得有限的隔离存储区配额。默认情况下,互联网和 Silverlight 应用程序在 Framework 4.0 中的配额是 1MB 。
【第16章】
(P575)
Framework 在 System.Net.* 命名空间中包含各种支持标准网络协议通信的类,例如 HTTP 、 TCP / IP 和 FTP 。下面是其中一些主要组件的小结 :
1. WebClient 外观类 —— 支持通过 HTTP 或 FTP 执行简单的 下载 / 上传 操作;
2. WebRequest 和 WebResponse 类 —— 支持更多的客户端 HTTP 或 FTP 操作;
3. HttpListener 类 —— 可用来编写 HTTP 服务器;
4. SmtpClient 类 —— 通过支持 SMTP 创建和发送电子邮件;
5. Dns 类 —— 支持域名和地址之间的转换;
6. TcpClient 、 UdpClient 、 TcpListener 和 Socket 类 —— 支持传输层和网络层的直接访问。
Framework 支持主要的 Internet 协议,但是它的功能不仅限于 Internet 连接,诸如 TCP / IP 等协议也可以广泛应用于局域网上。
大多数类型都位于传输层或应用层。
传输层定义了发送和接受字节的基础协议 (TCU 或 UDP) ;
应用层则定义支持特定应用程序的上层协议,例如获取 Web 页 (HTTP) 、 传输文件 (FTP) 、 发送邮件 (SMTP) 和域名与 IP 地址转换 (DNS) 。
通常,在应用层编程是最方便的。然而,有一些原因要求我们必须直接在传输层上进行操作,例如当需要使用一种 Framework 不支持的应用程序协议 (例如 POP3) 来接收邮件时。此外,当需要为某个特殊应用程序 (例如对等客户端) 发明一种自定义协议时,也是如此。
HTTP 属于应用层协议,它专门用于扩展通用的通信。它的基本运行方式是 “请给我这个 URL 的网页” ,可以很好地理解为 “返回使用这些参数调用这个方法的结果值” 。 HTTP 具有丰富的特性,它们在多层次业务应用程序和面向服务的体系结构中是非常有用的,例如验证和加密协议、消息组块、可扩展头信息和 Cookie ,并且多个服务器应用程序可以共享一个端口和 IP 地址。因此, HTTP 在 Framework 中得到很好的支持,包括直接支持以及通过 WCF 、 Web Services 和 ASP.NET 等技术实现的更高级支持。
(P576)
Framework 提供 FTP 客户端支持,这是最常用的 Internet 文件发送和接受协议。服务器端支持是通过 IIS 或 UNIX 服务器软件等形式实现的。
DNS (Domain Name Service : 域名服务) —— 域名和 IP 地址转换;
FTP (File Transfer Protocol : 文件传输协议) —— Internet 文件发送和接收的协议;
HTTP (Hypertext Transfer Protocol : 超文本传输协议) —— 查询网页和运行 Web 服务;
IIS (Internet Information Services : Internet 信息服务) —— 微软的 Web 服务器软件;
IP (Internet Protocol : Internet 协议) —— TCP 与 UDP 之下的网络层协议;
LAN (Local Area Network : 局域网) —— 大多数 LAN 使用 TCP / IP 等 Internet 协议;
POP (Post Office Protocol : 邮局协议) —— 查询 Internet 邮件;
SMTP (Simple Mail Transfer Protocol : 简单邮件传输协议) —— 发送 Internet 邮件;
TCP (Transmission and Control Protocol : 传输和控制协议) —— 传输层 Internet 协议,大多数更高级服务的基础;
UDP (Universal Datagram Protocol : 低开销服务使用传输层 Internet 协议,例如 “通用数据报协议” ) ;
UNC (Universal Naming Convention : 通用命名转换) —— \\computer\sharename\filename
URI (Uniform Resource Identifier : 统一资源标识符) —— 使用普遍的资源命名系统;
URL (Uniform Resource Locator : 统一资源定位符) —— 技术意义(逐渐停止使用) - URI 子集;流行意义 - URI 简称;
(P577)
要实现通信,计算机或设备都需要一个地址。 Internet 使用了两套系统 :
1. IPv4 : 这是目前的主流地址系统; IPv4 地址有 32 位。当用字符串表示时, IPv4 地址可以写为用点号分隔的 4 个十进制数。地址可能是全世界唯一的,也可能在一个特定子网中是唯一的;
2. IPv6 : 这是更新的 128 位地址系统。这些地址用字符串表示为用冒号分隔的十六进制。 .NET Framework 中要求地址加上方括号;
System.Net 命名空间的 IPAddress 类是采用其中一种协议的地址。它有一个构造函数可以接收字节数组,以及一个静态的 Parse 方法接收正确格式的字符串。
TCP 和 UDP 协议将每一个 IP 地址划分为 65535 个端口,从而允许一台计算机在一个地址上运行多个应用程序,每一个应用程序使用一个端口。许多程序都分配有标准端口,例如,HTTP 使用端口 80 ;SMTP 使用端口 25 。
从 49152 到 65535 的 TCP 和 UDP 端口是官方保留的,它们只用于测试和小规模部署。
IP 地址和端口组合在 .NET Framework 中是使用 IPEndPoint 类表示的。
(P578)
防火墙可以阻挡端口。在许多企业环境中,事实上只有少数端口是开放的,通常情况下,只开放端口 80 (不加密 HTTP) 和端口 443 (安全 HTTP) 。
URI 是一个具有特殊格式的字符串,它描述了一个 Internet 或 LAN 的资源,例如网页、文件或电子邮件地址。
正确的格式是由 IETF (Internet Engineering Task Force) 定义的。
URI 一般分成三个元素 : 协议 (scheme) 、 权限 (authority) 和路径 (path) 。
System 命名空间的 Uri 类正是采用这种划分方式,为每一种元素提供对应的属性。
Uri 类适合用来验证 URI 字符串的格式或将 URI 分割成相应的组成部分。另外,可以将 URI 作为一个简单的字符串进行处理,大多数网络连接方法都有接收 Uri 对象或字符串的重载方法。
在构造函数中传入以下字符串之一,就可以创建一个 Uri 对象 :
1. URI 字符串;
2. 硬盘中的一个文件的绝对路径;
3. LAN 中一个文件的 UNC 路径;
文件和 UNC 路径会自动转换为 URI : 添加协议 “file:” ,反斜杠会转换为斜杠。 Uri 的构造函数在创建 Uri 之前也会对传入的字符串执行一些基本的清理操作,包括将协议和主机名转换为小写、删除默认端口号和空端口号。如果传入一个不带协议的 URI 字符串,那么会抛出一个 UriFormatException 。
(P579)
Uri 有一个 IsLoopback 属性,它表示 Uri 是否引用本地主机 (IP 地址为 127.0.0.1) ;以及一个 IsFile 属性,它表示 Uri 是否引用一个本地或 UNC (IsUnc) 路径。如果 IsFile 返回 true , LocalPath 属性会返回一个符合本地操作系统习惯的 AbsolutePath (带反斜杠) ,然后可以用它来调用 File.Open 。
Uri 的实例有一些只读属性。要修改一个 Uri ,我们需要实例化一个 UriBuilder 对象,这是一个可写属性,它可以通过 Uri 属性转换为 Uri 。
Uri 也具有一些比较和截取路径的方法。
URI 后面的斜杠是很重要的,服务器会根据它来决定是否处理路径组成部分。
WebRequest 和 WebResponse 是管理 HTTP 和 FTP 客户端活动及 “file:” 协议的通用基类。它们封装了这些协议共用的 “请求 / 响应” 模型 : 客户端发起请求,然后等待服务器的响应。
WebClient 是一个便利的门店 (facade) 类,它负责调用 WebRequest 和 WebResponse ,可以节省很多编码。 WebClient 支持字符串、字节数组、文件或流;而 WebRequest 和 WebResponse 只支持流。但是, WebClient 也不是万能的,因为它也不支持某些特性 (如 cookie) 。
HttpClient 是另一个基于 WebRequest 和 WebResponse 的类 (更准确说是基于 HttpWebRequest 和 HttpWebResponse) ,并且是 Framework 4.5 新引入的类。
WebClient 主要作为 请求 / 响应 类之上薄薄的一层,而 HttpClient 则增加了更多的功能,能够处理基于 HTTP 的 Web API 、 基于 REST 的服务和自定义验证模式。
(P580)
WebClient 和 HttpClient 都支持以字符串或字节数组方式处理简单的文件 下载 / 上传 操作。它们都拥有一些异步方法,但是只有 WebClient 支持进度报告。
WinRT 应用程序不能使用 WebClient ,它必须使用 WebRequest / WebResponse 或 HttpClient (用于 HTTP 连接) 。
WebClient 的使用步骤 :
1. 实例化一个 WebClient 对象;
2. 设置 Proxy 属性值;
3. 在需要验证时设置 Credentials 属性值;
4. 使用相应的 URI 调用 DownloadXXX 或 UploadXXX 方法;
UploadValues 方法可用于以 POST 方法参数提交一个 HTTP 表单的值。
WebClient 还包含一个 BaseAddress 属性,可用于为所有地址添加一个字符串前缀。
(P581)
WebClient 被动实现了 IDisposable —— 因为它继承了 Component 。然而,它的 Dispose 方法在运行时并没有执行太多实际操作,所以不需要清理 WebClient 的实例。
从 Framework 4.5 开始, WebClient 提供了长任务方法的异步版本,它们会返回可以等待的任务。
await webClient.DownloadTaskAsync() 这些方法使用 “TaskAsync” 后缀,不同于使用 “Async” 后缀的 EAP 旧异步方法。但是,新方法不支持取消操作和进度报告的标准 “TAP” 模式。相反,在处理延续时,必须调用 WebClient 对象的 CancelAsync 方法;而处理进度报告时,则需要处理 DownloadProgressChanged / UploadProgressChanged 事件。
如果需要使用取消操作或进度报告,那么要避免使用同一个 WebClient 对象依次执行多个操作,因为这样会形成竞争条件。
WebRequest 和 WebResponse 比 WebClient 复杂,但是更加灵活。下面是开始使用的步骤 :
1. 使用一个 URI 调用 WebRequest.Create ,创建一个 Web 请求实例;
2. 设置 Proxy 属性;
3. 如果需要验证身份,则设置 Credentials 属性;
如果要上传数据,则 :
4. 调用请求对象的 GetRequestStream ,然后在流中写入数据。如果需要处理响应,则转到第 5 步。
如果要下载数据,则 :
5. 调用请求对象的 GetResponse ,创建一个 Web 响应实例;
6. 调用响应对象的 GetResponseStream ,然后 (可以使用 StreamReader) 从流中读取数据;
(P582)
静态方法 Create 会创建一个 WebRequest 类型的子类实例。
将 Web 请求对象转换为具体的类型,就可以访问特定协议的特性。
“https:” 协议是指通过安全套接层 (Secure Sockets Layer, SSL) 实现的安全 (加密) HTTP 。 WebClient 和 WebRequest 都会在遇到这种前缀时激活 SSL 。
“file:” 协议会将请求转发到一个 FileStream 对象,其目的是确定一个与读取 URI 一致的协议,它可能是一个网页、 FTP 站点或文件路径。
(P583)
WebRequest 包含一个 Timeout 属性,其单位为毫秒。如果出现超时,那么程序就会抛出一个 WebException 异常,其中包含一个 Status 属性 : WebExceptionStatus.Timeout 。 HTTP 的默认超时时间为 100 秒,而 FTP 的超时时间为无限。
WebRequest 对象不能回收并用于处理多个请求 —— 每一个实例只适用于一个作业。
HttpClient 是 Framework 4.5 新引入的类,它在 HttpWebRequest 和 HttpWebResponse 之上提供了另一层封装。它的设计是为了支持越来越多的 Web API 和 REST 服务,在处理比获取网页等更复杂的协议时实现比 WebClient 更佳的体验。具体地 :
1. 一个 HttpClient 就可以支持并发请求。如果要使用 WebClient 处理并发请求,则需要为每一个并发线程创建一个新实例,这时需要自定义请求头、 cookie 和 验证模式,因此会比较麻烦;
2. HttpClient 可用于编写和插入自定义消息处理器。这样可以创建单元测试桩函数,以及创建自定义管道 (用于记录日志、压缩、加密等) 。调用 WebClient 的单元测试代码则很难编写;
3. HttpClient 包含丰富且可扩展的请求头与内容类型系统;
HttpClient 不能完全代替 WebClient ,因为它不支持进度报告。
WebClient 也有一个优点,它支持 FTP 、 file:// 和 自定义 URI 模式,它也适用于所有 Framework 版本。
使用 HttpClient 的最简单方法是创建一个实例,然后使用 URI 调用其中一个 Get* 方法。
HttpClient 的所有 I / O 密集型方法都是异步的 (它们没有同步实现版本) 。
与 WebClient 不同,想要获得最佳性能的 HttpClient ,必须重用相同的实例 (否则诸如 DNS 解析操作会出现不必要的重复执行)。
HttpClient 允许并发操作。
HttpClient 包含一个 Timeout 属性和一个 BaseAddress 属性,它会为每一个请求添加一个 URI 前缀。
HttpClient 在一定程度上就是一层实现 : 通常使用的大部分属性都定义在另一个类中,即 HttpClientHandler 。
(P584)
GetStringAsync 、 GetByteArrayAsync 和 GetStreamAsync 方法是更常用的 GetAsync 方法的快捷方法。
HttpResponseMessage 包含一些访问请求头 和 HTTP StatusCode 的属性。与 WebClient 不同,除非显式调用 EnsureSuccessStatusCode ,否则返回不成功状态不会抛出异常。然而,通信或 DNS 错误会抛出异常。
HttpResponseMessage 包含一个 CopyToAsync 方法,它可以将数据写到另一个流中,适用于将输入写到一个文件中。
GetAsync 是 HTTP 的 4 种动作相关的 4 个方法之一 (其他方法是 PostAsync 、 PutAsync 和 DeleteAsync) 。
创建一个 HttpRequestMessage 对象,意味着可以自定义请求的属性,如请求头和内容本身,它们可用于上传数据。
在创建一个 HttpRequestMessage 对象之后,设置它的 Content 的属性,就可以上传内容。这个属性的类型是抽象类 HttpContent 。
大多数自定义请求的属性都不是在 HttpClient 中定义,而是在 HttpClientHandler 中定义。后者实际上是抽象类 HttpMessageHandler 的子类。
HttpMessageHandler 非常容易继承,同时也提供了 HttpClient 的扩展点。
(P586)
代理服务器 (proxy server) 是一个中间服务器,负责转发 HTTP 和 FTP 请求。
代理本身拥有地址,并且可能需要执行身份验证,所以只有特定的局域网用户可以访问互联网。
创建一个 WebClient 或 WebRequest 对象,就可以使用 WebProxy 对象通过代理服务器转发请求。
(P587)
如果要使用 HttpClient 访问代理,那么首先要创建一个 HttpClientHandler ,设置它的 Proxy 属性,然后将它传递给 HttpClient 的构造方法。
如果已知不存在代理,那么可以在 WebClient 和 WebRequest 对象上将 Proxy 属性设置为 null 。否则, Framework 可能会尝试自动检查代理设置,这会给请求增加 30 秒延迟。如果 Web 请求执行速度过慢,那么很可能就是这个原因造成的。
HttpClientHandler 还有一个 UseProxy 属性,将它设置为 false ,就可以将 Proxy 属性置空,从而禁止自动检测。
如果在创建 NetworkCredential 时提供一个域,那么就会使用基于 Windows 的身份验证协议。如果想要使用当前已验证的 Windows 用户,则可以在代理的 Credentials 属性上设置静态的 CredentialCache.DefaultNetworkCredentials 值。
创建一个 NetworkCredential 对象,将它设置到 WebClient 或 WebRequest 的 Credentials 属性上,就可以向 HTTP 或 FTP 站点提供用户名和密码。
(P588)
身份验证最终由一个 WebRequest 子类型处理,它会自动协商一个兼容协议。
(P589)
WebRequest 、 WebResponse 、 WebClient 及其流都会在遇到网络或协议错误时抛出一个 WebException 异常。
HttpClient 也有相同行为,但是它将 WebException 封装在一个 HttpRequestException 中。
使用 WebException 的 Status 属性,就可以确定具体的错误类型,它会返回一个枚举值 WebExceptionStatus 。
(P591)
WebClient 、 WebRequest 和 HttpClient 都可以添加自定义 HTTP 请求头,以及在响应中列举请求头信息。请求头只是一些 键 / 值 对,其中包含相应的元数据,如消息内容类型或服务器软件。
HttpClient 包含了一些强类型集合,其中包含与标准 HTTP 头信息相对应的属性。 DefaultRequestHeaders 属性包含适用于每一个请求的头信息。
HttpRequestMessage 类的 Headers 属性包含请求特有的头信息。
查询字符串只是通过问号 (?) 附加到 URI 后面的字符串,它可用于向服务器发送简单的数据。
WebClient 包含一个字典风格的属性,它可以简化查询字符串的操作。
(P592)
如果要使用 WebRequest 或 HttpClient 实现相同效果,那么必须手工赋给请求 URI 正确格式的字符串。
如果查询中包含符号或空格,那么必须使用 Uri 的 EscapeDataString 方法才能创建合法的 URI 。
EscapeDataString 与 EscapeUriString 类似,唯一不同的是前者进行了特殊字符的编码,如 & 和 = ,否则它们会破坏查询字符串。
WebClient 的 UploadValues 方法可以以 HTML 表单的方式提交数据。
NameValueCollection 中的键与 HTML 表单的输入框相对应。
使用 WebRequest 上传表单数据的操作更为复杂,如果需要使用 cookies 等特性,则必须采用这种方法。下面是具体的操作过程 :
1. 将请求的 ContentType 设置为 “application/x-www-form-urlencoded” ,将它的方法设置为 “POST” ;
2. 创建一个包含上传数据的字符串,并且将其编码为 : name1=value1&name2=value2&name3=value3...
3. 使用 Encoding.UTF8.GetBytes 将字符串转换为字节数组;
4. 将 Web 请求的 ContentLength 属性设置为字节数组的长度;
5. 调用 Web 请求的 GetRequestStream ,然后写入数据数组;
6. 调用 GetResponse ,读取服务器的响应。
(P593)
Cookie 是一种 名称 / 值 字符串对,它是 HTTP 服务器通过响应头发送到客户端的。 Web 浏览器客户端通常会记住 cookie ,然后在终止之前,后续请求都会将它们重复发送给服务器 (相同地址) 。
Cookie 使服务器知道它是否正在连接之前连接过的相同客户端,从而不需要在 URI 重复添加复杂的查询字符串。
默认情况下, HttpWebRequest 会忽略从服务器接收的任意 cookie 。为了接收 cookie ,必须创建一个 CookieContainer 对象,然后将它分配到 WebRequest 。然后,就可以列举响应中接收到的 cookie 。
(P594)
WebClient 门面类不支持 cookie 。
(P596)
可以使用 HttpListener 类编写自定义 HTTP 服务器。
(P599)
对于简单的 FTP 上传和下载操作,可以使用 WebClient 按照前面的方式实现。
(P600)
静态的 Dns 类封装了 DNS (Domain Name Service ,域名服务) ,它可以执行原始 IP 地址和人性化的域名之间的转换操作。
GetHostAddresses 方法可以将域名转换为 IP 地址 (或地址) 。
(P601)
GetHostEntry 方法则执行相反操作,将地址转换为域名。
GetHostEntry 方法还接受一个 IPAddress 对象,所以我们可以用一个字节数组来表示 IP 地址。
在使用 WebRequest 或 TcpClient 等类时,域名会自动解析为 IP 地址。然而,如果想要在应用程序的生命周期内向同一个地址发送多个网络请求,有时候需要先使用 DNS 将域名显式地转换为 IP 地址,然后再直接使用得到的 IP 地址进行通信,从而提高运行性能。这样就能够避免重复解析同一个域名,有助于 (使用 TcpClient 、 UdpClient 或 Socket ) 处理传输层协议。
System.Net.Mail 命名空间的 SmtpClient 类可用来通过普遍使用的简单邮件传输协议 (Simple Mail Transfer Protocol ,SMTP) 发送邮件消息。
要发送一条简单的文本消息,我们需要实例化 SmtpClient ,将它的 Host 属性设置为 SMTP 服务器地址,然后调用 Send 。
为了防止垃圾邮件, Internet 中大多数 SMTP 服务器都只接受来自 ISP 订阅者的连接,所以我们需要使用适合当前连接的 SMTP 地址才能成功发送邮件。
MailMessage 对象支持更多的选项,包括添加附件。
SmtpClient 可以为需要执行身份验证的服务器指定 Credentials ,如果支持 EnableSsl ,也可以将 TCP Port 修改为非默认值。通过修改 DeliveryMethod 属性,我们可以使用 SmtpClient 代替 IIS 发送邮件消息,或者直接将消息写到指定目录下的一个 .eml 文件中。
(P602)
TCP 和 UDP 是大多数 Internet (与局域网) 服务所依赖的传输层协议的基础。
HTTP 、 FTP 和 SMTP 使用 TCP ; DNS 使用 UDP 。
TCP 是面向连接的,具有可靠性机制; UDP 是无连接的,负载更小,并且支持广播。
BitTorrent 和 Voice over IP 都使用 UDP 。
传输层比其他上层协议具有更高灵活性,性能可能也更高,但是它要求用户自己处理一些具体任务,如身份验证和加密。
对于 TCP ,我们可以选择使用简单易用的 TcpClient 和 TcpListener 外观类,或者使用功能丰富的 Socket 类。事实上,它们可以混合使用,因为我们可以通过 TcpClient 的 Client 属性获得底层的 Socket 对象。Socket 类包含更多的配置选项,它支持网络层 (IP) 的直接访问,也支持一些非 Internet 协议,如 Novell 的 SPX/IPX 。
和其他协议一样, TCP 也区分客户端和服务器 : 客户端发起请求,而服务器则等待请求。
NetworkStream 提供一种双向通信手段,同时支持从服务器发送和接收字节数据。
(P604)
TcpClient 和 TcpListener 提供了基于任务的异步方法,可用于实现可扩展的并发性。使用这些方法,只需要将阻塞方法替换为它们对应的 *Async 版本方法,然后等待任务返回。
(P605)
.NET Framework 并没有提供任何 POP3 的应用层支持,所以要从一个 POP3 服务器接收邮件,必须在 TCP 层编写代码。
(P606)
Windows Runtime 通过 Windows.Networking.Sockets 命名空间实现 Tcp 功能。与 .NET 实现一样,其中主要有两个类,分别充当服务器和客户端角色。在 WinRT 中,它们分别是 StreamSocketListener 和 StreamSocket 。
【第17章】
(P608)
序列化与反序列化,通过它对象可以表示成一个纯文本或者二进制形式。
序列化是把内存中的一个对象或者对象图 (一组互相引用的对象) 转换成一个字节流或者一组可以保存或传输的 XML 节点。反序列化正好相反,它把一个数据流重新构造成一个内存中的对象或对象图。
序列化和反序列化通常用于 :
1. 通过网络或应用程序边界传输对象;
2. 在文件或数据库中保存对象的表示;
序列化与反序列化也用于深度克隆对象。
数据契约和 XML 序列化引擎也可以被用作通用目的工具来加载和保存已知结构的 XML 文件。
.NET Framework 从两个角度来支持序列化与反序列化 : 第一,从想进行序列化和反序列化对象的客户端角度; 第二,从想控制其如何被序列化的类型角度。
在 .NET Framework 中有 4 种序列化机制 :
1. 数据契约序列化器;
2. 二进制序列化器;
3. (基于属性的) XML 序列化器 (XmlSerializer) ;
4. IXmlSerializable 接口;
(P609)
其中前三种 “引擎” 可以完成大部分或所有序列化操作。而最后的 IXmlSerializable 接口是一个可以通过使用 XmlReader 和 XmlWriter 进行序列化的起桥梁作用的钩子 (hook) 。
IXmlSerializable 可以联合数据契约序列化器或者 XmlSerializer 来处理更复杂的 XML 序列化任务。
IXmlSerializable 的分数假设已经使用 XmlReader 和 XmlWriter 最优化地 (手) 写代码。
XML 序列化引擎要求回收相同的 XmlSerializer 对象以达到更佳的性能。
出现这三种引擎在一定程度上是由于历史原因。 Framework 在序列化上基于两个完全不同的目的 :
1. 真实的序列化包含类型及其引用的 .NET 对象图;
2. XML 和 SOAP 消息之间的互操作标准;
第一种由 Remoting 的需求而产生;而第二种是由于 Web 服务。写一个序列化引擎来同时完成这两项任务非常困难,所以 Microsoft 编写了两个引擎 : 二进制序列化器和 XML 序列化器。
后来在 .NET Framework 3.0 中出现 WCF 时,其部分目标在于统一 Remoting 和 Web 服务。这就要求一个新的序列化引擎,所以就出现了数据契约序列化器。数据契约序列化器统一了旧有的两个和消息有关的引擎的特性。但是在这个上下文之外,这两个旧的序列化引擎还是很重要的。
数据契约序列化器在这三种序列化引擎中是最新的也是最有用的引擎,并被 WCF 使用。它在下面两种情形下尤其强大 :
1. 通过符合标准的消息协议来交换信息;
2. 需要好的版本容差能力,并且能够保留对象引用;
数据契约序列化器支持一种数据契约模型 : 它能帮助把类型的底层细节与被序列化过的数据结构解耦。这为我们提供了优秀的版本容差性,也就意味着我们可以反序列化从早期或者后来版本序列化过来的数据类型。甚至可以反序列化已经被重命名或者被移到不同程序集中的类型。
(P610)
数据契约序列化器可以处理大多数的对象图,尽管它需要比二进制序列化器更多的辅助。如果能够灵活地构造 XML ,它也可被用作通用目的的读写 XML 文件的工具。但是如果需要存储数据属性或者要处理随机出现的 XML 元素,就不能使用数据契约序列化器了。
二进制序列化器比较容易使用、非常的自动化,并且在 .NET Remoting 中自始至终都被很好地支持。
Remoting 在同一进程中的两个应用域之间通信时使用二进制序列化器。
二进制序列化器被高度地自动化了 : 只需要一个属性就可以使一个复杂类型可完全序列化。当所有类型都要求被高保真序列化时,二进制序列化器要比数据契约序列化器快。但是它把类型的内部结构与被序列化数据的格式紧密耦合,导致了比较差的版本容差性 (在 Framework 2.0 之前,即使添加一个字段也会成为破坏版本的变化) 。二进制引擎也不是真正地为生成 XML 而设计的,尽管它为基于 SOAP 的消息提供了一个有限的可以和简单类型互操作的格式化器。
XML 序列化引擎只能产生 XML ,它没有其他能够保持和恢复复杂对象图的引擎那么强大 (它不能够恢复共享的对象引用) 。但是对于处理比较随意的 XML 结构,它是三者之中最灵活的。
XML 引擎也提供了较好的版本容差性。
XMLSerializer 被 ASMX Web 服务使用。
实现 IXmlSerializable 意味着通过使用一个 XmlReader 和 XmlWriter 来完成序列化。 IXmlSerializable 接口被 XmlSerializer 和数据契约序列化器所识别,所以它可以有选择地被用来处理更复杂的类型。它也可以直接被 WCF 和 ASMX Web 服务使用。
(P611)
WCF 总是使用数据契约序列化器,尽管它可以和其他引擎的属性和接口进行互操作。
Remoting 总是使用二进制序列化引擎。
Web 服务总是使用 XMLSerializer 。
使用数据契约序列化器的基本步骤 :
1. 决定是使用 DataContractSerializer 还是 NetDataContractSerializer ;
2. 使用 [DataContract] 和 [DataMember] 属性修饰要序列化的对象和成员;
3. 实例化序列化器后调用 WriteObject 或 ReadObject ;
如果选择 DataContractSerializer ,同时需要注册已知类型 (也能够被序列化的子类型) ,并且要决定是否保留对象引用。
可能也需要采取特殊措施来保证集合能被正确地序列化。
与数据契约序列化器相关的类型被定义在 System.Runtime.Serialization 命名空间中,并包含在同名的程序集中。
有两个数据契约序列化器 :
1. DataContractSerializer —— .NET 类型与数据契约类型松耦合;
2. NetDataContractSerializer —— .NET 类型与数据契约类型紧耦合;
DataContractSerializer 可以产生可互操作的符合标准的 XML 。
(P612)
如果通过 WCF 通信或者 读 / 写 一个 XML 文件,可能倾向于使用 DataContractSerializer 。
选择序列化器后,下一步就是添加相应的属性到要序列化的类型和成员上,至少应该 :
1. 添加 [DataContract] 属性到每个类型上;
2. 添加 [DataMember] 属性到每个包含的成员上;
(P613)
DataContractSerializer 的构造方法需要一个根对象类型 (显式序列化的对象类型) ,相反的, NetDataContractSerializer 就不需要。
NetDataContractSerializer 在其他方面与 DataContractSerializer 的用法相同。
两种序列化器都默认使用 XML 格式化器。
使用 XmlReader ,可以为了可读性让输出包含缩进。
指定名称和命名空间可以把契约标识与 .NET 类型名称解耦。它能够保证当重构和改变类型的名称或命名空间时,序列化不会受到影响。
(P614)
[DataMember] 可以支持 public 和 private 字段和属性。字段和属性的数据类型可以是下列类型的任何一种 :
1. 任何基本类型;
2. DateTime 、 TimeSpan 、 Guid 、 Uri 或 Enum 值;
3. 上述类型的 Nullable 类型;
4. Byte[] (在 XML 中序列化为 base 64) ;
5. 任何用 DataContract 修饰的已知类型;
6. 任何 IEnumerable 类型;
7. 任何被 [Serializable] 修饰,或者实现了 ISerializable 的类型;
8. 实现了 IXmlSerializable 的任何类型;
可以同时使用二进制格式化器和 DataContractSerializer 或者 NetDataContractSerializer ,过程是一样的。
二进制格式化器输出会比 XML 格式化器稍微小一些,当类型中包含大的数组时就会明显地看到小得多。
在使用 NetDataContractSerializer 时,不需要特别地处理子类的序列化,除非子类需要 [DataContract] 属性。
DataContractSerializer 必须要了解它可能序列化或反序列化的所有子类型。
(P616)
当序列化子类型时,不管使用哪种序列化器, NetDataContractSerializer 会导致性能上的损失。就好像是当遇到子类型时,它就必须停下来思考一下。
当在一个应用程序服务器上处理大量并发请求时才会考虑序列化性能。
(P617)
NetDataContractSerializer 总是会保留引用相等性。而 DataContractSerializer 不会,除非指定它保留。
可以在构造 DataContractSerializer 时指定参数 preserveObjectReferences 为 true 来要求引用完整性。
(P618)
如果某个成员对于一个类型是非常重要的,可以通过指定 [IsRequired] 要求它必须出现,如果成员没有出现,在序列化时会抛出一个异常。
数据契约序列化器对数据成员的数据要求极其苛刻。反序列化器实际上会跳过任何被认为在序列外的成员。
在序列化成员时按下面的顺序 :
1. 从基类到子类;
2. 根据 Order 从低到高 (对于 [Order] 属性被设置的数据成员) ;
3. 字母表顺序 (使用传统的字符串比较法) ;
(P619)
要指定顺序的主要原因是为了遵循特定的 XML Schema 。 XML 元素的顺序等同于数据成员顺序。
(P620)
数据契约序列化器可以保持和恢复可遍历集合。
(P622)
如果要在序列化之前或之后执行一个自定义方法,可以通过在方法上标记以下属性 :
1. [OnSerializing] —— 指示在序列化之前调用这个方法;
2. [OnSerialized] —— 指示在序列化之后调用这个方法;
3. [OnDeserializing] —— 指示在反序列化之前调用这个方法;
4. [OnDeserialized] —— 指示在反序列化之后调用这个方法;
自定义方法只能定义一个 StreamingContext 类型的参数。这个参数是为了与二进制引擎保持一致而被要求的,它不被数据契约序列化器使用。
[OnSerializing] 和 [OnDeserialized] 在处理超出数据契约引擎能力之外的成员时有用,例如一个超额的集合或者没有实现标准接口的集合。
(P623)
[OnSerializing] 标记的方法也可以被用作有条件的序列化字段。
注意数据契约反序列化器会绕过字段初始化器和构造方法。标记了 [OnDeserializing] 的方法在反序列化过程中起着伪造构造方法的作用,并且它对初始化被排除在序列化外的字段很有用。
使用这 4 个属性修饰的方法可能是私有的,如果子类需要参与其中,那么它们可以使用相同的属性定义自己的方法,然后它们一样可以执行。
(P624)
数据契约序列化器也可以序列化标记了二进制序列化引擎中的属性或接口类型。这种功能是非常重要的,因为这是为了支持已经被写入 Framework 3.0 以下版本 (包括 .NET Framework) 中的二进制引擎。
下面两项可以标记一个可被二进制引擎序列化的类型 :
1. [Serializable] 属性;
2. 实现 ISerializable ;
二进制互操作性对于序列化已有类型并且需要同时支持这两种引擎的情况比较有用。它也提供了扩展数据契约序列化器的另一种方式,因为二进制引擎的 ISerializable 要比数据契约属性更灵活。但是,数据契约序列化器不能通过 ISerializable 格式化添加的数据。
(P625)
数据契约序列化器的一个限制是它几乎不能控制 XML 的结构。在一个 WCF 应用程序中,这实际上是有好处的,因为它使得基础结构更容易符合标准消息协议。
如果需要控制 XML 的结构,可以实现 IXmlSerializable 接口,然后使用 XmlReader 和 XmlWriter 来手动地读和写 XML ,数据契约序列化器仅允许在那些需要这一控制的类型上执行这些操作。
二进制序列化引擎被 Remoting 隐式地使用,它可以用来完成把对象保存到磁盘或从磁盘上还原对象之类的任务。二进制序列化被高度地自动化了,并可以用最少的操作来处理复杂的对象图。
有两种方式让一个类型支持二进制序列化。第一种是基于属性;第二种是实现 ISerializable 接口。添加属性相对比较简单,而实现 ISerializable 更灵活。实现 ISerializable 主要是为了 :
1. 动态地控制什么要被序列化;
2. 让可序列化类型能够被其他部分更友好地继承;
一个类型可以使用单个属性指定为可序列化的。
[Serializable] 属性使序列化器包含类型中所有的字段。这既包含私有字段,也包含公共字段 (但不包含属性) 。每一个字段本身都可序列化,否则就会抛出一个异常。基本 .NET 类型,例如 string 和 int 支持序列化 (许多其他 .NET 类型也是) 。
[Serializable] 属性不能被继承,所以子类不会自动成为可序列化的,除非也在子类上标记上这个属性。
对于自动属性,二进制序列化引擎会序列化底层的被编译出的字段。但是,当增加属性时,重新编译这个类型会改变这个字段的名称,这就会破坏已序列化数据的兼容性。处理方法就是在 [Serializable] 的类型里避免使用自动属性或者实现 ISerializable 接口。
(P626)
为了序列化一个实例,可以实例化一个格式化器,然后调用 Serialize 方法。在二进制引擎中有两个可用的格式化器 :
1. BinaryFormatter —— 两者之中效率稍高,在更少的时间里产生更小的输出。它的命名空间是 System.Runtime.Serialization.Formatters.Binary ,程序集为 mscorlib 。
2. SoapFormatter —— 它支持在使用 Remoting 时基本的 SOAP 样式的消息。它的命名空间是 System.Runtime.Serialization.Formatters.Soap ,程序集为 System.Runtime.Serialization.Formatters.Soap.dll ;
SoapFormatter 没有 BinaryFormatter 实用。 SoapFormatter 不支持泛型或者筛选对版本容差有必要的额外数据。
反序列化器在重新创建对象时会绕过所有的构造方法。在这个过程中实际调用了 FormatterServices.GetUninitializedObject 方法来完成这个工作。可以自己调用这个方法来实现可能会非常复杂的设计模式。
序列化过的数据包含类型和程序集的全部信息,所以如果试图把序列化的结果转换到一个不同程序集中的类型,结果会产生一个错误。在反序列化过程中,序列化器会完全恢复对象引用到序列化的状态。集合同样如此,它会对集合像其他类型一样处理 (所有在 System.Collections.* 下的类型都被标记为可序列化) 。
二进制引擎可以处理大且复杂的对象图而不需要特别辅助 (不用保证所有参与的成员都可序列化) 。唯一要注意的是,序列化器的性能会随着对象图的引用数量的增加而降低。这样在一个要处理大量并发请求的 Remoting 服务器上就会成为一个问题。
(P627)
不同于数据契约对要序列化的字段使用选择性加入方针,二进制引擎使用选择性排除方针。
对于不想序列化的字段,必须显式地使用 [NonSerialized] 属性来标记它们。
不序列化的成员在反序列化后总是为空或 null ,即使在构造方法或字段初始化器中设置了它们。
(P628)
二进制引擎也支持 [OnSerializing] 和 [OnSerialized] 属性,这两个属性用来标记在序列化之前或之后要被调用的方法上。
默认,添加一个字段会破坏已经序列化的数据的兼容性,除非新的字段附加了 [OptionalField] 属性。
(P629)
版本健壮性十分重要,避免重命名和删除字段,同时避免追溯性地添加 [NonSerialized] 属性,永远不要改变字段的类型。
如果在双向通信时,要求版本健壮性,必须使用二进制格式化器,否则需要通过实现 ISerializable 来手动地控制序列化。
实现 ISerializable 可以让一个类型完全控制其二进制序列化和反序列化。
GetObjectData 在序列化时被触发,它的任务就是把想序列化的所有字段存放到 SerializationInfo (一个 名称 / 值 的字典) 对象里。
(P630)
把 GetObjectData 方法设置为 virtual 可以让子类扩展序列化而不用重新实现这个接口。
SerializationInfo 也包含相应的属性以用来控制实例应该反序列化的类型和程序集。
StreamingContext 参数是它包含的结构,一个枚举值指示这个序列化的实例保存的位置 (磁盘、 Remoting 等,尽管这个值不总是有) 。
除了实现 ISerializable ,一个控制其序列化的类型也需要提供一个反序列化构造方法,这个方法包含和 GetObjectData 方法一样的两个参数。构造方法可以被声明为任何访问级别,运行时总能够找到它。特别是,可以声明它为 protected 级别,这样子类就可以调用它了。
(P632)
Framework 提供了专门的 XML 序列化引擎,即在 System.Xml.Serializaion 命名空间下的 XmlSerializer 。它适合把 .NET 类型序列化为 XML 文件,它也被 ASMX Web 服务隐式地使用。
和二进制类似,可以使用以下两种方式 :
1. 在类型上使用定义在 System.Xml.Serialization 上的属性;
2. 实现 IXmlSerializable ;
然而不同于二进制引擎,实现接口 (例如 IXmlSerializable ) 就会完全避开引擎,要完全使用 XmlReader 和 XmlWriter 来实现序列化。
为了使用 XmlSerializer ,要实例化它,并调用 Serialize 和 Deserialize 方法传入 Stream 和对象实例。
(P633)
Serialize 和 Deserialize 方法可以与 Stream 、 XmlWriter / XmlReader 或者 TextWriter / TextReader 一起工作。
XmlSerializer 可以序列化没有标记任何属性的类型。
默认,它会序列化类型上的所有公共字段和属性。
可以使用 [XmlIgnore] 属性来排除不想被序列化的成员。
不同于其他两个引擎, XmlSerializer 不能识别 [OnDeserializing] 属性,在反序列化时依赖于一个无参数的构造方法,如果没有无参的构造方法,就会抛出一个异常。
尽管 XmlSerializer 可以序列化任何类型,但是它会识别以下类型,并且会进行特殊的处理 :
1. 基本类型、 DateTime 、 TimeSpan 、 Guid 以及这些类型的可空类型版本;
2. Byte[] (它会被转化为 base64 编码) ;
3. 一个 XmlAttribute 或者 XmlElement (它们的内容会被注入到流中) ;
4. 任何实现了 IXmlSerializable 的类型;
5. 任何集合类型;
XML 反序列化器允许版本容差 : 如果缺少元素或属性,或者有多余的数据出现,它都可以正常工作。
(P634)
字段和属性默认都被序列化为 XML 元素。
默认的 XML 命名空间为空 (不同于数据契约序列化器使用类型的命名空间) 。
为了指定一个 XML 命名空间, [XmlElement] 和 [XmlAttribute] 都接受一个 Namespace 的参数。也可以对类型本身使用 [XmlRoot] 来给它分配名称和命名空间。
XmlSerializer 会按照成员在类中定义的顺序写元素。可以通过在 XmlElement 属性上指定 Order 值来改变这个顺序。
一旦使用了 Order ,所有要序列化的成员都得使用。
而反序列化器并不关心元素的顺序,不管元素以任何顺序出现,类型总能够被恰当地反序列化。
(P635)
XmlSerializer 会自动地递归对象引用。
(P636)
如果有两个属性或字段引用了相同的对象,那么这个对象会被序列化两次。如果想保留引用相等性,必须使用其他的序列化引擎。
(P637)
XmlSerializer 识别和序列化具体的集合类型,而不需要其他干涉。
(P640)
实现 IXmlSerializable 的规则如下 :
1. ReadXml 应该读取最外层起始元素,然后读取内容,最后才是最外层结束元素;
2. WriteXml 应该只写入内容;
通过 XmlSerializer 序列化和反序列化时会自动调用 WriteXml 和 ReadXml 方法。
【第18章】
(P641)
程序集是 .NET 中的基本部署单元,也是所有类的容器。
程序集包含已编译的类和它们的 IL 代码、运行时资源,以及用于控制版本、安全性和引用其他程序集的信息。
程序集也为类解析和安全许可定义了边界。
一般来说,一个程序集包含单个 PE (Windows Portable Executable ,可移植的执行体) 文件,如果是应用程序,则带有 .exe 扩展名;如果是可重用的库,则扩展名为 .dll 。
程序集包含 4 项内容 :
1. 一个程序集清单 —— 向 .NET 运行时提供信息,例如程序集的名称、版本、请求的权限以及引用的其他程序集;
2. 一个应用程序清单 —— 向操作系统提供信息,例如程序集应该被如何部署和是否需要管理提升;
3. 一些已编译的类 —— 程序集中定义的类的 IL 代码和元数据;
4. 资源 —— 嵌入程序集中的其他数据,例如图像和可本地化的文本;
所有这些内容中,只有程序集清单是必需的,尽管程序集几乎总是包含已编译的类。
程序集不管是可执行文件还是库,结构是类似的。主要的不同点是,可执行文件定义一个入口点。
(P641)
程序集清单有两个目的 :
1. 向托管宿主环境描述程序集;
2. 到程序集中模块、类和资源的目录;
因此,程序集是自描述的。
(P642)
消费者可以发现程序集的数据、类和函数等所有内容,无需额外的文件。
程序集清单不是显式地添加到程序集的,而是作为编译的一部分自动嵌入到程序集中的。
下面总结了程序集清单中存储的主要数据 :
1. 程序集的简单名称;
2. 版本号 (AssemblyVersion) ;
3. 程序集的公共密钥和已签名的散列 (如果是强命名的) ;
4. 一系列引用的程序集,包括它们的版本和公共密钥;
5. 组成程序集的一系列模块;
6. 程序集定义的一系列类和包含每个类的模块;
7. 一组可选的由程序集要求或拒绝的安全权限 (AssemblyPermission) ;
8. 附属程序集针对的文化 (AssemblyCulture) ;
清单也可以存储以下信息数据 :
1. 完整的标题和描述 (AssemblyTitle 和 AssemblyDescription) ;
2. 公司和版权信息 (AssemblyCompany 和 AssemblyCopyright) ;
3. 显式版本 (AssemblyInformationVersion) ;
4. 自定义数据的其他属性;
这些数据有些来自提供给编译器的参数,其他的数据来自程序集属性 (括号中的内容) 。
可以利用 .NET 工具 ildasm.exe 查看程序集清单的内容。
可以利用程序集属性指定绝大部分清单内容。
这些声明通常都定义在项目的一个文件中。
Visual Studio 为此对每个新 C# 项目都在 Properties 文件夹中自动创建一个名为 AssemblyInfo.cs 的文件,预定义了一组默认的程序集属性,为进一步的自定义提供起点。
应用程序清单是一个 XML 文件,它向操作系统提供关于程序集的信息。如果存在的话,应用程序清单在 .NET 托管宿主环境加载程序集之前被读取和处理,因而可以影响操作系统如何启动应用程序的进程。
(P643)
Metro 应用有更详细的配置清单,它包含程序功能声明,它决定了操作系统所分配的权限。编辑这个文件的最简单方法是使用 Visual Studio ,双击配置清单文件就可以显示编辑界面。
可以用两种方式部署 .NET 应用程序清单 :
1. 作为程序集所在文件夹中的一个特殊命名的文件;
2. 嵌入程序集中;
作为一个单独的文件,其名称必须匹配程序集的名称,后缀为 .manifest 。
.NET 工具 ildasm.exe 对嵌入式应用程序清单的存在视而不见。但是如果在 Solution Explorer 中双击程序集, Visual Studio 会指出嵌入式应用程序清单是否存在。
程序集的内容实际上存储在一个或多个称为模块的中间容器中。
一个模块对应于一个包含程序集内容的文件。
采用额外的容器层的原因是,为了在构建包含多种编程语言中编译的代码的程序集时,允许程序集跨多个文件,这是一个很有用的特性。
(P644)
在多文件程序集中,主模块总是包含清单;其他的模块可以包含 IL 和资源。清单描述组成程序集的所有其他模块的相对位置。
多文件程序集必须从命令行编译, Visual Studio 中不支持。
为了编译程序集,需要利用 /t 开关调用 csc 编译器来创建每个模块,然后再用程序集链接器工具 al.exe 将它们链接起来。
尽管很少有需要多文件程序集的情况,即使在处理单模块程序集时,但是时常需要了解模块这一额外的容器层。主要应用场景跟反射有关。
System.Refelction 中的 Assembly 类是在运行时访问程序集元数据的入口。
有很多方式可以获得程序集对象,最简单的方式是通过 Type 的 Assembly 属性。
(P645)
也可以通过调用 Assembly 的静态方法来获得 Assembly 对象 :
1. GetExecutingAssembly —— 返回定义当前正在执行的函数的程序集;
2. GetCallingAssembly —— 跟 GetExecutingAssembly 执行相同的操作,但是针对的是调用当前正在执行的函数的函数;
3. GetEntryAssembly —— 返回定义应用程序初始入口方法的程序集;
一旦有了 Assembly 对象,就可以使用它的属性和方法来查询程序集的元数据和反射它的类。
程序集成员 :
1. FullName 、 GetName —— 返回完全限定的名称或者 AssemblyName 对象;
2. CodeBase 、 Location —— 程序集文件的位置;
3. Load 、 LoadFrom 、 LoadFile —— 手动将程序集加载到当前应用程序域中;
4. GlobalAssemblyCache —— 指出程序集是否定义在 GAC 中;
5. GetSatelliteAssembly —— 找到给定文化的卫星程序集;
6. GetType 、 GetTypes —— 返回定义在程序集中的一个或所有类;
7. EntryPoint —— 返回应用程序的入口方法,例如 MethodInfo ;
8. GetModules 、 ManifestModule —— 返回程序集的所有模块或主模块;
9. GetCustomAttributes —— 返回程序集的属性;
强命名的程序集具有唯一的、不可更改的身份。通过向清单添加以下两类元数据来实现 :
1. 属于程序集创作者的唯一编号;
2. 程序集的已签名散列,证实程序集产生的唯一编号持有者;
这需要一个 公共 / 私有 密钥对。公共密钥提供唯一的身份识别号,私有密钥帮助签名。
强名称签名不同于 Authenticode 签名。
公共密钥对于保证程序集引用的唯一性有价值 : 强命名的程序集将公共密钥合并到它的身份中。签名对于安全性有价值,它防止恶意人员篡改程序集。没有私有密钥,无法发布程序集的修改版本时不出现其签名中断 (导致加载时错误) 。
(P646)
向弱命名的程序集添加一个强名称会更改它的身份。因此,有必要一开始就给生产型程序集 (Production Assembly) 命名一个强名称。
强命名的程序集也可以注册在 GAC 中。
要给程序集命名一个强名称,首先利用实用工具 sn.exe 生成一个 公共 / 私有 密钥对。
强命名的程序集不能引用弱命名的程序集。这是要强命名所有生产型程序集的另一个重要原因。
每个程序集具有一个独立的密钥对是有利的,在以后转移某个特定应用程序 (以及它引用的程序集) 的所有权时,可以做到最小暴露。但是使得创建可以识别所有程序集的安全策略更难了,也使得验证动态加载的程序集更为困难了。
在有数百个开发人员的组织中,你可能想要限制对程序集进行签名的密钥对的访问,原因有两个 :
1. 如果密钥对泄露,你的程序集就不再是不可篡改的了;
2. 测试程序集如果已签名和泄露,就会被恶意地宣称为真正的程序集;
延迟签名的程序集用正确的公共密钥进行标记,但是没有用私有密钥签名。
(P647)
延迟签名的程序集相当于被篡改的程序集,通常会被 CLR 拒绝。
要延迟签名,需要一个只包含公共密钥的文件。
必须从命令行手动禁用程序集验证,否则,程序集将不会执行。
程序集的身份包含四种来自其清单的元数据 :
1. 它的简单名称;
2. 它的版本 (如果未指定,就是 0.0.0.0 ) ;
3. 它的文化 (如果不是卫星程序集, 就是 neutral) ;
4. 它的公共密钥标记 (如果不是强命名的, 就是 null) ;
(P648)
完全限定程序集名称是一个包含 4 个身份识别组件的字符串。
如果程序集没有 AssemblyVersion 属性,则版本显示为 “0.0.0.0” 。如果未签名,则其公共密钥标记显示为 “null” 。
Assembly 对象的 FullName 属性返回它的完全限定名称。编译器在清单中记录程序集引用时总是使用完全限定名称。
完全限定程序集名称不包含它在磁盘上的目录路径。
AssemblyName 类的完全限定程序集名称的每一个组件都具有一个类型化属性。 AssemblyName 有两个目的 :
1. 解析或构建完全限定程序集名称;
2. 存储一些额外的数据,以帮助解析 (寻找) 程序集;
可以通过以下三种方式获得 AssemblyName :
1. 实例化一个 AssemblyName ,提供完全限定名称;
2. 在一个现有 Assembly 上调用 GetName ;
3. 调用 AssemblyName.GetAssemblyName ,提供到磁盘上程序集文件的路径;
(P649)
可以不用任何参数实例化一个 AssemblyName ,然后设置它的每个属性以构建完全限定名称。以这种方式构造的 AssemblyName 是易变的。
Version 本身是一个强类型化的表示,具有 Major 、 Minor 、 Build 和版本号属性。
GetPublicKey 返回完全加密的公共密钥。
GetPublicToken 返回建立身份时使用的最后 8 个字节。
由于版本是程序集名称的一个有机部分,所以改变 AssemblyVersion 属性就会改变程序集的身份。这将影响与引用程序集的兼容性,在不间断的更新中会出现意想不到的情况。要解决这个问题,有以下两个独立的程序集级别的属性用于表示与版本相关的信息,两者都被 CLR 省略 :
1. AssemblyInformationVersion —— 显示给最终用户的版本。这在 “Windows File Properties” 对话框中作为 “Product Version” 出现。可以包含任何字符串。通常程序中的所有程序集会被分配相同的信息版本号;
2. AssemblyFileVersion —— 用于引用此程序集的构建号。这在 “Windows File Properties” 对话框中作为 “File Version” 出现。跟 AssemblyVersion 一样,它必须包含一个字符串,最多由 4 个用句点分隔的数字组成;
Authenticode 是一个代码签名系统,其目的是证明发行商的身份。
Authenticode 和强名称签名是独立的,可以用任何一个或同时用两个系统对程序集进行签名。
(P651)
如果还想对程序集进行强名称签名 (强烈推荐) ,那么必须在 Authenticode 签名之前进行强名称签名。
(P652)
最好避免对 .NET 3.5 或更早的程序集进行 Authenticode 签名。
作为安装 .NET Framework 的一部分,在计算机上创建一个中心仓库,用于存储 .NET 程序集,这就是所谓的全局程序集高速缓存 ( Global Assembly Cache , GAC) 。 GAC 包含 .NET Framework 本身的一个集中副本,并且它也可以用来集中自定义的程序集。
(P653)
对于非常大的程序集, GAC 可以缩短启动时间,因为 CLR 只需要在安装时验证一次 GAC 中程序集的签名,而不是每次加载程序集时都要验证。按百分比来说,如果用 ngen.exe 工具为程序集生成了本机映射 (选择非重叠的基地址) ,就会有这一优势。
GAC 中的程序集总是完全受信任的,即使是从运行在受限的沙箱中调用程序集。
要将程序集安装到 GAC ,第一步是给程序集命名一个强名称。
(P654)
应用程序通常不仅仅包含可执行代码,还包含诸如文本、图像或 XML 文件等内容。这些内容可以表示为程序集中的资源。资源有两个重叠的用例 :
1. 合并不能进入源代码的数据,例如图像;
2. 存储在多语言应用程序中可能需要转换的数据;
程序集资源最终是一个带有名称的字节流,可以将程序集看作包含一个按字符串排列的字节数组字典。
Framework 可以通过中间的 .resources 容器添加内容。一些容器包含可能需要转换成不同语言的内容。
(P655)
本地化的 .resources 可打包为在运行时根据用户的操作系统语言被自动挑选的单个卫星程序集。
要使用 Visual Studio 直接嵌入资源 :
1. 将文件添加到项目;
2. 将构建操作设置为 “Embedded Resource” ;
资源名称区分大小写,所以 Visual Studio 中包含资源的项目子文件夹名称也区分大小写。
(P656)
要获得一个资源,可以在包含该资源的程序集上调用 GetManifestResourceStream ,返回一个流,然后可以将其读作任何其他名字。
GetManifestResourceNames 返回程序集中所有资源的名称。
.resources 文件包含的是潜在地可本地化的内容。 .resources 文件最终是程序集中的一个嵌入式资源,就像任何其他类型的文件一样。区别在于必须 :
1. 首先将内容打包到 .resources 文件中;
2. 通过 ResourceManager 或 pack URI 而不是 GetManifestResourceStream 访问它的内容;
.resources 文件的结构形式是二进制的,所以不是可读的;因此,必须依赖于 Framework 或 Visual Studio 提供的工具来处理它们。
处理字符串或简单数据类的标准方法是使用 .resx 格式,该格式可以通过 Visual Studio 或 resgen 工具转换成 .resources 文件。
.resx 格式也适合于针对 Windows Forms 或 ASP.NET 应用程序的图像。
在 WPF 应用程序中,必须对需要由 URI 引用的图像或类似的内容使用 Visual Studio 的 “Resource” 构建操作。无论是否需要本地化,这一点都是适用的。
(P657)
.resx 文件是一种用于生成 .resources 文件的设计时格式。
.resx 文件使用 XML 通过 名 / 值 对进行构造。
要在 Visual Studio 中创建 .resx 文件,可以添加一个 “Resource File” 类的项目条目。其他工作都是自动完成的 :
1. 创建正确的头部;
2. 设计器提供用于添加字符串、图像、文件和其他类型的数据;
3. .resx 文件自动转转成 .resources 格式,并在编译时嵌入到程序集中;
4. 编写一个类用于以后访问数据;
资源设计器将图像添加为类型化的 Image 对象 (System.Drawing.dll) ,而不是作为字节数组,这使得它们不适用于 WPF 应用程序。
(P659)
可以简单地通过添加新卫星程序集而增强语言支持,无需更改主程序集。
卫星程序集不能包含可执行代码,只能包含资源。
卫星程序集部署在程序集文件夹的子目录中。
(P661)
文化分成文化和子文化。一种文化代表一种特定的语言;一种子文化代表该语言的一个地区变种。
在 .NET 中用 System.Globalization.CultureInfo 类表示文化,可以检查应用程序的当前文化。
CurrentCulture 反映 Windows 控制面板的区域设置,而 CurrentUICulture 反映操作系统的语言。
一个典型的应用程序包含一个可执行的主程序集和一组引用的库程序集。
程序集解析是指定位所引用程序集的过程。
程序集解析发生在编译时和运行时。
(P662)
在自定义程序集加载和解析方面, Metro 应用只有很少的支持。特别是,它从不支持从任意文件位置加载程序集,而且没有 AssemblyResolve 事件。
所有类都在程序集范围内。
程序集就像类的地址。
程序集组成类的运行时身份的重要部分。
程序集也是类到它的代码和元数据的句柄。
AssemblyResolve 事件允许干预和手动加载 CLR 找不到的程序集。如果处理该事件,可以在各个位置散发引用的程序集,并加载它们。
在 AssemblyResolve 事件处理程序中,通过调用 Assembly 类中三个静态方法 ( Load 、LoadFrom 或 LoadFile ) 中的一个,找到并加载程序集。这些方法返回对新加载的程序集的引用,然后再返回给调用者。
(P663)
ResolveEventArgs 事件比较特殊,因为它具有返回类。如果有多个处理程序,那么第一个返回非空 Assembly 的程序优先。
Assembly 类中的三个 Load 方法在 AssemblyResolve 处理程序内部和外部都很有用。在事件处理程序外部时,它们可以加载和执行编译时没有引用的程序集。可能会加载程序集的一个示例情况是在执行插件时。
在调用 Load 、 LoadFrom 或 LoadFile 之前慎重考虑 : 这些方法将程序集永久地加载到当前应用程序域,即使不对产生的 Assembly 对象执行任何操作。加载程序集具有一些副作用 : 它会锁定程序集文件,还会影响后续的类解析。
卸载程序集的唯一方式是卸载整个应用程序域 (另一个避免锁定程序集的方法是对检测路径的程序集执行阴影拷贝 (shadow copying)) 。
如果只想检查一个程序集,不想执行它的任何代码,那么可以加载到只反射上下文中。
要从完全限定名称 (不带位置) 加载程序集,可调用 Assembly.Load 。这指示 CLR 使用普遍自动解析系统寻找程序集。 CLR 本身使用 Load 寻找所引用的程序集。
要从文件名加载程序集,可调用 LoadFrom 或 LoadFile 。
要从 URI 加载程序集,可调用 LoadFrom 。
要从字节数组加载程序集,可调用 Load 。
通过调用 AppDomain 的 GetAssemblies 方法,可以看到哪些程序集当前被加载到内存中。
LoadFrom 和 LoadFile 都可以从文件名加载程序集。它们有两点区别。首先,如果有一个相同身份的程序集从另一个位置加载到了内存中,那么 LoadFrom 提供前一副本。
LoadFile 提供新副本。
但是,如果从同一位置加载了两次,那么两种方法都提供前一次已缓存的副本。
相反,从同一字节数组两次加载一个程序集,会提供两个不同的 Assembly 对象。
(P664)
在内存中,来自 2 个相同程序的类型是兼容的,这是避免加载重复程序集的主要原因,也是尽量使用 LoadFrom 而不使用 LoadFile 的原因。
LoadFrom 和 LoadFile 的另一个区别是, LoadFrom 会告诉 CLR 前向引用的位置,而 LoadFile 则不会。
如果直接在代码中引用一个类型,那么就称为静态引用 (statically referencing) 该类型。编译器会将该类型的引用添加到正在编译的程序集中,以及包含该类型的程序集名称 (但是不包含如何在运行时寻找该类型的信息) 。
在解析静态引用时, CLR 会先检查 GAC ,然后检查检测路径 (通常是应用的基目录) ,最后触发 AssemblyResolve 事件。但是,在这些操作之前,它会先检查程序集是否已经加载。然而,它只考虑以下情况的程序集 :
1. 已经从一个路径加载,否则就会出现在自己的路径上 (检测路径) ;
2. 已经从 AssemblyResolve 事件的响应中加载;
在调用 LoadFrom / LoadFile 时必须非常小心,要先检查程序集是否已经存在于应用的基目录 (除非确实想加载同一个程序集的多个版本) 。
(P665)
如果在 AssemblyResolve 事件响应中加载,则不存在这个问题 (无论是使用 LoadFrom 、 LoadFile 或后面将会介绍的从字节数组加载), 因为事件只触发检测路径之外的程序集。
无论使用 LoadFrom 还是 LoadFile , CLR 都一定会先在 GAC 中查找所请求的程序集。
使用 ReflectionOnlyLoadFrom (它会将程序加载到只有反映的环境中), 可以跳过 GAC 。
程序集的 Location 属性通常会返回其在文件系统的物理位置 (如果有) 。
而 CodeBase 属性则以 URI 形式映射这个位置。
如果要寻找程序集在磁盘的位置,只使用 Location 是不可靠的。更好的方法是同时检查两个属性。
【第19章】
(P670)
在运行时检查元数据和编译代码的操作称为 “反射” 。
System.Type 的实例代表了类型的元数据。因为 Type 的应用领域非常广泛,所以它存在于 System 命名空间中,而非 System.Reflection 命名空间中。
通过调用对象上的 GetType 或者使用 C# 的 typeof 运算符,可以获得 System.Type 实例。
(P671)
还可以通过名称获取类型。如果引用了该类型的程序集。
如果没有程序集对象,可以通过其程序集限定名称获取类型 (该类型的全称会带有程序集完整的限定名称)。
一旦拥有了 System.Type 对象,就可以使用它的属性访问类型的名称、程序集、基础类型、可见性等。
一个 System.Type 实例就是打开类型 (及其定义的程序集) 的全部元数据的一个入口。
System.Type 是个抽象的概念,因此实际上 typeof 运算符获得的是 Type 子类。对于 mscorlib 来说, CLR 使用的这些子类都是内部的,称为 RuntimeType 。
Metro 应用模板隐藏了大多数类型成员,转而将它们封装在 TypeInfo 类中。调用 GetTypeInfo ,就可以得到这个类。
完整的 .NET 框架也包含 TypeInfo ,所以能在 Metro 中正常运行的代码也可以在标准库 .NET 应用中运行,但是只适用于 Framework 4.5 (旧版本不支持) 。
(P672)
TypeInfo 还包含其他一些反射成员的属性和方法。
Metro 应用只实现了有限的反射机制。特别是它们无法访问非公共成员类型,也无法使用 Reflection.Emit 。
可以将 typeof 和 GetType 与数组类型一起使用。还可以通过调用元素类型上的 MakeArrayType 获取数组类型。
可以向 MakeArray 传递整型参数,以创建多维矩形数组。
GetElementType 返回数组的元素类型。
GetArrayRank 返回矩形数组的维数。
要重新获得嵌套类型,可调用包含类型的 GetNestedTypes 。
在使用嵌套类型时需要特别注意的是 CLR 会认为嵌套类型拥有特定 “嵌套” 可访问等级。
类型具有 Namespace 、 Name 和 FullName 特性。在大多数情况中, FullName 是前两者的组合。
Type 还具有 AssemblyQualifiedName 特性,使用它可以返回带有逗号和其程序集完整名称的 FullName 值。同样可以将该字符串传递给 Type.GetType ,然后会在默认的加载环境中单独获取类型。
(P673)
对于嵌套类型来说,包含类型仅在 FullName 中出现。
+ 表示将包含类型与嵌套的命名空间区分开。
泛型类型名称带有‘后缀,还带有类型参数的编号。如果泛型类型被绑定,那么该法则同时应用于 Name 和 FullName 。
然而,如果该泛型类型是封闭式的, FullName (仅仅) 获得基本的额外附加信息。
数组通过在 typeof 表达式中使用的相同后缀表示。
指针类型也与数组类似。
描述 ref 和 out 参数的类型带有 & 后缀。
(P674)
类型可以公开 BaseType 特性。
GetInterfaces 方法会返回类型实现的接口。
反射为 C# 的静态 is 运算符提供了两种等价的动态运算符 :
1. IsInstanceOfType —— 可以接收类型和实例;
2. IsAssignableFrom —— 可以接收两个类型;
可以使用两种方法通过对象的类型动态地实例化对象 :
1. 调用静态 Activator.CreateInstance 方法;
2. 调用 ConstructorInfo 对象上的 Invoke , ConstructorInfo 对象是通过调用类型 (高级环境) 上的 GetConstructor 获得的;
Activator.CreateInstance 可以接收已传递到构造方法的 Type 和可选的参数。
(P675)
使用 CreateInstance 可以设定许多其他选项,如用于加载类型的程序集、目标应用程序域和是否与非全局构造方法绑定。如果运行时无法找到适当的构造方法,那么会抛出 MissingMethodException 。
当参数值无法在重载的构造方法之间消除时,必须调用 ConstructorInfo 上的 Invoke 。
当类型不明确时,应该将一个 null 参数传递给 Activator.CreateInstance 。在这种情况需要使用 ConstructorInfo 进行替换。
在构造对象时进行动态实例化会增加几微妙的时间。相对而言这是一个较长的时间,因为 CLR 实例化对象的速度非常快 (在小型类上简单的 new 操作不足十纳秒) 。
要根据元素类型动态实例化数组,应首先调用 MakeArrayType 。
(P676)
Type 可以代表封闭式或未绑定的泛型类型。
在编译时,封闭式泛型类型可以实例化,而未绑定的类型不能实例化。
MakeGenericType 方法可以将未绑定的泛型类型转换为封闭式泛型类型。只需传递需要的类型参数就可以实现。
使用 GetGenericTypeDefinition 方法可以实现相反的操作。
当 Type 为泛型时, IsGenericType 会返回 true ,而当泛型类型为未绑定时, IsGenericTypeDefinition 会返回 true 。
GetGenericArguments 可以为封闭式泛型类型返回类型参数。
对于未绑定的泛型类型来说, GetGenericArguments 会返回在泛型类型定义中指定为占位符类型的伪类型。
在运行时,所有泛型类型不是未绑定的就是封闭式的。
在 typeof(Foo<>) 这类表达式中泛型类型是未绑定的 (相对来说这种情况比较常见);在其他情况中,泛型类型是封闭式的。
在运行时不存在开放式泛型类型 : 所有开放式类型都会被编译器关闭。
(P677)
使用 GetMembers 方法可以返回类型的成员。
TypeInfo 提供了另一个 (更简单的) 成员反射协议。这个 API 对于目标平台为 Framework 4.5 的应用是可选的,而 Metro 应用则是强制选择的,因为 Metro 应用没有与 GetMethods 方法等价的方法。
TypeInfo 并没有像 GetMethods 这样可以返回数组的方法,而只有返回 IEnumerable
如果在调用时没有使用参数, GetMembers 会返回类型 (及其基本类型) 的所有公共成员。
GetMember 通过名称检索特定成员,但是因为成员可能会被重新加载, GetMember 仍旧会返回一个数组。
(P678)
MemberInfo 也具有 MemberTypes 类型的 MemberType 特性。
下面列出的是该特性的典型值 : All 、 Custom 、 Field 、 NestedType 、 TypeInfo 、 Constructor 、 Event 、 Method 、 Property ;
当调用 GetMembers 时,可以传递一个 MemberTypes 实例,以限定它返回的成员类型。还可以通过调用 GetMethods 、 GetFields 、 GetProperties 、 GetEvents 、 GetConstructors 或 GetNestedTypes ,限定返回的结果。这些方法还有专门用于特定成员的版本。
对类型的成员进行检索时应尽可能地具体,因而如果要在以后添加成员,就无需拆分代码。如果要通过名称检索方法,指定所有参数类型可以确保出现方法重载时,代码仍旧可以运行。
MemberInfo 对象具有 Name 特性和以下两个 Type 特性 :
1. DeclaringType —— 返回定义该成员的类型;
2. ReflectedType —— 根据所调用种类的 GetMembers 返回类型;
当根据基础类型定义的成员进行调用时,会出现两种不同情况 : DeclaringType 会返回基础类型;而 ReflectedType 会返回子类型。
(P679)
MemberInfo 还定义了用于返回自定义属性的方法。
MemberInfo 本身在成员中不重要,因为它是类型的概要基础。
可以根据 MemberInfo 的 MemberType 特性,将 MemberInfo 投射到其子类型上。如果通过 GetMethod 、 GetField 、 GetProperty 、 GetEvent 、 GetConstructor 或 GetNestedType (或者它们的复数版本) 获取成员,就不必进行投射。
(P680)
每个 MemberInfo 子类都具有大量特性和方法,以便公开成员元数据的可见性、修饰符、泛型类型参数、参数、返回类型和自定义属性。
有些 C# 构造 (即索引器、枚举、运算符和终止器) 在涉及 CLR 时就被设计出来了。尤其应该注意以下几点 :
1. C# 索引器可以转换为接收一个或多个参数的特性,而且可以标识为类型的 [DefaultMembber] ;
2. C# 枚举可以通过每个成员的静态域转换为 System.Enum 的子类型;
3. C# 运算符可以转换为被特殊命名的静态方法,而且带有 “op_” 前缀;
4. C# 析构函数可以转换为覆盖 Finalize 的方法;
另一种复杂的情况是特性或事件实际上由两部分组成 :
1. 描述特性或事件的元数据 (由 PropertyInfo 或 EventInfo 封装) ;
2. 一个或两个反向方法 (backing Method) ;
在 C# 程序中,反向方法被封装在特性或事件定义中。但是当将它们编译为 IL 时,反向方法会被表示为原始方法,而且可以像其他方法那样调用。这意味着 GetMethods 会返回与原始方法并列的特性和事件反向方法。
(P681)
既可以为未绑定的泛型类型获取成员元数据,也可以为封闭式泛型类型获取成员元数据。
从未绑定的和封闭式泛型类型返回的 MemberInfo 对象总是独特的,即使对于签名中不带泛型类型参数的成员也是如此。
未绑定泛型类型的成员不能被动态调用。
(P682)
一旦拥有了 MemberInfo 对象,就可以动态地调用它或者 获取 / 设置 它的值。这种操作称为动态绑定或后期绑定,因为要在运行时选择调用成员,而不是在编译时选择调用成员。
使用 GetValue 和 SetValue 可以获取和设置 PropertyInfo 或 FieldInfo 的值。
要动态调用方法 (如在 MethodInfo 上调用 Invoke) ,应为该方法提供一组参数。如果参数类型错误,那么在运行时就会出现异常。在进行动态调用时,会失去编译时的类型安全,但是仍旧可以拥有运行时的类型安全 (就像使用 dynamic 关键字一样) 。
(P688)
通过调用 Assembly 对象上的 GetType 或 GetTypes ,可以动态反射程序集。
GetTypes 仅会返回顶级类型和非嵌套类型。
(P694)
System.Reflection.Emit 命名空间含有用于在运行时创建元数据和 IL 的类。
(P697)
IL 中没有 while 、 do 和 for 循环;这些循环是通过标签、相等 goto 和条件 goto 语句实现的。
(P698)
new 等价于 IL 中的 Newobj 操作码。
【第20章】
(P718)
C# 依靠动态语言运行时 (DLR) 执行动态绑定。
Framework 4.0 是第一个带有 DLR 的 Framework 版本。
(P719)
每种对动态绑定提供支持的语言都会提供专门的绑定器,以帮助 DLR 以专门方式为该语言解释表达式。
(P724)
C# 的静态类型化严格说是一把双刃剑。一方面,它在编译时保证程序的正确性。另一方面,它偶尔会导致编码困难或无法使用代码进行表述,在这种情况中必须使用反射,动态绑定比反射更清晰、更快速。
(P726)
对象可以通过实现 IDynamicMetaObjectProvider 提供其绑定语义 (或者通过子类化 DynamicObject 更容易地提供其绑定语义, DynamicObject 提供了对该接口的默认实现) 。
(P729)
真正的动态语言 (如 IronPython 和 IronRuby) 确实允许执行随机字符串。而且该功能对一些任务 (如编写脚本、动态配置和实现动态规则引擎) 很有用。
【第21章】
(P731)
.NET 中的权限提供了一个独立于操作系统的安全层。其功能有两部分 :
1. 沙箱 —— 限制不能完全可信的 .NET 程序集可以执行的操作类型;
2. 授权 —— 限制谁可以做什么;
通过 .NET 中支持的加密功能可以存储或交换机密、防偷听、检测信息篡改、为存储密码生成单向哈希表和创建数字签名。
Framework 对沙箱和授权都使用权限。权限根据条件阻止代码的执行。沙箱使用代码访问权限;授权使用身份和角色权限。
代码访问安全最常通过 CLR 或托管环境 (如 ASP.NET 或 Internet Explorer) 对你进行限制,而授权通常由你实现,以防止未授权的调用程序访问你的程序。
(P732)
身份和角色安全主要用于编写中间层应用程序和网页应用服务。通常可以对一组角色进行决定,然后对于提供的每个方法,可以要求调用程序为特定角色。
(P738)
为了帮助避免特权提升攻击,默认情况下 CLR 不允许部分可信的程序集调用完全可信的程序集。
(P753)
System.Security.Cryptography 中的大多数类型位于 mscorlib.dll 和 System.dll 中。 ProtectedData 是一个例外,它位于 System.Security.dll 中。
(P754)
散列法提供了一种加密方式。这种加密方式非常适用于存储数据库中的密码,因为不需要 (或不想要) 看到解密的版本。要进行验证,仅需散列用户输入的信息,然后将其与数据库中存储的信息相比较即可。
不论源数据的长度有多少,散列编码永远为较小的固定大小。这使其在比较文件或检查数据流 (与校验和不同) 时发挥重要作用。源数据中更改任何位置的单个位都会使得散列编码发生巨大的变化。
要进行散列操作,可调用 HashAlgorithm 某个子类 (如 SHA256 或 MD5) 上的 ComputeHash 。
ComputeHash 方法还可以接收字节数组,这对散列法密码非常方便。
Encoding 对象上的 GetBytes 方法将一个字符串转换为一个字节数组; GetString 方法将该数组重新转换为字符串。然而, Encoding 对象无法将加密的或散列的字节数组转换为字符串,因为编码数据通常会破坏文本编码规则。可以使用下列 Convert.ToBase64String 方法和 Convert.FromBase64String 方法代替。这些方法可以使用字节数组和合法 (与 XML 友好) 的字符串相互转换。
MD5 和 SHA256 是 HashAlgorithm 的两个子类型,它们是由 .NET Framework 提供的。下面按照安全等级的升序 (和以字节为单位的散列长度) 列出了主要算法 :
MD5(16) -> SHA1(20) -> SHA256(32) -> SHA384(48) -> SHA512(64)
算法的长度越短,其执行速度就越快。
MD5 的执行速度比 SHA512 的执行速度快 20 多倍,而且非常适合计算文件的校验和。
使用 MD5 每秒钟可以加密数百兆字节,然后将结果存储到 Guid 中 (Guid 的长度恰好为 16 字节,而且作为一个值类型它比字节数组更易于处理) 。然而,较短的散列会增加破解密码的可能性 (两个不同的文件生成相同的散列) 。
在加密密码或其他区分安全等级的数据时,至少应该使用 SHA256 。人们认为在这些情况中使用 MD5 、 SHA1 是不安全的, MD5 和 SHA1 仅适用于防止意外破解,而无法防止有预谋的篡改。
SHA384 的执行速度并不快于 SHA512 的执行速度,如果需要获取比 SHA256 更高的安全性,可以使用 SHA512 。
较长的 SHA 算法适用于密码加密,但是它们需要增强密码策略的强度以减弱字典攻击的威胁 (字典攻击是指攻击者通过对字典中的每个词应用散列算法,创建密码查询表的攻击策略) 。
(P755)
Rfc2898DeriveBytes 和 PasswordDeriveBytes 类可以准确地执行这类增加密码长度的任务。
Framework 还提供了 160 位的 RIPEMD 散列算法,其安全性比 SHA1 稍好。但是,它会受到 .NET 低效实现的影响,这使得其执行速度比 SHA512 的执行速度更慢。
对称加密在加密和解密时使用相同的密钥。 Framework 提供了 4 种对称加密算法,这些算法中 Rijndael 是最方便的。 Rijndael 既快速又安全,而且拥有两个实现 :
1. Rijndael 类,从 Framework 1.0 之后的版本可以使用它;
2. Aes 类,它是在 Framework 3.5 中引入的;
除了 Aes 不允许通过更改块尺寸消弱密码外,这两个类几乎相同。
Aes 是 CLR 安全团队推荐使用的类。
(P756)
各个类使用不同的密码系统。 Aes 使用数据密码系统,通过 encryptor 和 decryptor 转换应用密码算法;
CryptoStream 使用数据流加密算法,用于数据流加密。可以使用不同的对称算法替换 Aes ,而仍旧需要使用 CryptoStream 。
CryptoStream 是双向的,因此可以根据是选择 CryptoStreamMode.Read 还是 CryptoStreamMode.Write ,读取数据流或向数据流中写入信息。加密机和解密机都是对读和写的理解,这生成了 4 种组合,这些选择可能使人感到茫然!将读取创建为 “拉” 模型和将写入创建为 “推” 模型可以帮助理解。如果仍旧有疑问,可以将加密的写入和解密的读取作为起点;这通常是最常见的方式。
使用 System.Cryptography 中的 RandomNumberGenerator 可以生成随机密钥或 IV 。实际上它生成的数字是无法预测的或具有密码强度的 (System.Random 类没有提供相同的保证) 。
使用 MemoryStream 完全可以在内存中进行加密和解密。
(P757)
CryptoStream 是一个链接器,它可以将其他流链接起来。
(P759)
公共密钥加密是非对称的,因此加密和解密使用不同的密钥。
(P760)
.NET Framework 提供了许多非对称算法,其中 RSA 是最流行的算法。
【第22章】
(P763)
同步 (Synchronization) 是指协调并发操作,实现可预测的结果。如果有多个线程访问相同的数据,那么同步就非常重要;这个应用领域很容易出现问题。
(P764)
排他锁结构有三种 : lock 语句、 Mutex 和 SpinLock 。 lock 是最方便和最常用的结构 :
1. Mutex 可以跨越多个进程 (计算机范围的锁) ;
2. SpinLock 实现了微优化,可以减少高度并发场景的上下文切换;
(P765)
事实上, C# 的 lock 语句是 Monitor.Enter 和 Monitor.Exit 方法调用及 try / finally 语句块的简写语法。
如果未先调用同一个对象的 Monitor.Enter ,而直接调用 Monitor.Exit ,就会抛出异常。
(P766)
为访问任意可写共享域的代码添加锁。即使是最简单的情况 (如某个域的赋值操作) ,也必须考虑同步问题。
(P768)
如果 lock 语句块中抛出异常,则可能破坏通过锁实现的原子操作。
线程可以用嵌套 (重入) 的方式重复锁住同一个对象。
在这些情况中,只有当最外层 lock 语句退出时,或者执行相同数量的 Monitor.Exit 语句,对象才会解除锁。
(P769)
如果两个线程互相等待对方所占用的资源,就会形成死锁,使得双方都无法继续执行。
死锁是多线程中最难解决的问题 —— 特别是其中涉及许多相关对象时。基本上,最难的问题是无法确定调用获取了哪些锁。
(P770)
锁的执行速度很快 : 在目前的计算机上,如果未出现争夺者,那么一般可以在 80 纳秒内获得和释放一个锁;如果出现争夺者,那么相应的上下文切换会将过载增加到毫秒级,但是这个时间远远小于线程的实际调度时间。
Mutex 类似于 C# 的锁,但是它可以支持多个进程。换而言之, Mutex 可用于计算机范围或应用程序范围。
获得和释放一个无争夺的 Mutex 只需要几毫秒 —— 时间比锁操作慢 50 倍。
使用一个 Mutex 类,就可以调用 WaitOne 方法获得锁,或者调用 ReleaseMutex 释放锁。关闭或去掉一个 Mutex 会自动释放互斥锁。与 lock 语句一样, Mutex 只能在它所在的线程上释放。
(P771)
线程安全性主要是通过锁和减少线程交互可能性而实现。
(P789)
从 Framework 4.0 开始,我们可以使用 Lazy
(P793)
Suspend 和 Resume 可以冻结和解冻另一个线程。虽然在概念上与阻塞不同 (可以通过它的 ThreadState 查询) ,但是冻结的线程就像进入了阻塞状态。与 Interrupt 一样, Suspend / Resume 也缺少有效的用例,并且也可能存在危险;如果暂停一个获得了锁的线程,那么其他线程就无法获得这个锁 (包括自己的锁) ,这使得程序很容易发生死锁。因此, Framework 2.0 废弃了 Suspend 和 Resume 。
(P794)
.NET Framework 提供了四种定时器,以下两种是通用的多线程定时器 :
1. System.Threading.Timer ;
2. System.Timers.Timer ;
其他两种是特殊用途的单线程定时器 :
1. System.Windows.Forms.Timer (Windows Forms 定时器) ;
2. System.Windows.Threading.DispatcherTimer (WPF 定时器) ;
多线程定时器更加强大、精确和灵活,而在运行需要更新 Windows Forms 控件或 WPF 元素的简单任务时,单线程定时器更加安全和方便。
System.Threading.Timer 是最简单的多线程定时器,它只有一个构造方法和两个方法。
(P795)
.NET Framework 提供另一个与 System.Timers 命名空间中名称相同的定时器类。它简单地封装了 System.Threading.Timer ,在使用完全相同的底层引擎时更加方便。
(P796)
单线程定时器不能在各自环境之外使用。
【第23章】
(P797)
Parallel 类和任务并行结构统称为任务并行库 (Task Parallel Library , TPL) ;
(P798)
通过编程方式利用多内核或多处理器称为并行编程,它是多线程更宽泛概念的一个子集。
(P799)
PFX (Parallel Framework , 并行框架) 主要用于并行编程 : 利用多内核处理器加快计算密集型代码的执行速度。
PLINQ 将自动并行化本地的 LINQ 查询。 PLINQ 的优势是易于使用,因为它把工作划分和结果整理的任务转给了 Framework 。
要使用 PLINQ ,只要在输入序列上调用 AsParallel() 方法,然后继续执行 LINQ 查询。
(P800)
AsParallel 是 System.Linq.ParallelEnumerable 中的一个扩展方法。它基于 ParallelQuery
对于接受两个输入序列的查询运算符 (Join 、 GroupJoin 、 Concat 、 Union 、 Intersect 、 Except 和 Zip) ,必须对这两个输入序列应用 AsParallel() 方法,否则将抛出异常。但不需要在查询进行时一直对它应用 AsParallel ,因为 PLINQ 的查询运算符输出另一个 ParallelQuery 序列。事实上,再次调用 AsParallel 会降低效率,因为它会强制合并和重新划分查询。
PLINQ 仅用于本地集合 : 它不能与 LINQ to SQL 或 Entity Framework 一起使用,因为在这种情况下, LINQ 会转换为 SQL ,然后在数据库服务器上执行。然而,可以使用 PLINQ 基于从数据库查询获得的数据集来执行另外的本地查询。
(P801)
大多数 LINQ to Objects 查询执行速度很快,不仅没有必要并行化,而且划分、整理和协调额外线程的开销实际上会降低执行速度。
和普通的 LINQ 查询一样, PLINQ 查询也是延迟求值的。
(P804)
因为 PLINQ 在并行线程上运行查询,必须注意不能执行非线程安全的操作。
(P806)
PLINQ 的优点之一是,它能够方便地把来自并行工作的结果整理到一个输出序列中。但有时,结束时要做的全部工作就是让序列在每个元素上运行一些函数。
如果这是实情,而且可以忽略元素被处理的顺序,使用 PLINQ 的 ForAll 方法可以提高效率。
ForAll 方法在 ParallelQuery 的每个输出元素上运行一个委托。它正确关联到 PLINQ 的内部,省略了整理和枚举结果的步骤。
(P807)
整理和枚举结果不是复杂的大型操作,因此当存在大量快速执行的输入元素时, ForAll 优化能够获得最佳效果。
PLINQ 有三种用于给线程指派输入元素的划分策略 : 块划分、范围划分、哈希划分;
哈希划分效率相对较低,因为它必须预先计算每个元素的哈希代码,才能在同一线程上处理带有相同哈希代码的元素。如果觉得这样做太慢,唯一的选择就是调用 AsSequential 来禁用并行化。
概括地说,范围划分用于较长的序列,而且当每个元素花费的 CPU 时间大致相等时速度更快。否则,块划分的速度一般更快。
(P816)
Task.Run 可以创建和启动一个 Task 或 Task
【第24章】
(P833)
应用域是指运行中的 .NET 程序所在的独立区域。它提供了一个可控内存区域作为程序集和相关配置的容器,同时划定分布式程序的交互区域。
每个 .NET 进程通常拥有一个应用域 : 默认域。默认域在进程开始时由 CLR 自动创建。可以为应用程序建立额外的应用域,并且额外的应用域可以提供隔离,而且与单独的进程相比,降低额外系统开销和交互复杂性。它也可以应用于加载测试、应用程序补丁和运行稳定性错误恢复机制中。
通常情况下,进程的应用域是在用户双击可执行文件或者启动一个系统服务程序的时候,由操作系统建立的。
但是,通过 CLR 的整合,互联网信息服务进程 (IIS) 和数据库服务进程 (SQL) 等也可以拥有应用域。
对于简单应用程序,进程和默认域同时结束运行。但是对于 IIS 和 SQL ,进程控制着 .NET 应用域的生命周期,在合适的时候生成应用域和销毁应用域。
在进程中,可以通过调用静态方法 AppDomain.CreateDomain 和 AppDomain.Unload 创建和销毁应用域。
谨记 : 当由 CLR 在程序开始时创建的应用域即默认域销毁时,应用程序关闭并且销毁该程序其他所有应用域。通过 AppDomain 属性 IsDefaultDomain ,可以确定应用域是否是默认域。
(P834)
ApplicationBase 属性控制应用域的根文件夹,该根文件夹指定了自动检测程序集时的范围。默认域的根文件夹是主要的可执行文件所在的文件夹。对于创建的新应用域,其根文件夹可根据需要任意选取。
(P839)
应用域可以通过命名管道共享数据。
(P840)
管道在其第一次使用的时候被建立。
进程化是指通过委托在其他应用域内实例化对象,这是与其他应用域交互最灵活的方法。
【第25章】
(P844)
P / Invoke 是平台调用服务 (Platform Invocation Services) 的简称,允许访问未托管 DLL 中的函数、构件和回调。
通过在该函数的定义中添加 extern 关键字和 DllImport 属性,可以将该函数定义或一个同名的静态方法,从而在程序中直接调用。
CLR 中包括一个封送器,可以实现 .NET 类型和非托管类型的相互转换。
IntPtr 是一个用来封装非托管句柄的结构,在 32 位平台下,它的位宽是 32 位;在 64 位平台下,它的位宽是 64 位。
(P845)
在 .NET 程序内,仍然有多种类型可以选择。以非托管句柄为例,可以映射为 IntPtr 类型、 int 类型、 uint 类型、 long 类型和 ulong 类型。
大多数情况下非托管句柄封装一个地址或者指针,因此必须转换成一个 IntPtr 类型以匹配 32 位和 64 位的系统。一个典型的示例是 HWND 句柄。
(P846)
如果不能确定怎样调用一个 Win32 方法,通常可以通过搜索方法的名字和 DllImport ,在网络上找到相关的示例。
(P847)
P / Invoke 层作为在托管和非托管代码中一个固有的编程模型,对两者相关的结构映射起到了很大作用。 C# 不但可以调用 C 函数,而且可以作为 C 函数的回调函数,前提是 P / Invoke 层需要映射非托管函数指针到托管代码空间的的合法结构。托管代码中的委托等同于一个指针,因此 P / Invoke 层会将 C# 中的委托与 C 中的指针相互映射。
(P854)
.NET 程序对 COM 对象都有特殊的支持,使得 COM 程序可以在 .NET 程序中调用,反之亦然。 C# 5.0 和 CLR 4.0 增强了在 .NET 中部署和使用 COM 的功能。
(P855)
某种程度上来说, .NET 程序是在 COM 规则上进化而来的 : .NET 平台有助于跨语言开发并且允许二进制组件的更新而不影响依赖于该组件的程序正常运行。
【第26章】
(P861)
正则表达式可以对字符串进行模式化识别。 .NET 中的正则表达式规范是基于编程语言 Perl 5 的,并且支持查找替换功能。
正则表达式一般用于处理下列问题 :
1. 判定输入字符是否是密码或者手机号;
2. 将文本数据转换成结构化形式;
3. 替换文档中固定形式的文本;
一个常用的正则表达式运算符是量词。量词 “?” 表示前面的字符出现一次或者零次。换句话说, “?” 表示前面的字符是可选的。前面的字符可以是单个字符,也可以是放在方括号内的由多个字符构成的复杂结构。
(P862)
Regex.Match 方法可以搜索大型字符串。它返回的对象具有匹配的长度、索引位和匹配的真实值等属性。
可以将 Regex.Match 方法认为是字符串索引方法 IndexOf 的增强版。不同的是 Regex.Match 搜索的是一种模式而非普通字符串。
IsMatch 方法是 Match 的一种捷径,它首先调用 Match 方法,然后判断返回对象的 Success 属性。
默认状态下,正则表达式引擎按照字符串从左到右的顺序进行匹配,所以返回的是左起第一个匹配字符串。可以使用 NextMatch 方法返回更多的匹配值。
Matches 方法通过数组返回所有的匹配值。
另一个常见的正则表达式运算符是交替符,用一个竖线表示 —— “|” 。交替符前后的表达式是可选的。
圆括号将可选的表达式同其他表达式分隔开。
(P863)
Regex 实例是不可更改的。
正则表达式匹配引擎是很快的,就算没有编译,一个简单的匹配也用不了一毫秒。
RegexOptions 标志可以控制正则表达式匹配的行为。
(P864)
当要查找的串中含有元字符,需要在元字符前加反斜杠。
(P865)
\d 表示一个十进制数字,所以 \d 可以匹配任何数字。 \D 表示非数字。
\w 表示一个单词字符,包括字母、数字和下划线。 \W 表示非单词字符,可以用于表示非英语字母。
. 匹配所有字符,除了 \n (但是包括 \r ) 。
如果将 \d 、 \w 、 . 与量词一起使用,可以得到很多的变化。
(P867)
锚点 ^ 和 $ 代表确定的位置,默认表示 :
1. ^ —— 匹配字符串的开头;
2. $ —— 匹配字符串的结束;
(P868)
\b 常用来匹配整个单词。
(P870)
Regex.Replace 方法与 string.Replace 的功能类似,不过它使用正则表达式进行查找。
(P871)
静态的 Regex.Split 方法是 string.Split 方法加强版,它使用了正则表达式替换了分隔符的模式。