admin管理员组

文章数量:1568352

《果壳中的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<TSource> 类型的集合转换成一个数组、清单、字典、检索或者序列,这种方法包括 : ToArray 、 ToList 、 ToDictionary 、 ToLookup 、 AsEnumerable 、 AsQueryable ;

[集合] --> [单个元素或标量值]

1. 元素运算符 —— 从集合中取出单个特定的元素,使用的运算符有 : First 、 FirstOrDefault 、 Last 、 LastOrDefault 、 Single 、 SingleOrDefault 、 ElementAt 、 ElementAtOrDefault 、 DefaultIfEmpty ;

2. 聚合方法 —— 对集合中的元素进行某种计算,然后返回一个标量值 (通常是一个数字) 。使用的运算符有 : Aggregate 、 Average 、 Count 、 LongCount 、 Sum 、 Max 、 Min ;

3. 数量词 —— 一种返回 true 或者 false 的聚合方法,使用的运算符有 : All 、 Any 、 Contains 、 SequenceEqual ;

(P332)

[空] --> [集合]

第三种查询运算符不需要输入但可以输出一个集合。

生成方法 —— 生成一个简单的集合,使用的方法有 : Empty 、 Range 、 Repeat ;

(P333)

经过各种方法的筛选,最终得到的序列中的元素只能比原始序列少或者相等,绝不可能比原始序列还多。在筛选过程中,集合中的元素类型及元素值是不会改变的,和输入时始终保持一致。

如果和 let 语句配合使用的话,Where 语句可以在一个查询中出现多次。

(P334)

标准的 C# 变量作用域规则同样适用于 LINQ 查询。也就是说,在使用一个查询变量前,必须先声明,否则不能使用。

Where 判断选择性地接受一个 int 型的第二参数。这个参数用于指定输入序列中特定位置上的元素,在查询中可以使用这个数值进行元素的筛选。

下面几个关键字如果用在 string 类型的查询中将会被转换成 SQL 中的 LIKE 关键字 : Contains 、 StartsWith 、 EndsWith 。

Contains 关键字仅用于本地集合的比较。如果想要比较两个不同列的数据,则需要使用 SqlMethods.Like 方法。

SqlMethods.Like 也可以进行更复杂的比较操作。

在 LINQ to SQL 和 EF 中,可以使用 COntains 方法来查询一个本地集合。

如果本地集合是一个对象集合或其他非数值类型的集合,LINQ to SQL 或者 EF ,也可能把 Contains 关键字翻译成一个 EXISTS 子查询。

(P335)

Take 返回集合的前 n 个元素,并且放弃其余元素;Skip 则是跳过前 n 个元素,并且返回其余元素。

在 SQL Server 2005 中,LINQ to SQL 和 EF 中的 Take 和 Skip 运算符会被翻译成 ROW_NUMBER 方法,而在更早的 SQL Server 数据库版本中则会被翻译成 Top n 查询。

TakeWhile 运算符会遍历输入集合,然后输出每个元素,直到给定的判断为 false 时停止输出,并忽略剩余的元素。

SkipWhile 运算符会遍历输入集合,忽略判断条件为真之前的每个元素,直到给定的判断为 false 时输出剩余的元素。

在 SQL 中没有与 TakeWhile 和 SkipWhile 对应的查询方式,如果在 LINQ-to-db 查询中使用,将会导致一个运行时错误。

(P336)

Distinct 的作用是返回一个没有重复元素的序列,它会删除输入序列中的重复元素。在这里,判断两个元素是否重复的规则是可以自定义的,如果没有自定义,那么就使用默认的判断规则。

因为 string 实现了 IEnumerable<char> 接口,所以我们可以在一个字符串上直接使用 LINQ 方法。

在查询一个数据库时, Select 和 SelectMany 是最常用的连接操作方法;对于本地查询来说,使用 Join 和 Group 的效率最好。

在使用 Select 时,通常不会减少序列中的元素数量。每个元素可以被转换成需要的形式,并且这个形式需要通过 Lambda 表达式来定义。

(P337)

在条件查询中,一般不需要对查询结果进行映射,之所以要使用 select 运算符,是为了满足 LINQ 查询必须以 select 或者 group 语句结尾的语法要求。

Select 表达式还接受一个整型的可选参数,这个参数实际上是一个索引,使用它可以得到输入序列中元素的位置。需要注意的是,这种参数只能在本地查询中使用。

可以在 Select 语句中再嵌套 Select 子句来构成嵌套查询,这种嵌套查询的结果是一个多层次的对象集合。

(P338)

内部的子查询总是针对外部查询的某个元素进行。

Select 内部的子查询可以将一个多层次的对象映射成另一个多层次的对象,也可以将一组关联的单层次对象映射成一个多层次的对象模型。

在对本地集合的查询中,如果 Select 语句中包含 Select 子查询,那么整个查询是双重的延迟加载。

子查询的映射在 LINQ to SQL 和 EF 中都可以实现,并且可以用来实现 SQL 的连接功能。

(P339)

我们将查询结果映射到匿名类中,这种映射方式适用于查询过程中暂存中间结果集的情况,但是当需要将结果返回给客户端使用的时候,这种映射方式就不能满足需求了,因为匿名类型只能在一个方法内作为本地变量存在。

(P341)

SelectMany 可以将两个集合组成一个更大的集合。

(P342)

在分层次的数据查询中,使用 SelectMany 和 Select 得到的结果是相同的,但是在查询单层次的数据源 (如数组) 的时候,Select 要完成同样的任务,就需要使用嵌套循环了。

SelectMany 的好处就是在于,无论输入集合是什么类型的,它输出的集合肯定是一个数组类型的二维集合,结果集的数据不会有层次关系。

在查询表达式语法中,from 运算符有两个作用,在查询一开始的 from 的作用都是引入查询集合和范围变量;其他任何位置再出现 from 子句,编译器都会将其翻译成 SelectMany 。

(P343)

在需要用到外部变量的情况下,选择使用查询表达式语法是最佳选择。因为在这种情况中,这种语法不仅便于书写,而且表达方式也更接近查询逻辑。

(P344)

在 LINQ to SQL 和 EF 中, SelectMany 可以实现交叉连接、不等连接、内连接以及左外连接。

(P345)

在标准 SQL 中,所有的连接都要通过 join 关键字实现。

在 Entity Framework 的实体类中,并不会直接存储一个外键值,而是存储外键所关联对象的集合,所以当需要使用外键所关联的数据时,直接使用实体类属性中附带的数据集合即可,不用像 LINQ to SQL 查询中那样手动地进行连接来得到外键集合中的数据。

对于本地集合的查询中,为了提高执行效率,应该尽量先筛选,再连接。

如果有需要的话,可以引入新的表来进行连接,查询时的连接并不限于两个表之间,多个表也可以进行。在 LINQ 中,可以通过添加一个 from 子句来实现。

(P347)

正确的做法是在 DefaultIfEmpty 运算符之前使用 Where 语句。

Join 和 GroupJoin 的作用是连接两个集合进行查询,然后返回一个查询结果集。他们的不同点在于,Join 返回的是非嵌套结构的数据集合,而 GroupJoin 返回的则是嵌套结构的数据集合。

Join 和 GroupJoin 的长处在于对本地集合的查询,也就是对内存中数据的查询效率比较高。它们的缺点是目前只支持内连接和左外连接,并且连接条件必须是相等连接。需要用到交叉连接或者非等值连接时,就只能选择 Select 或者 SelectMany 运算符。在 LINQ to SQL 或者 EF 查询中, Join 和 GroupJoin 运算符在功能上与 Select 和 SelectMany 是没有什么区别的。

(P352)

当 into 关键字出现在 join 后面的时候,编译器会将 into 关键字翻译成 GroupJoin 来执行。而当 into 出现在 Select 或者 Group 子句之后时,则翻译成扩展现有的查询。虽然都是 into 关键字,但是出现在不同的地方,差别非常大。有一点它们是相同的,into 关键字总是引入一个新的变量。

GroupJoin 的返回结果实际上是集合的集合,也就是一个集合中的元素还是集合。

(P355)

Zip 是在 .NET Framework 4.0 中新加入的一个运算符,它可以同时枚举两个集合中的元素 (就像拉链的两边一样) ,返回的集合是经过处理的元素对。

两个集合中不能配对的元素会直接被忽略。需要注意的是,Zip 运算符只能用于本地集合的查询,它不支持对数据库的查询。

经过排序的集合中的元素值和未排序之前是相同的,只是元素的顺序不同。

(P356)

OrderBy 可以按照指定的方式对集合中的元素进行排序,具体的排序方式可以在 KeySelector 表达式中定义。

如果通过 OrderBy 按照指定顺序进行排序后,集合中的元素相对顺序仍无法确定时,可以使用 ThenBy 。

ThenBy 关键字的作用是在前一次排序的基础上再进行一次排序。在一个查询中,可以使用任意多个 ThenBy 关键字。

(P357)

LINQ 中还提供了 OrderByDescending 和 ThenByDescending 关键字,这两个关键字也是用于完成对集合的排序功能,它们的功能和 OrderBy / ThenBy 相同,用法也一样,只是它们排序后的集合中的元素是按指定字段的降序排序。

在对本地集合的查询中,LINQ 会根据默认的 IComparable 接口中的算法对集合中的元素进行排序。如果不想使用默认的排序方式,可以自己实现一个 IComparable 对象,然后将这个对象传递给查询 LINQ 。

在查询表达式语法中我们没有办法将一个 IComparable 对象传递给查询语句,也就不能进行自定义的查询。

在使用了排序操作的查询中,排序运算符会将集合转换成 IEnumerable<T> 类型的一个特殊子类。具体来说,对 Enumerable 类型的集合查询时,返回 IOrderedEnumerable 类型的集合;在对 Queryable 类型的集合查询时,返回 IOrderedQueryable 类型的集合。这两种子类型是为排序专门设计的,在它们上面可以直接使用 ThenBy 运算符来进行多次排序。

(P358)

在对远程数据源的查询中,需要用 AsQueryable 代替 AsEnumerable 。

(P359)

GroupBy 可以将一个非嵌套的集合按某种条件分组,然后将得到的分组结果以组为单位封装到一个集合中。

Enumerable.GroupBy 的内部实现是,首先将集合中的所有元素按照键值的关系存储到一个临时的字典类型的集合中。然后再将这个临时集合中的所有分组返回给调用者。这里一个分组就是一个键和它所对应的一个小集合。

默认情况下,分组之后的元素不会对原始元素做任何处理,如果需要在分组过程中对元素做某些处理的话,可以给元素选择器指定一个参数。

(P360)

GroupBy 只对集合进行分组,并不做任何排序操作,如果想要对集合进行排序的话,需要使用额外的 OrderBy 关键字。

在查询表达式语法中,GroupBy 可以使用下面这个格式来创建 : group 元素表达式 by 键表达式 。

和其他的查询一样,当查询语句中出现了 select 或者 group 的时候,整个查询就结束了,如果不想让查询就此结束,那么就需要扩展整个查询,可以使用 into 关键字。

在 group by 查询中,经常需要扩展查询语句,因为需要对分组后的集合进一步进行处理。

在 LINQ 中, group by 后面跟着 where 查询相当于 SQL 中的 HAVING 关键字。这个 where 所作用的对象是整个集合或者集合中的每个分组,而不是单个元素。

分组操作同样适用于对数据库的查询。如果是在 EF 中,在使用了关联属性的情况下,分组操作并不像在 SQL 中那样常用。

(P361)

LINQ 中的分组功能对 SQL 中的 “GROUP BY” 进行了很大的扩展,可以认为 LINQ 中的分组是 SQL 中分组功能的一个超集。

和传统 SQL 查询不同点是,在 LINQ 中不需要对分组或者排序子句中的变量进行映射。

当需要使用集合中多个键来进行分组时,可以使用匿名类型将这几个键封装到一起。

(P362)

Concat 运算符的作用是合并两个集合,合并方式是将第一个集合中所有元素放置到结果集中,然后再将第二个集合中的元素放在第一个结果集的后面,然后返回结果集。Union 执行的也是这种合并操作,但是它最后会将结果集中重复的元素去除,以保证结果集中每个元素都是唯一的。

当对两个不同类型但基类型却相同的序列执行合并时,需要显式地指定这两个集合的类型以及合并之后的集合类型。

Intersect 运算符用于取出两个集合中元素的交集。Except 用于取出只出现在第一个集合中的元素,如果某个元素在两个集合中都存在,那么这个元素就不会包含在结果中。

Enumerable.Except 的内部实现方式是,首先将第一个集合中的所有元素加载到一个字典集合中,然后再对比第二个集合中的元素,如果字典中的某个元素在第二个集合中出现了,那么就将这个元素从字典中移除。

(P363)

从根本上讲,LINQ 处理的是 IEnumerable<T> 类型的集合,之所以现在众多的集合类型都可以使用 LINQ 进行处理,是因为编译器内部可以将其他类型的序列转换成 IEnumerable<T> 类型的。

OfType 和 Cast 可以将非 IEnumerable 类型的集合转换成 IEnumerable<T> 类型的集合。

Cast 和 OfType 运算符的唯一不同就是它们遇到不相容类型时的处理方式 : Cast 会抛出异常,而 OfType 则会忽略这个类型不相容的元素。

元素相容的规则与 C# 的 is 运算符完全相同,因此只能考虑引用转换和拆箱转换。

Cast 运算符的内部实现与 OfType 完全相同,只是省略了类型检查那行代码。

OfType 和 Cast 的另一个重要功能是 : 按类型从集合中取出元素。

(P365)

ToArray 和 ToList 可以分别将集合转换成数组和泛型集合。这两个运算符也会强制 LINQ 查询语句立即执行,也就是说当整个查询是延迟加载的时候,一旦遇到 ToArray 或者 ToList ,整个语句会被立即执行。

ToDictionary 方法也会强制查询语句立即执行,然后将查询结果放在一个 Dictionary 类型的集合中。 ToDictionary 方法中的键选择器必须为每个元素提供一个唯一的键,也就是说不同元素的键是不能重复的,否则在查询的时候系统会抛出异常。而 Tolookup 方法的要求则不同,它允许多个元素共用相同的键。

AsEnumerable 将一个其他类型的集合转换成 IEnumerable<T> 类型,这样可以强制编译器使用 Enumerable 类中的方法来解析查询中的运算符。

AsQueryable 方法则会将一个其他类型的集合转换成 IQueryable<T> 类型的集合,前提是被转换的集合实现了 IQueryable<T> 接口。否则 IQueryable<T> 会实例化一个对象,然后存储在本地数组外面,看起来是可以调用 IQueryable 中的方法,但实际上这些方法并没有真正的意义。

(P366)

所有以 "OrDefault" 结尾的方法有一个共同点,那就是当集合为空或者集合中没有符合要求的元素时,这些方法不抛出异常,而是返回一个默认类型的值 default(TSource) 。

对于引用类型的元素来说 default(TSource) 是 null ,而对于值类型的元素来说,这个默认值通常是 0 。

为了避免出现异常,在使用 Single 运算符时必须保证集合中有且仅有一个元素;而 SingleOrDefault 运算符则要求集合中有一个或零个元素。

Single 是所有元素运算符中要求最多的,而 FirstOrDefault 和 LastOrDefault 则对集合中的元素没有什么要求。

(P367)

在 LINQ to SQL 和 EF 中, Single 运算符通常应用于使用主键到数据库中查找特定的单个元素。

ElementAt 运算符可以根据指定的下标取出集合中的元素。

Enumerable.ElementAt 的实现方式是,如果它所查询的集合实现了 IList<T> 接口,那么在取元素的时候,就使用 IList<T> 中的索引器。否则,就使用自定义的循环方法,在循环中依次向后查找元素,循环 n 次之后,返回下一个元素。ElementAt 运算符不能在 LINQ to SQL 和 EF 中使用。

DefaultIfEmpty 可以将一个空的集合转换成 null 或者 default() 类型。这个运算符一般用于定义外连接查询。

(P368)

Count 运算符的作用是返回集合中元素的个数。

Enumerable.Count 方法的内部实现方式如下 : 首先判断输入集合有没有实现 ICollection<T> 接口,如果实现了,那么它的就调用 ICollection<T>.Count 方法得到元素个数。否则就遍历整个集合中的元素,统计出元素的个数,然后返回。

还可以为 Count 这个方法添加一个筛选条件。

LongCount 运算符的作用和 Count 是相同的,只是它的返回值类型是 int64 ,也就是它能用于大数据量的统计, int64 能统计大概 20 亿个元素的集合。

Min 和 Max 返回集合中最小和最大的元素。

如果集合没有实现 IComparable<T> 接口的话,那么我们就必须为这两个运算符提供选择器。

选择器表达式不仅定义了元素的比较方式,还定义了最后的结果集的类型。

(P369)

Sum 和 Average 的返回值类型是有限的,它们内置了以下几种固定的返回值类型 : int 、 long 、 float 、 double 、 decimal 以及这几种类型的可空类型。这里返回值都是值类型,也就是,Sum 和 Average 的预期结果都是数字。而 Min 和 Max 则会返回所有实现了 IComparable<T> 接口的类型。

更进一步讲, 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 的重载方法可以指定命名空间和前缀。

本文标签: 果壳学习笔记权威指南