C# 2012 说明指南(六)
十八、枚举器和迭代器
枚举器和可枚举类型
在《??》第十二章中,你看到了你可以使用一个foreach语句循环遍历一个数组的元素。在这一章中,你将仔细观察数组,看看为什么它们可以被foreach语句处理。您还将看到如何使用迭代器将这种能力添加到您自己的用户定义的类中。
使用 foreach 语句
当您对一个数组使用foreach语句时,该语句会一个接一个地显示数组中的每个元素,允许您读取它的值。例如,下面的代码声明了一个包含四个元素的数组,然后使用一个foreach循环打印出各项的值:
` int[] arr1 = { 10, 11, 12, 13 }; // Define the array.
foreach (int item in arr1) // Enumerate the elements. Console.WriteLine("Item value: {0}", item);`
该代码产生以下输出:
Item value: 10 Item value: 11 Item value: 12 Item value: 13
为什么这适用于数组?原因是数组可以根据请求产生一个名为枚举器的对象。枚举器是一个对象,它可以根据请求依次返回数组的元素。枚举器“知道”项目的顺序,并跟踪它在序列中的位置。然后,当被请求时,它返回当前项。
对于具有枚举数的类型,必须有一种检索它的方法。检索对象的枚举数的方法是调用对象的GetEnumerator方法。实现了GetEnumerator方法的类型被称为可枚举类型,或者简称为可枚举类型。数组是可枚举的。
图 18-1 说明了枚举数和枚举数的关系。
图 18-1 。枚举器和枚举器概述
foreach构造被设计成使用可枚举的。只要要迭代的对象是可枚举类型,比如数组,它就会执行以下操作:
- Get the enumerator of the object by calling its
GetEnumeratormethod.- It requests each item from the enumerator and uses it as iteration variable for your code to read (but not change).
Must be enumerable ↓ foreach( *Type VarName* in *EnumerableObject* ) { ... }
IEnumerator 接口
一个枚举器实现了IEnumerator接口,该接口包含三个函数成员:Current、MoveNext和Reset。
Currentis the attribute of the item at the current position in the return sequence.
- It is a read-only property.
- It returns a reference of type
object, so it can return any type of object.MoveNextis a method to advance the position of the enumerator to the next item in the collection. It also returns a Boolean value indicating whether the new position is a valid position or beyond the end of the sequence.
- If the new position is valid, the method returns to
true.- If the new position is invalid (that is, the current position exceeds the end point), the method returns
false.- The initial position of the enumerator is before the first item in the sequence, so
MoveNextmust call before accessesCurrentfor the first time.Resetis a method to reset the position to the initial state.
图 18-2 展示了一个由三个项目组成的集合,显示在图的左边,以及它的枚举器,显示在右边。在图中,枚举器是一个名为ArrEnumerator的类的实例。
图 18-2 。小集合的枚举器
枚举器跟踪序列中当前项目的方式完全依赖于实现。它可能被实现为对一个对象的引用、一个索引值或者其他完全不同的东西。对于内置的一维数组类型,它只是项目的索引。
图 18-3 显示了三个项目集合的枚举器的状态。这些状态被标记为 1 到 5。
- Note that in state 1, the initial position of the enumerator is 1 (that is, before the first element of the collection).
- Every transition between states is caused by a call to
MoveNext, which will raise the position in the sequence. Every call toMoveNextbetween state 1 and state 4 returnstrue. However, in the transition between States 4 and 5, the position ends at the last item in the set, so the method returnsfalse.- In the final state, any further call to
MoveNextreturnsfalse.
图 18-3 。计数器的状态
给定一个集合的枚举器,您应该能够通过使用MoveNext和Current成员循环遍历集合中的项目来模拟一个foreach循环。例如,您知道数组是可枚举的,所以下面的代码手动执行foreach语句自动执行的操作。事实上,当你写一个foreach循环时,C# 编译器会生成与此非常相似的代码(当然是在 CIL)。
static void Main() { int[] MyArray = { 10, 11, 12, 13 }; // Create an array. Get and store the enumerator. <ins> ↓ </ins> IEnumerator ie = MyArray.GetEnumerator(); Move to the next position. <ins> ↓ </ins> while ( ie.MoveNext() ) { Get the current item. <ins> ↓ </ins> int i = (int) ie.Current; Console.WriteLine("{0}", i); // Write it out. } }
这段代码产生以下输出,就像您使用了内置的foreach语句一样:
10 11 12 13
图 18-4 说明了代码示例中数组的结构。
***图 18-4。*美国 .NET 数组类实现 IEnumerable。
IEnumerable 接口
可枚举类是实现IEnumerable接口的类。接口只有一个成员,方法GetEnumerator,它返回对象的枚举数。
图 18-5 显示了类MyClass,该类有三项要枚举,通过实现GetEnumerator方法来实现IEnumerable接口。
图 18-5 。GetEnumerator 方法返回该类的枚举器对象。
下面的代码显示了可枚举类的声明形式:
using System.Collections; Implements the IEnumerable interface ↓ class MyClass : IEnumerable { public IEnumerator GetEnumerator { ... } ... ↑ } Returns an object of type IEnumerator
下面的代码给出了一个可枚举类的例子,它使用了一个名为ColorEnumerator的枚举器类,它实现了IEnumerator。我将在下一节展示ColorEnumerator的实现。
` using System.Collections;
class MyColors: IEnumerable { string[] Colors = { "Red", "Yellow", "Blue" };
public IEnumerator GetEnumerator() { return new ColorEnumerator(Colors); } ↑ } An instance of the enumerator class`
使用 IEnumerable 和 IEnumerator 的例子
下面的代码展示了一个名为Spectrum的可枚举类及其枚举器类ColorEnumerator的完整示例。类Program在方法Main中创建了一个MyColors的实例,并在foreach循环中使用它。
`using System; using System.Collections;
class ColorEnumerator : IEnumerator { string[] _colors; int _position = -1;
public ColorEnumerator( string[] theColors ) // Constructor { _colors = new string[theColors.Length]; for ( int i = 0; i < theColors.Length; i++ ) _colors[i] = theColors[i]; }
public object Current // Implement Current. { get { if ( _position == -1 ) throw new InvalidOperationException(); if ( _position >= _colors.Length ) throw new InvalidOperationException();
return _colors[_position]; } }
public bool MoveNext() // Implement MoveNext. { if ( _position < _colors.Length - 1 ) { _position++; return true; } else return false; }
public void Reset() // Implement Reset.
{
_position = -1;
}
} class Spectrum : IEnumerable
{
string[] Colors = { "violet", "blue", "cyan", "green", "yellow", "orange", "red" };
public IEnumerator GetEnumerator() { return new ColorEnumerator( Colors ); } }
class Program { static void Main() { Spectrum spectrum = new Spectrum(); foreach ( string color in spectrum ) Console.WriteLine( color ); } }`
该代码产生以下输出:
violet blue cyan green yellow orange red
通用枚举接口
到目前为止,我描述的枚举接口都是非通用版本。实际上,您应该主要使用接口的通用版本,即IEnumerable<T>和IEnumerator<T>。它们被称为泛型是因为它们使用 C# 泛型,这在第十七章中有所涉及。使用它们与使用非泛型形式基本相同。
两者之间的本质区别如下:
- In the form of non-universal interface
- The
GetEnumeratormethod of interfaceIEnumerablereturns an enumerator class instance that implementsIEnumerator.- The class that implements
IEnumeratorimplements the propertyCurrent, which returns a reference of typeobject, and then you must convert it to the actual type of the object.- Use common interface form.
- The
GetEnumeratormethod of interfaceIEnumerable<T>returns an instance of the class that implementsIEnumerator<T>.- The class that implements
IEnumerator<T>implements the propertyCurrent, which returns an instance of the actual type instead of a reference to the base classobject.
然而,需要注意的最重要的一点是,到目前为止,我们看到的非泛型接口实现不是类型安全的。它们返回对类型object的引用,然后必须将其转换为实际类型。
然而,使用通用接口,枚举器是类型安全的,返回对实际类型的引用。如果您通过实现接口来创建自己的可枚举数,这是您应该采用的方法。非泛型接口形式适用于 C# 2.0 引入泛型之前开发的遗留代码。
虽然通用版本与非通用版本相同或者更容易使用,但是它们的结构稍微复杂一些。图 18-6 和 18-7 说明了它们的结构。
图 18-6 。实现 IEnumerator < T >接口的类的结构
**图 18-7。**实现 IEnumerable < T >接口的类的结构
迭代器
可枚举类和枚举器在 .NET 集合类,所以熟悉它们的工作方式很重要。但是现在您已经知道了如何创建自己的可枚举类和枚举器,您可能会很高兴地了解到,从 C# 2.0 开始,该语言提供了一种更简单的创建枚举器和枚举器的方法。事实上,编译器会为您创建它们。产生它们的结构被称为迭代器。您可以在任何需要使用手动编码的枚举器或枚举器的地方使用由迭代器生成的枚举器和枚举器。
在我解释细节之前,我们先来看两个例子。下面的方法声明实现了一个迭代器,该迭代器生成并返回一个枚举数。
- Iterator returns a universal enumerator, which returns three items of type
string.
yield return这是枚举
Return an enumerator that returns strings. <ins> ↓ </ins> public IEnumerator<string> BlackAndWhite() // Version 1 { yield return "black"; // yield return yield return "gray"; // yield return yield return "white"; // yield return }
下面的方法声明是产生相同结果的另一个版本:
` Return an enumerator that returns strings. ↓ public IEnumerator BlackAndWhite() // Version 2 { string[] theColors = { "black", "gray", "white" };
for (int i = 0; i < theColors.Length; i++) yield return theColors[i]; // yield return }`
我还没有解释yield return语句,但是在检查这些代码段时,您可能会觉得这段代码有些不同。好像不太对。yield return语句到底是做什么的?
例如,在第一个版本中,如果方法在第一个yield return语句上返回,那么最后两个语句永远不会到达。如果它没有在第一个语句中返回,而是继续到方法的末尾,那么值会发生什么呢?在第二个版本中,如果循环体中的yield return语句在第一次迭代时返回,那么循环将永远不会到达任何后续迭代。
除此之外,枚举器并不只是一次性返回所有的元素——它会在每次访问Current属性时返回一个新值。那么,这如何给你一个枚举数呢?显然,这段代码不同于之前展示的任何代码。
迭代器块
一个迭代器块是一个包含一个或多个yield语句的代码块。以下三种类型的代码块都可以是迭代器块:
- A legal system
- A legal system
- A legal system
迭代器块的处理方式不同于其他块。其他块包含被强制处理的语句序列。也就是说,执行块中的第一条语句,然后执行后续语句,最后控制权离开块。
另一方面,迭代器块不是一次执行的命令序列。相反,它是声明性的;它描述了您希望编译器为您生成的枚举数类的行为。迭代器块中的代码描述了如何枚举元素。
迭代器块有两个特殊的语句:
yield return
yield break
编译器获取如何枚举项的描述,并使用它来构建枚举器类,包括所有必需的方法和属性实现。结果类嵌套在声明迭代器的类中。
根据迭代器块使用的返回类型,你可以让迭代器产生一个枚举器或者一个可枚举器,如图图 18-8 所示。
***图 18-8。*你可以让一个迭代器块产生一个枚举器或者一个可枚举器,这取决于你指定的返回类型。
使用迭代器创建枚举器
下面的代码演示了如何使用迭代器创建一个可枚举的类。
- Method
BlackAndWhiteis an iterator block that generates a method that returns the enumerator of classMyClass.MyClassalso implements the methodGetEnumerator, which just callsBlackAndWhiteand returns the enumerator returned byBlackAndWhite.- Note that in
Main, you can directly use the instance of this class inforeachstatement, because this class implementsGetEnumerator, so it is enumerable.
` class MyClass { public IEnumerator GetEnumerator() { return BlackAndWhite(); // Returns the enumerator } Returns an enumerator ↓ public IEnumerator BlackAndWhite() // Iterator { yield return "black"; yield return "gray"; yield return "white"; } }
class Program { static void Main() { MyClass mc = new MyClass(); Use the instance of MyClass. ↓ foreach (string shade in mc) Console.WriteLine(shade); } }`
该代码产生以下输出:
black gray white
图 18-9 左边显示了MyClass的代码,右边显示了结果对象。注意有多少是由编译器自动构建的。
- The code of the iterator is shown on the left side of the figure, showing that its return type is
IEnumerator<string>.- On the right side of the diagram, the diagram shows that the nested class implements
IEnumerator<string>.
图 18-9 。生成枚举器的迭代器块
使用迭代器创建一个可枚举
前面的例子创建了一个包含两部分的类:产生返回枚举数的方法的迭代器和返回枚举数的GetEnumerator方法。在这个例子中,迭代器产生一个可枚举的而不是一个的枚举器。这个例子和上一个例子有一些重要的区别:
- In the example, the iterator method
BlackAndWhitereturns aIEnumerator<string>, andMyClassimplements the methodGetEnumeratorby returningBlackAndWhite. In this example, the iterator methodBlackAndWhitereturns aIEnumerable<string>instead of aIEnumerator<string>. Therefore,MyClassimplements itsGetEnumeratormethod by first calling the methodBlackAndWhiteto obtain an enumerable object, then calling theGetEnumeratormethod of the object and returning the result.- Note that in the
foreachstatement ofMain, you can use an instance of the class or callBlackAndWhitedirectly, because it returns an enumerable one. There are both ways.
` class MyClass { public IEnumerator GetEnumerator() { IEnumerable myEnumerable = BlackAndWhite(); // Get enumerable. return myEnumerable.GetEnumerator(); // Get enumerator. } Returns an enumerable ↓ public IEnumerable BlackAndWhite() { yield return "black"; yield return "gray"; yield return "white"; } }
class Program { static void Main() { MyClass mc = new MyClass(); Use the class object. ↓ foreach (string shade in mc) Console.Write("{0} ", shade); Use the class iterator method. ↓ foreach (string shade in mc.BlackAndWhite()) Console.Write("{0} ", shade); } }`
这段代码产生以下输出:
black gray white black gray white
图 18-10 说明了代码中可枚举迭代器产生的泛型可枚举。
- The code of the iterator is shown on the left side of the figure, showing that its return type is
IEnumerable<string>.- On the right side of the diagram, the diagram shows that the nested class implements both
IEnumerator<string>andIEnumerable<string>.
***图 18-10。*编译器生成一个既是枚举器又是枚举器的类。它还产生返回可枚举对象的方法 BlackAndWhite。
常见迭代器模式
前两节展示了您可以创建一个迭代器来返回一个枚举数或一个枚举数。图 18-11 总结了如何使用常见的迭代器模式。
- When you implement an iterator that returns an enumerator, you must make class enumerable by implementing
GetEnumeratorso that it returns the enumerator returned by the iterator. This is shown on the left side of the diagram.- In a class, when you implement an iterator that returns enumerable, you can make the class itself enumerable or un-enumerable by making the class implement
GetEnumeratoror not.
- If you implement
GetEnumerator, let it call the iterator method to get an instance of the automatically generated class that implementsIEnumerable. Next, return the enumerator built byGetEnumeratorfrom thisIEnumerableobject, as shown in the right figure.- If you decide not to make the class itself enumerable, you can still use the enumerable returned by the iterator by not implementing
GetEnumerator, and call the iterator method directly, as shown in the secondforeachstatement on the right.
图 18-11 。常见的迭代器模式
产生多个可枚举数
在下面的例子中,类Spectrum有两个可枚举的迭代器——一个从紫外端到红外端枚举光谱的颜色,另一个从相反的方向枚举。请注意,尽管它有两个返回可枚举的方法,但该类本身是不可枚举的,因为它没有实现GetEnumerator。
` using System; using System.Collections.Generic;
class Spectrum { string[] colors = { "violet", "blue", "cyan", "green", "yellow", "orange", "red" }; Returns an enumerable ↓ public IEnumerable UVtoIR() { for ( int i=0; i < colors.Length; i++ ) yield return colors[i]; } Returns an enumerable ↓ public IEnumerable IRtoUV() { for ( int i=colors.Length - 1; i >= 0; i-- ) yield return colors[i]; } }
class Program { static void Main() { Spectrum spectrum = new Spectrum();
foreach ( string color in spectrum.UVtoIR() ) Console.Write( "{0} ", color ); Console.WriteLine();
foreach ( string color in spectrum.IRtoUV() ) Console.Write( "{0} ", color ); Console.WriteLine(); } }`
该代码产生以下输出:
violet blue cyan green yellow orange red red orange yellow green cyan blue violet
迭代器作为属性
前面的例子使用迭代器产生一个具有两个可枚举数的类。这个例子说明了两件事。首先,它使用迭代器产生一个具有两个枚举器的类。第二,它展示了迭代器如何实现为属性而不是方法。
代码声明了两个属性,这两个属性定义了两个不同的枚举数。根据布尔变量_listFromUVtoIR的值,GetEnumerator方法返回两个枚举器中的一个或另一个。如果_listFromUVtoIR为true,则返回UVtoIR枚举数。否则,返回IRtoUV枚举器。
`using System; using System.Collections.Generic;
class Spectrum { bool _listFromUVtoIR;
string[] colors = { "violet", "blue", "cyan", "green", "yellow", "orange", "red" };
public Spectrum( bool listFromUVtoIR ) { _listFromUVtoIR = listFromUVtoIR; }
public IEnumerator GetEnumerator() { return _listFromUVtoIR ? UVtoIR : IRtoUV; }
public IEnumerator UVtoIR { get { for ( int i=0; i < colors.Length; i++ ) yield return colors[i]; } }
public IEnumerator IRtoUV
{
get
{
for ( int i=colors.Length - 1; i >= 0; i-- )
yield return colors[i];
}
}
} class Program
{
static void Main()
{
Spectrum startUV = new Spectrum( true );
Spectrum startIR = new Spectrum( false );
foreach ( string color in startUV ) Console.Write( "{0} ", color ); Console.WriteLine();
foreach ( string color in startIR ) Console.Write( "{0} ", color ); Console.WriteLine(); } }`
该代码产生以下输出:
violet blue cyan green yellow orange red red orange yellow green cyan blue violet
幕后用迭代器
以下是关于迭代器需要知道的一些其他重要的事情:
- Iterator needs
System.Collections.Genericnamespace, so it should be included withusinginstruction.Resetmethod is not supported in enumerator generated by compiler. It is implemented because the interface needs it, but if it is called, the implementation will throw aSystem.NotSupportedExceptionexception. Note that theResetmethod is shown in gray in Figure 18-9 .
在幕后,编译器生成的枚举器类是一个状态机,有四种状态:
- Before : the initial state before
MoveNextis called for the first time. ** Running : the state entered whenMoveNextis called. In this state, the enumerator determines and sets the position of the next item. When it encounters ayield return,yield breakor the end of the iterator body, it exits the state.* Pending : The state machine waits for the next call ofMoveNext.* After : There are no more items to enumerate.*
**如果状态机处于之前的或暂停状态,并且调用了MoveNext方法,它将进入运行状态。在运行状态下,确定集合中的下一个项目并设定位置。
如果有更多项目,状态机进入暂停状态。如果没有更多的项目,它将在后进入状态,并保持不变。图 18-12 显示了状态机。
**图 18-12。迭代器状态机
十九、LINQ 简介
什么是 LINQ?
在关系数据库系统中,数据被很好地组织成规范化的表,并使用一种非常简单但功能强大的查询语言——SQL 来访问。SQL 可以处理数据库中的任何数据集,因为数据是按照严格的规则组织到表中的。
然而,在程序中,与数据库相反,数据存储在完全不同的类对象或结构中。因此,还没有通用的查询语言来从数据结构中检索数据。从对象中检索数据的方法一直是作为程序的一部分定制设计的。然而,LINQ 使查询对象集合变得很容易。
以下是 LINQ 的重要高级特征:
- LINQ stands for language comprehensive query , pronounced as link .
- It's an extension of LINQ. NET Framework, and allows you to query the data collection in a way similar to querying the database with SQL. With LINQ, you can query data from databases, program object collections, XML documents, etc.
下面的代码显示了一个使用 LINQ 的简单示例。在这段代码中,被查询的数据源只是一个由int组成的数组。查询的定义是带有from和select关键字的语句。虽然在这个语句中查询是由定义的,但是直到底部的foreach语句中需要结果时才真正执行。
` static void Main() { int[] numbers = { 2, 12, 5, 15 }; // Data source
IEnumerable lowNums = // Define and store the query. from n in numbers where n < 10 select n;
foreach (var x in lowNums) // Execute the query. Console.Write("{0}, ", x); }`
该代码产生以下输出:
2, 5,
LINQ 供应商
在前面的例子中,数据源只是一个由int组成的数组,它是程序的内存对象。然而,LINQ 可以处理许多不同类型的数据源,比如 SQL 数据库、XML 文档和许多其他数据源。然而,对于每种数据源类型,都必须有一个代码模块根据该数据源类型实现 LINQ 查询。这些代码模块被称为 LINQ 提供者。关于 LINQ 提供商的要点如下:
- Microsoft provides LINQ providers for some common data source types, as shown in figure and figure 19-1 .
- You can use any LINQ-supported language (C# in our example) to query any data source type with LINQ provider. Third parties are constantly developing new LINQ providers for various data source types.
图 19-1 。LINQ、支持 LINQ 的语言和 LINQ 提供者
有整本书致力于 LINQ 的所有形式和微妙之处,但这显然超出了本章的范围。相反,本章将向您介绍 LINQ,并解释如何将它用于程序对象(LINQ 到对象)和 XML (LINQ 到 XML)。
匿名类型
在详细介绍 LINQ 的查询特性之前,我将首先介绍一种语言特性,它允许您创建未命名的类类型。毫不奇怪,这些被称为匿名类型。匿名类型通常用于 LINQ 查询的结果。
第六章讲述了对象初始化器,它允许你在使用对象创建表达式时初始化一个新类实例的字段和属性。提醒您一下,这种对象创建表达式由三部分组成:关键字new、类名或构造函数以及对象初始化器。对象初始化器由一组花括号之间的逗号分隔的成员初始化器列表组成。
创建匿名类型的变量使用相同的形式——但是没有类名或构造函数。以下代码行显示了匿名类型的对象创建表达式形式:
No class name Anonymous object initializer ↓ <ins> ↓ </ins> new { <ins>FieldProp = InitExpr</ins>, <ins>FieldProp = InitExpr</ins>, ...} ↑ ↑ Member initializer Member initializer
下面的代码显示了一个创建和使用匿名类型的示例。它创建了一个名为student的变量,其匿名类型有两个string属性和一个int属性。注意在WriteLine语句中,实例的成员被访问,就像它们是一个命名类型的成员一样。
` static void Main( ) { var student = new {Name="Mary Jones", Age=19, Major="History"}; ↑ ↑ Must use var Anonymous object initializer
Console.WriteLine("{0}, Age {1}, Major: {2}", student.Name, student.Age, student.Major); }`
该代码产生以下输出:
Mary Jones, Age 19, Major: History
关于匿名类型,需要了解的重要事项如下:
- Anonymous types can only be used for local variables, not class members.
- Because anonymous types have no name, the keyword
varmust be used as the variable type.- You can't assign attributes to an object of anonymous type. Attributes created by the compiler for anonymous types are read-only.
当编译器遇到匿名类型的对象初始化器时,它用自己构造的私有名称创建一个新的类类型。对于每个成员初始化器,它推断其类型并创建一个只读属性来访问其值。属性与成员初始值设定项同名。一旦构造了匿名类型,编译器就创建该类型的对象。
除了成员初始值设定项的赋值形式,匿名类型对象初始值设定项还允许其他两种形式:简单标识符和成员访问表达式。这些形式称为投影初始化器,不使用赋值表达式。相反,它们使用标识符或被访问对象的成员名称作为匿名类型成员的名称。下面的变量声明显示了所有三种形式。第一个成员初始化器是赋值形式的。第二个是成员访问表达式,第三个是标识符。
var student = new { Age = 19, Other.Name, Major };
例如,下面的代码显示了如何使用它。请注意,投影初始值设定项必须在匿名类型声明之前定义。Major是局部变量,Name是Other类的静态字段。
` class Other { static public string Name = "Mary Jones"; }
class Program { static void Main() { string Major = "History"; Assignment form Identifier ↓ ↓ var student = new { Age = 19, Other.Name, Major}; ↑ Member access Console.WriteLine("{0}, Age {1}, Major: {2}", student.Name, student.Age, student.Major); } }`
该代码产生以下输出:
Mary Jones, Age 19, Major: History
刚刚显示的对象初始值设定项的投影初始值设定项形式与此处显示的赋值形式具有完全相同的结果:
var student = new { Age = Age, Name = Other.Name, Major = Major};
如果编译器遇到另一个具有相同参数名、相同推断类型和相同顺序的匿名对象初始值设定项,它将重用已经创建的匿名类型,并只创建一个新实例,而不是新的匿名类型。
方法语法和查询语法
LINQ 为指定查询提供了两种语法形式:查询语法和方法语法。
- Method syntax uses standard method calls. These methods come from a set of standard query operators, which I will describe later in this chapter.
- The query syntax looks very much like an SQL statement. Query syntax is written in the form of query expression.
- You can combine the two forms in a single query.
查询语法是一种声明性形式,这意味着您的查询描述了您想要返回的内容,但没有指定如何执行查询。方法语法是一种命令式形式,它指定了调用查询方法的确切顺序。使用查询语法表达的查询由 C# 编译器翻译成方法调用形式。这两种形式的运行时性能没有区别。
Microsoft 建议使用查询语法,因为它可读性更强,更清楚地表达了您的查询意图,因此不容易出错。但是,有些运算符只能使用方法语法编写。
下面的代码显示了这两个窗体和一个组合窗体的示例。在方法语法部分,注意到Where方法的参数使用了 lambda 表达式,如第十三章所述。在这一章的后面我会谈到它在 LINQ 的使用。
`static void Main( ) { int[] numbers = { 2, 5, 28, 31, 17, 16, 42 };
var numsQuery = from n in numbers // Query syntax where n < 20 select n;
var numsMethod = numbers.Where(x => x < 20); // Method syntax
int numsCount = (from n in numbers // Combined where n < 20 select n).Count();
foreach (var x in numsQuery) Console.Write("{0}, ", x); Console.WriteLine();
foreach (var x in numsMethod) Console.Write("{0}, ", x); Console.WriteLine();
Console.WriteLine(numsCount); }`
该代码产生以下输出:
2, 5, 17, 16, 2, 5, 17, 16, 4
查询变量
LINQ 查询可以返回两种类型的结果之一:一个枚举,它列出满足查询参数的项目;或者单个值,称为标量,它是满足查询的结果的某种形式的汇总。
在下面的代码示例中,发生了以下情况:
- The first statement creates an array of
intand initializes it with three values.- The second statement specifies a LINQ query that lists the results of the query.
- The third statement executes the query, and then calls the LINQ method (
Count), which returns the number of items returned by the query. I will introduce operators that return scalars later in this chapter, such asCount.
` int[] numbers = { 2, 5, 28 };
IEnumerable lowNums = from n in numbers // Returns an enumerator where n < 20 select n;
int numsCount = (from n in numbers // Returns an int where n < 20 select n).Count();`
第二个和第三个语句等号左边的变量叫做查询变量。尽管在示例语句中明确给出了查询变量的类型(IEnumerable<T>和int),但是您也可以使用var关键字来代替类型名,并让编译器推断查询变量的类型。
理解如何使用查询变量很重要。执行上述代码后,查询变量lowNums是否包含查询结果而非。相反,编译器已经创建了代码,如果稍后在代码中调用该代码,将运行该代码来执行查询。
然而,查询变量numsCount包含一个实际的整数值,它只能通过实际运行查询来获得。
查询执行时间的差异可以总结如下:
- If the query expression returns an enumeration, the query will not be executed until the enumeration is processed.
- If the enumeration is processed multiple times, the query is executed multiple times.
- If the data changes between the time when the enumeration is generated and the time when the query is executed, the query will be run against the new data.
- If the query expression returns a scalar, the query is executed immediately and the result is stored in the query variable.
查询表达式的结构
一个查询表达式由一个from子句后跟一个查询体组成,如图图 19-2 所示。关于查询表达式,需要了解的一些重要信息如下:
子句必须按所示顺序出现。* The required two parts are
fromclause andselect...groupclause.* Other terms are optional.* In the LINQ query expression, theselectclause is at the end of the expression. This is different from SQL, where theSELECTstatement is located at the beginning of the query. One of the reasons for using this location in C# is that it allows Visual Studio's intellisense to provide you with more options when you enter code.* There can be any number offrom...let...whereclauses, as shown in the figure.
图 19-2 。查询语句的结构由一个 from 子句后跟一个查询体组成。
from 子句
from子句指定用作数据源的数据集合。它还引入了迭代变量。关于from条款的要点如下:
迭代变量
- The syntax of
fromclause is as follows, where
*Type*is the type of element in the set. This is optional because the compiler can infer the type from the collection.*Item*is the name of the iteration variable.*Items*is the name of the set to be queried. Collections must be enumerable, as described in Chapter 18.
Iteration variable declaration <ins> ↓ </ins> from Type Item in *Items*
以下代码显示了用于查询四个int的数组的查询表达式。迭代变量item将表示数组中四个元素中的每一个,并将被其后的where和select子句选择或拒绝。这段代码省略了迭代变量的可选类型(int)。
` int[] arr1 = {10, 11, 12, 13}; Iteration variable ↓ var query = from item in arr1 where item < 13 ← Uses the iteration variable select item; ← Uses the iteration variable
foreach( var item in query ) Console.Write("{0}, ", item );`
该代码产生以下输出:
10, 11, 12,
图 19-3 显示了from子句的语法。同样,类型说明符是可选的,因为它可以被编译器推断出来。可以有任意数量的可选join子句。
图 19-3 。from 子句的语法
尽管 LINQ from条款和foreach声明之间有很大的相似性,但也有几个主要区别:
- The foreach statement forcibly specifies that the items in the collection should be considered in order, from the first to the last. The from clause declares that every item in the collection must be considered, but does not assume the order.
- The foreach statement executes its body where it is encountered in the code. On the other hand, the from clause does nothing. It creates a background code object that can be queried later. Only when the control flow of the program encounters a statement that accesses the query variable will the query be executed.
连接子句
LINQ 中的join子句很像 SQL 中的JOIN子句。如果您熟悉 SQL 中的连接,那么 LINQ 中的连接对您来说并不陌生,除了您现在可以在对象集合和数据库表上执行它们。如果您是 joins 的新手或者需要复习,那么下一节将帮助您理清思路。
关于join首先要知道的重要事情如下:
- You can use a connection to merge data from two or more collections.
- A join operation takes two sets and creates a new temporary object set, in which each object in the new set contains all fields of one object from the two initial sets.
下面显示了一个join的语法。它指定第二个集合将与前一个子句中的集合相联接。注意上下文关键字equals,它必须用于比较字段,而不是==操作符。
Keyword Keyword Keyword Keyword ↓ ↓ ↓ ↓ join <ins>*Identifier* in *Collection2*</ins> on <ins>*Field1* equals *Field2*</ins> ↑ ↑ Specify additional collection The fields to compare and ID to reference it. for equality
图 19-4 说明了join子句的语法。
图 19-4 。join 子句的语法
以下带注释的语句显示了一个join子句的示例:
什么是联接?
LINQ 的 A join获取两个集合并创建一个新的集合,其中每个元素都有来自两个原始集合的元素的成员。
例如,下面的代码声明了两个类:Student和CourseStudent。
- The object of type
Studentcontains the student's last name and student ID.- The object of type
CourseStudentrepresents the students who have registered for the course, including the course name and student ID number.
` public class Student { public int StID; public string LastName; }
public class CourseStudent { public string CourseName; public int StID; }`
图 19-5 显示了一个项目中的情况,其中有三个学生和三门课程,学生注册了各种课程。该程序有一个名为students的数组,包含Student个对象,还有一个名为studentsInCourses的数组,包含CourseStudent个对象,针对每门课程中注册的每个学生。
图 19-5 。参加各种课程的学生
现在假设您想要获取某门课程中每个学生的姓氏。students数组有姓氏,但没有班级注册信息。studentsInCourses数组有课程注册信息,但没有学生姓名。但是我们可以使用学号(StID)将这些信息联系在一起,这对两个数组的对象都是通用的。您可以通过在StID字段上连接来实现这一点。
图 19-6 显示了连接是如何工作的。左栏显示的是students数组,右栏显示的是studentsInCourses数组。如果我们获取第一个学生记录,并将其 ID 与每个studentsInCourses对象中的学生 ID 进行比较,我们会发现其中两个匹配,如中间列顶部所示。如果我们对另外两个学生做同样的事情,我们会发现第二个学生选了一门课,第三个学生选了两门课。
中间一列中的五个灰色对象表示字段StID上两个数组的连接。每个对象包含三个字段:来自Students类的LastName字段,来自CourseStudent类的CourseName字段,以及两个类共有的StID字段。
图 19-6 。两个对象数组及其在字段 StId 上的连接
下面的代码将整个例子放在一起。该查询查找所有学习历史课程的学生的姓氏。
` class Program { public class Student { // Declare classes. public int StID; public string LastName; }
public class CourseStudent { public string CourseName; public int StID; }
static Student[] students = new Student[] { new Student { StID = 1, LastName = "Carson" }, new Student { StID = 2, LastName = "Klassen" }, new Student { StID = 3, LastName = "Fleming" }, }; // Initialize arrays. static CourseStudent[] studentsInCourses = new CourseStudent[] { new CourseStudent { CourseName = "Art", StID = 1 }, new CourseStudent { CourseName = "Art", StID = 2 }, new CourseStudent { CourseName = "History", StID = 1 }, new CourseStudent { CourseName = "History", StID = 3 }, new CourseStudent { CourseName = "Physics", StID = 3 }, };
static void Main( ) { // Find the last names of the students taking history. var query = from s in students join c in studentsInCourses on s.StID equals c.StID where c.CourseName == "History" select s.LastName;
// Display the names of the students taking history. foreach (var q in query) Console.WriteLine("Student taking History: {0}", q); } }`
该代码产生以下输出:
Student taking History: Carson Student taking History: Fleming
此从。。。让。。。查询正文中的 where 部分
可选的from...let...where部分是查询体的第一部分。它可以包含组成它的三个子句中的任意一个——from子句、let子句和where子句。图 19-7 总结了三个子句的语法。
图 19-7 。from 的语法。。。让。。。where 子句
from 子句
您看到查询表达式以必需的from子句开始,后面是查询体。主体本身可以从任意数量的附加from子句开始,其中每个后续的from子句指定一个附加的源数据集合,并引入一个新的迭代变量用于进一步的评估。所有from子句的语法和含义都是相同的。
下面的代码显示了这种用法的一个示例。
- The first
fromclause is a required clause of the query expression.- The second
fromclause is the first clause of the query body.- The
selectclause creates objects of anonymous type.
` static void Main() { var groupA = new[] { 3, 4, 5, 6 }; var groupB = new[] { 6, 7, 8, 9 };
var someInts = from a in groupA ← Required first from clause from b in groupB ← First clause of query body where a > 4 && b <= 8 select new {a, b, sum = a + b}; ← Object of anonymous type
foreach (var a in someInts) Console.WriteLine(a); }`
该代码产生以下输出:
{ a = 5, b = 6, sum = 11 } { a = 5, b = 7, sum = 12 } { a = 5, b = 8, sum = 13 } { a = 6, b = 6, sum = 12 } { a = 6, b = 7, sum = 13 } { a = 6, b = 8, sum = 14 }
let 子句
let子句接受表达式的求值,并将其分配给一个标识符,以便在其他求值中使用。let子句的语法如下:
let Identifier = Expression
例如,以下代码中的查询表达式将数组groupA的每个成员与数组groupB的每个元素配对。where子句从两个数组中删除两个和不等于 12 的每组整数。
` static void Main() { var groupA = new[] { 3, 4, 5, 6 }; var groupB = new[] { 6, 7, 8, 9 };
var someInts = from a in groupA from b in groupB let sum = a + b ← Store result in new variable. where sum == 12 select new {a, b, sum};
foreach (var a in someInts) Console.WriteLine(a); }`
该代码产生以下输出:
{ a = 3, b = 9, sum = 12 } { a = 4, b = 8, sum = 12 } { a = 5, b = 7, sum = 12 } { a = 6, b = 6, sum = 12 }
where 子句
如果项目不符合指定的条件,则where子句会将它们排除在进一步考虑之外。where子句的语法如下:
where BooleanExpression
关于where子句需要了解的重要事项如下:
- A query expression can have any number of
whereclauses as long as they are infrom...let...whereparagraph.- A project must meet all
whereclauses to avoid further consideration and exclusion.
以下代码显示了一个包含两个where子句的查询表达式示例。where子句从两个数组中删除每组整数,其中两个数组的和不大于或等于 11,并且groupA中的元素不是值 4。所选的每组元素必须满足两个 where子句的条件。
` static void Main() { var groupA = new[] { 3, 4, 5, 6 }; var groupB = new[] { 6, 7, 8, 9 };
var someInts = from int a in groupA from int b in groupB let sum = a + b where sum >= 11 ← Condition 1 where a == 4 ← Condition 2 select new {a, b, sum};
foreach (var a in someInts) Console.WriteLine(a); }`
该代码产生以下输出:
{ a = 4, b = 7, sum = 11 } { a = 4, b = 8, sum = 12 } { a = 4, b = 9, sum = 13 }
order by 子句
orderby子句接受一个表达式,并根据该表达式按顺序返回结果项。
图 19-8 显示了orderby子句的语法。可选关键字ascending和descending设置订单的方向。表达式通常是项的字段。该字段不一定是数值字段。它也可以是另一种可排序的类型,如字符串。
orderby
ascending
descending
- There can be any number of
orderbyclauses and they must be separated by commas.
图 19-8 。orderby 子句的语法
以下代码显示了一个按学生年龄排序的学生记录示例。请注意,学生信息数组存储在一个匿名类型数组中。
` static void Main( ) { var students = new [] // Array of objects of an anonymous type { new { LName="Jones", FName="Mary", Age=19, Major="History" }, new { LName="Smith", FName="Bob", Age=20, Major="CompSci" }, new { LName="Fleming", FName="Carol", Age=21, Major="History" } };
var query = from student in students orderby student.Age ← Order by Age. select student;
foreach (var s in query) { Console.WriteLine("{0}, {1}: {2} - {3}", s.LName, s.FName, s.Age, s.Major); } }`
该代码产生以下输出:
Jones, Mary: 19 - History Smith, Bob: 20 - CompSci Fleming, Carol: 21 - History
选择。。。集团条款
有两种类型的子句组成了select...group部分:select子句和group...by子句。虽然select...group部分前面的子句指定了数据源和要选择的对象,但是select...group部分执行以下操作:
- The
selectclause specifies which parts of the selected object should be selected. It can specify any of the following:
- Whole data item
- A field in a data item.
- A new object (or any other value) consisting of several fields in a data item.
- The
group...byclause is optional, and it specifies how to group the selected items. I will discuss thegroup...byclause later in this chapter.
图 19-9 显示了select...group子句的语法。
***图 19-9。*选择的语法。。。集团条款
下面的代码展示了一个使用select子句选择整个数据项的例子。首先,程序创建一个匿名类型的对象数组。然后,查询表达式使用select语句选择数组中的每一项。
` using System; using System.Linq; class Program { static void Main() { var students = new[] // Array of objects of an anonymous type { new { LName="Jones", FName="Mary", Age=19, Major="History" }, new { LName="Smith", FName="Bob", Age=20, Major="CompSci" }, new { LName="Fleming", FName="Carol", Age=21, Major="History" } };
var query = from s in students select s;
foreach (var q in query) Console.WriteLine("{0}, {1}: Age {2}, {3}", q.LName, q.FName, q.Age, q.Major); } }`
该代码产生以下输出:
Jones, Mary: Age 19, History Smith, Bob: Age 20, CompSci Fleming, Carol: Age 21, History
您还可以使用select子句来选择对象的特定字段。例如,如果用下面两条语句替换上例中相应的两条语句,代码将只选择学生的姓氏。
` var query = from s in students select s.LName;
foreach (var q in query) Console.WriteLine(q);`
通过这种替换,程序产生以下输出,仅打印姓氏:
Jones Smith Fleming
查询中的匿名类型
查询结果可以由源集合中的项、源集合中项的字段或匿名类型组成。
您可以在select子句中创建一个匿名类型,方法是用花括号将您想要包含在该类型中的以逗号分隔的字段列表括起来。例如,要使上一节中的代码只选择学生的姓名和专业,可以使用以下语法:
select new { s.LastName, s.FirstName, s.Major }; ↑ Anonymous type
下面的代码在select子句中创建一个匿名类型,并在后面的WriteLine语句中使用它。
` using System; using System.Linq;
class Program { static void Main() { var students = new[] // Array of objects of an anonymous type { new { LName="Jones", FName="Mary", Age=19, Major="History" }, new { LName="Smith", FName="Bob", Age=20, Major="CompSci" }, new { LName="Fleming", FName="Carol", Age=21, Major="History" } };
var query = from s in students select new { s.LName, s.FName, s.Major }; ↑ Create anonymous type. foreach (var q in query) Console.WriteLine("{0} {1} -- {2}", q.FName, q.LName, q.Major ); } •↑ } Access fields of anonymous type`
该代码产生以下输出:
Mary Jones -- History Bob Smith -- CompSci Carol Fleming -- History
群子句
group子句根据指定的标准对选定的对象进行分组。例如,对于前面示例中的学生数组,程序可以根据学生的专业对他们进行分组。
关于group条款需要了解的重要事项如下:
- When items are included in the query results, they are grouped according to the values of specific fields. The attribute of grouping items is called key .
- Queries with a group clause do not return an enumeration of items from the original source. Instead, it returns an enumerable value that enumerates the formed project groups.
- The group itself is enumerable and can enumerate the actual items.
group子句的语法示例如下:
group student by student.Major; ↑ ↑ Keyword Keyword
例如,以下代码根据专业对学生进行分组:
` static void Main( ) { var students = new[] // Array of objects of an anonymous type { new { LName="Jones", FName="Mary", Age=19, Major="History" }, new { LName="Smith", FName="Bob", Age=20, Major="CompSci" }, new { LName="Fleming", FName="Carol", Age=21, Major="History" } };
var query = from student in students group student by student.Major;
foreach (var s in query) // Enumerate the groups. { Console.WriteLine("{0}", s.Key); ↑ Grouping key foreach (var t in s) // Enumerate the items in the group. Console.WriteLine(" {0}, {1}", t.LName, t.FName); } }`
这段代码产生以下输出:
History Jones, Mary Fleming, Carol CompSci Smith, Bob
图 19-10 显示了从查询表达式返回并存储在查询变量中的对象。
- The object returned by the query is an enumerable object, which enumerates the groups obtained by the query.
- Each group is distinguished by a field called Key.
- Each group itself is enumerable and its items can be enumerated.
***图 19-10。*group 子句返回对象集合的集合,而不是对象的集合。
查询延续:into 子句
查询 continuation 子句获取查询的一部分的结果,并为其指定一个名称,以便可以在查询的另一部分中使用。图 19-11 显示了查询延续的语法。
***图 19-11。*查询延续子句的语法
例如,下面的查询连接了groupA和groupB,并将结果命名为groupAandB。然后从groupAandB执行简单的选择。
` static void Main() { var groupA = new[] { 3, 4, 5, 6 }; var groupB = new[] { 4, 5, 6, 7 };
var someInts = from a in groupA join b in groupB on a equals b into groupAandB ← Query continuation from c in groupAandB select c;
foreach (var a in someInts) Console.Write("{0} ", a); }`
该代码产生以下输出:
4 5 6
标准查询运算符
标准查询操作符由一组称为应用编程接口(API)的方法组成 .NET 数组或集合。标准查询运算符的重要特征如下:
- The collection object being queried is called a sequence, and the ienumerable < T > interface must be implemented, where t is the type.
- Standard query operators use method syntax.
- Some operators return IEnumerable objects (or other sequences), while others return scalars. Operators that return scalars immediately execute their queries and return a value instead of an enumerable object.
- Many of these operators take predicates as parameters. Predicate is a method that takes an object as a parameter and returns true or false according to whether the object meets certain criteria.
例如,以下代码显示了运算符Sum和Count的用法,它们返回int s。请注意以下代码:
- As a method , the operator acts directly on the object sequence , in this case, the array
numbers.- The return type is not
IEnumerableobject, butint.
` class Program { static int[] numbers = new int[] {2, 4, 6};
static void Main( ) { int total = numbers.Sum(); int howMany = numbers.Count(); ↑ ↑ ↑ Scalar Sequence Operator object Console.WriteLine("Total: {0}, Count: {1}", total, howMany); } }`
该代码产生以下输出:
Total: 12, Count: 3
有 47 个标准查询运算符。它们对一个或多个序列进行操作。一个序列是任何实现IEnumerable<>接口的类。这包括诸如List<>、Dictionary<>、Stack<>和Array这样的等级。标准查询操作符可以帮助您以非常强大的方式查询和操作这些类型的对象。
表 19-1 列出了这些运算符,并给出了足够的信息,让你知道每个运算符的用途和大致意思。然而,大多数都有几个重载,允许不同的选项和行为。你应该仔细阅读这个列表,熟悉这些可以节省你大量时间和精力的强大工具。当你需要使用它们的时候,你可以在网上查找完整的文档。
标准查询操作符的签名
标准查询操作符是在类System.Linq.Enumerable中声明的方法。然而,这些方法不是普通的方法——它们是扩展泛型类IEnumerable<T>的扩展方法。
我在第七章和第十七章中介绍了扩展方法,但是这是一个让您了解如何扩展的好机会 .NET 使用它们。这将为您自己的代码提供一个很好的模型,并让您更好地理解标准查询操作符。
回顾一下,回想一下扩展方法是公共的静态方法,虽然在一个类中定义,但它们被设计成向另一个不同的类添加功能,这个类被列为第一个形参。这个形参前面必须有关键字this。
例如,以下是三个标准查询操作符的签名:Count、First和Where。乍一看,这些可能有点吓人。请注意以下关于签名的内容:
- Because operators are generic methods, they have a generic parameter (
T) associated with their names.- Because operators are extension methods of extension class ienumerable < T >, they meet the following syntax requirements:
- They are declared as
publicandstatic.- They have a
thisextension indicator before the first parameter.- They take
IEnumerable<T>as the first parameter type.
Always Name and First public, static generic param parameter <ins> ↓ </ins> <ins> ↓ </ins> <ins> ↓ </ins> public static int Count<T>( this IEnumerable<T> source ); public static T First<T>( this IEnumerable<T> source ); public static IEnumerable<T> Where<T>( this IEnumerable<T> source, ... ); ↑ ↑ Return Extension method type indicator
为了显示直接调用扩展方法和将其作为扩展调用之间的语法差异,下面的代码使用两种形式调用标准查询操作符Count和First。两个操作符都只接受一个参数——对IEnumerable<T>对象的引用。
- The Count operator returns a single value, that is, the count of all elements in the sequence.
- The first operator returns the first element of the sequence.
在这段代码中,前两次使用了运算符,它们被直接调用,就像普通方法一样,将数组的名称作为第一个参数传递。但是,在下面的两行中,使用扩展语法调用它们,就好像它们是数组的方法成员一样。这是有效的,因为 .NET 类Array实现了IEnumerable<T>接口。
注意,在这种情况下,没有提供任何参数。相反,数组名已从参数列表移到方法名之前。在那里,它就像包含了方法的声明一样被使用。
方法语法调用和扩展语法调用在语义上是等价的——只是它们的语法不同。
` using System.Linq; ... static void Main( ) { int[] intArray = new int[] { 3, 4, 5, 6, 7, 9 }; Array as parameter ↓ var count1 = Enumerable.Count(intArray); // Method syntax var firstNum1 = Enumerable.First(intArray); // Method syntax
var count2 = intArray.Count(); // Extension syntax var firstNum2 = intArray.First(); // Extension syntax ↑ Array as extended object Console.WriteLine("Count: {0}, FirstNumber: {1}", count1, firstNum1); Console.WriteLine("Count: {0}, FirstNumber: {1}", count2, firstNum2); }`
该代码产生以下输出:
Count: 6, FirstNumber: 3 Count: 6, FirstNumber: 3
查询表达式和标准查询运算符
标准查询运算符集是一组用于执行查询的方法。正如在本章开始时提到的,每个查询表达式也可以使用带有标准查询操作符的方法语法来编写。编译器将每个查询表达式翻译成标准的查询操作符形式。
显然,由于所有的查询表达式都被翻译成标准的查询操作符,这些操作符可以执行查询表达式所做的一切。但是运算符也提供了查询表达式形式中没有的附加功能。例如,在前面的例子中使用的操作符Sum和Count只能用方法语法来表达。
但是,查询表达式和方法语法这两种形式可以结合使用。例如,下面的代码显示了一个也使用运算符Count的查询表达式。请注意,语句的查询表达式部分在括号内,后跟一个点和方法名。
` static void Main() { var numbers = new int[] { 2, 6, 4, 8, 10 };
int howMany = (from n in numbers where n < 7 select n).Count(); ↑ ↑ Query expression Operator
Console.WriteLine("Count: {0}", howMany); }`
该代码产生以下输出:
Count: 3
代表作为参数
正如您在上一节中看到的,每个操作符的第一个参数是对一个IEnumerable<T>对象的引用。其后的参数可以是任何类型。许多操作符将通用委托作为参数。(通用代表在第十七章的中进行了解释。)关于作为参数的泛型委托,最重要的事情是:
- Generic delegates are used to provide custom code to operators.
为了解释这一点,我将从一个例子开始,展示使用Count操作符的几种方法。Count操作符是重载的,有两种形式。正如您在前面的示例中看到的,第一个表单有一个返回集合中元素数量的参数。这里重复了它的签名:
public static int Count<T>(this IEnumerable<T> source);
但是,假设您只想计算数组的奇数个元素。要做到这一点,您必须为Count方法提供代码来确定一个整数是否是奇数。
为此,您需要使用第二种形式的Count方法,如下所示。作为它的第二个参数,它接受一个泛型委托。在调用它时,您必须提供一个委托对象,该对象接受一个类型为T的输入参数并返回一个布尔值。委托代码的返回值必须指定该元素是否应包括在计数中。
public static int Count<T>(this IEnumerable<T> source, <ins>Func<T, bool> predicate</ins> ); ↑ Generic delegate
例如,下面的代码使用第二种形式的Count操作符来指示它只包含那些奇数值。它通过提供一个 lambda 表达式来实现这一点,如果输入值是奇数,则返回true,否则返回false。(同样,lambda 表达式在第十三章的中讨论过。)在集合的每次迭代中,Count使用当前值作为输入来调用这个方法(由 lambda 表达式表示)。如果输入是奇数,该方法返回true,并且Count将该元素包含在总数中。
` static void Main() { int[] intArray = new int[] { 3, 4, 5, 6, 7, 9 };
var countOdd = intArray.Count(n => n % 2 == 1); ↑ Lambda expression identifying the odd values Console.WriteLine("Count of odd numbers: {0}", countOdd); }`
该代码产生以下输出:
Count of odd numbers: 4
LINQ 预定义的委托类型
像前面例子中的Count操作符一样,许多 LINQ 操作符要求您提供指导操作符如何执行操作的代码。您可以通过使用委托对象作为参数来实现这一点。
记住第十三章中的内容,你可以把委托对象看作是一个包含一个方法或一系列方法的对象,这些方法具有特定的签名和返回类型。当委托被调用时,它包含的方法按顺序被调用。
LINQ 定义了两类通用委托类型,用于标准查询操作符。这些是Func代表和Action代表。每个集合有 17 个成员。
- The delegate objects you create as actual parameters must be these delegate types or these forms.
TRrepresents the return type, and it is always the last of in the type parameter list.
这里列出了前四个通用Func委托。第一种形式不带方法参数,返回返回类型的对象。第二个函数接受单个方法参数并返回值,依此类推。
public delegate TR Func<out TR> ( ); public delegate TR Func<in T1, out TR > ( T1 a1 ); public delegate TR Func<in T1, in T2, out TR > ( T1 a1, T2 a2 ); public delegate TR Func<<ins>in T1, in T2, in T3, out TR</ins>>( <ins>T1 a1, T2 a2, T3 a3</ins> ); ↑ ↑ ↑ Return type Type parameters Method parameters
注意,返回类型参数有out关键字,使其成为协变的。因此,它可以接受声明的类型或从该类型派生的任何类型。输入参数有in关键字,使它们成为逆变的。因此,它们可以接受声明的类型或从该类型派生的任何类型。
记住这一点,如果您再次查看下面显示的Count的声明,您会看到第二个参数必须是一个委托对象,它将某个类型的单个值T作为方法参数,并返回一个类型为bool的值。正如本章前面提到的,这种形式的委托称为谓词。
public static int Count<T>(this IEnumerable<T> source, Func<T, bool> predicate ); ↑ ↑ Parameter type Return type
前四个Action代表如下。它们与Func委托相同,只是它们没有返回值,因此也没有返回值类型参数。它们的所有类型参数都是逆变的。
public delegate void Action ( ); public delegate void Action<in T1> ( T1 a1 ); public delegate void Action<in T1, in T2> ( T1 a1, T2 a2 ); public delegate void Action<in T1, in T2, in T3>( T1 a1, T2 a2, T3 a3 );
使用委托参数的例子
现在你更好地理解了Count的签名和 LINQ 对通用委托参数的使用,你将更好地理解一个完整的例子。
下面的代码声明了方法IsOdd,该方法接受一个类型为int的参数,并返回一个指定输入参数是否为奇数的bool值。方法Main执行以下操作:
- Declare an array of
intas the data source.- Create a entrusted object named
MyDelwith typeFunc<int, bool>, and initialize it with methodIsOdd. Note that you don't need to declare theFuncdelegate type, because, as you can see, it has been predefined by LINQ.- Call
Countwith the entrusted object.
` class Program { static bool IsOdd(int x) // Method to be used by the delegate object { return x % 2 == 1; // Return true if x is odd. }
static void Main() { int[] intArray = new int[] { 3, 4, 5, 6, 7, 9 };
Func<int, bool> myDel = new Func<int, bool>(IsOdd); // Delegate object var countOdd = intArray.Count(myDel); // Use delegate.
Console.WriteLine("Count of odd numbers: {0}", countOdd); } }`
该代码产生以下输出:
Count of odd numbers: 4
使用 Lambda 表达式参数的例子
前面的示例使用了一个单独的方法和一个委托将代码附加到运算符。这需要声明方法,声明委托对象,然后将委托对象传递给操作符。这种方法工作正常,并且如果满足以下任一条件,这就是正确的方法:
- If the method must be called from somewhere in the program instead of from the place where the delegate object is initialized.
- If the code in the method body is not just one or two long statements
但是,如果这两个条件都不成立,您可能希望使用一种更紧凑、更本地化的方法,通过 lambda 表达式向操作符提供代码。
我们可以通过首先完全删除IsOdd方法并将等效的 lambda 表达式直接放在 delegate 对象的声明中,来修改前面的示例以使用 lambda 表达式。新代码更短、更简洁,如下所示:
` class Program { static void Main() { int[] intArray = new int[] { 3, 4, 5, 6, 7, 9 }; Lambda expression ↓ var countOdd = intArray.Count( x => x % 2 == 1 );
Console.WriteLine("Count of odd numbers: {0}", countOdd); } }`
与前面的示例一样,此代码产生以下输出:
Count of odd numbers: 4
我们也可以使用匿名方法来代替 lambda 表达式,如下所示。不过这更冗长,因为 lambda 表达式在语义上是等价的,而且不太冗长,所以没有理由再使用匿名方法了。
` class Program { static void Main( ) { int[] intArray = new int[] { 3, 4, 5, 6, 7, 9 }; Anonymous method ↓ Func<int, bool> myDel = delegate(int x) { return x % 2 == 1; }; var countOdd = intArray.Count(myDel);
Console.WriteLine("Count of odd numbers: {0}", countOdd); } }`
LINQ 到 XML
可扩展标记语言(XML)是存储和交换数据的重要手段。LINQ 为这种语言增加了一些特性,使得处理 XML 比以前的方法(如 XPath 和 XSLT)容易得多。如果您熟悉这些方法,您可能会很高兴听到 LINQ 到 XML 以多种方式简化了 XML 的创建、查询和操作,包括:
- You can use a statement to create an XML tree in a top-down way.
- You can create and manipulate XML in memory without XML documents to contain trees.
- You can create and operate string nodes without
Textchild nodes.- One of the great differences (improvement! ) is that you don't have to traverse the XML tree to search for it. Instead, you just need to query the tree and let it return your results.
虽然我不会对 XML 进行完整的论述,但在描述 LINQ 提供的一些 XML 操作特性之前,我会先对它进行简单的介绍。
标记语言
一个标记语言是放置在文档中的一组标签,用来给出关于文档中信息的信息并组织其内容。也就是说,标记标签不是文档的数据——它们包含关于数据的数据。关于数据的数据称为元数据。**
标记语言是一组定义好的标记,用来表达关于文档内容的特定类型的元数据。例如,HTML 是最广为人知的标记语言。其标签中的元数据包含关于网页应该如何在浏览器中呈现以及如何使用超文本链接在页面之间导航的信息。
虽然大多数标记语言都包含一组预定义的标记,但 XML 只包含少数几个已定义的标记,其余的由程序员定义,以表示特定文档类型所需的任何种类的元数据。只要数据的作者和读者就标签的含义达成一致,标签就可以包含设计者想要的任何有用的信息。
XML 基础知识
XML 文档中的数据包含在 XML 树中,XML 树主要由一组嵌套元素组成。
元素是 XML 树的基本组成部分。每个元素都有一个名称,并且可以包含数据。有些还可以包含其他嵌套元素。元素由开始和结束标记来划分。元素包含的任何数据都必须在开始和结束标记之间。
- The opening tag begins with the opening angle bracket, followed by the element name, optionally followed by any attribute, followed by the closing angle bracket:
<PhoneNumber>- The closing tag begins with an opening angle bracket, followed by a slash character, followed by the element name, followed by the closing angle bracket:
</PhoneNumber>- Elements with no content can be represented by a single tag, which starts with an opening angle bracket, followed by the element name, followed by a slash, and finally ends with an ending angle bracket:
<PhoneNumber />
下面的 XML 片段显示了一个名为EmployeeName的元素,后跟一个名为PhoneNumber的空元素。
Opening tag Closing tag <ins> ↓ </ins> <ins> ↓ </ins> <EmployeeName><ins>Sally Jones</ins></EmployeeName> ↑ ↑ Content <PhoneNumber /> ← Element with no content
关于 XML,需要知道的其他重要事情如下:
The XML document must have a root element that contains all other elements.
XML tags must be nested correctly.
XML attributes are name/value pairs that contain additional metadata of elements. The value part of the attribute must always be enclosed in quotation marks, which can be double quotation marks or single quotation marks.
Keep white space in XML document. This is different from HTML, where white space is merged into one space in the output.
下面的 XML 文档是一个包含两名雇员信息的 XML 示例。为了清楚地显示元素,这个 XML 树非常简单。关于 XML 树,需要注意的重要事项如下:
- The tree contains a root node of type
Employees, which contains two child nodes of typeEmployee.- Each
Employeenode contains a node containing the employee's name and phone number.
<Employees> <Employee> <Name>Bob Smith</Name> <PhoneNumber>408-555-1000</PhoneNumber> <CellPhone /> </Employee> <Employee> <Name>Sally Jones</Name> <PhoneNumber>415-555-2000</PhoneNumber> <PhoneNumber>415-555-2001</PhoneNumber> </Employee> </Employees>
图 19-12 展示了样本 XML 树的层次结构。
***图 19-12。*样本 XML 树的层次结构
XML 类
LINQ 到 XML 可以通过两种方式处理 XML。第一种方式是作为简化的 XML 操作 API。第二种方法是使用你在本章前面看到的 LINQ 查询工具。我将从介绍 LINQ 到 XML API 开始。
LINQ 到 XML API 由许多代表 XML 树组件的类组成。您将使用的三个最重要的类是XElement、XAttribute和XDocument。还有其他的类,但是这些是主要的。
在图 19-12 中,你看到了 XML 树是一组嵌套的元素。图 19-13 显示了用于构建 XML 树的类以及它们是如何嵌套的。
例如,该图显示了以下内容:
XDocumentA node can have the following nodes as its direct child nodes:
- It is at most one of the following node types:
XDeclarationnode,XDocumentTypenode andXElementnode.- Any number of
XProcessingInstructionnodes- If there is a top-level
XElementnode underXDocument, it is the root of other elements in the XML tree.- The root can contain any number of nested
XElement,XCommentorXProcessingInstructionnodes in turn, and can be nested to any level.
***图 19-13。*XML 节点的包容结构
除了XAttribute类之外,大多数用于创建 XML 树的类都是从一个名为XNode的类中派生出来的,在文献中统称为 XNodes 。图 19-13 显示了白色云朵中的XNode类,而XAttribute类显示在灰色云朵中。
创建、保存、加载和显示 XML 文档
展示 XML API 的简单性和用法的最佳方式是展示简单的代码示例。例如,下面的代码显示了在使用 XML 时执行几项重要任务是多么简单。
它首先创建一个简单的 XML 树,由一个名为Employees的节点和两个包含两名雇员姓名的子节点组成。请注意以下关于代码的内容:
- A tree is created with a statement that creates all nested elements in the tree. This is called functional structure .
- Use the object to create an expression, and use the constructor of the node type to create each element in place.
创建树之后,代码使用XDocument的Save方法将它保存到一个名为EmployeesFile.xml的文件中。然后,它使用XDocument的静态Load方法从文件中读回 XML 树,并将树分配给一个新的XDocument对象。最后,它使用WriteLine显示新的XDocument对象持有的树的结构。
` using System; using System.Xml.Linq; // Required namespace
class Program { static void Main( ) { XDocument employees1 = new XDocument( // Create the XML document. new XElement("Employees", // Create the root element. new XElement("Name", "Bob Smith"), // Create element. new XElement("Name", "Sally Jones") // Create element. ) );
employees1.Save("EmployeesFile.xml"); // Save to a file.
// Load the saved document into a new variable. XDocument employees2 = XDocument.Load("EmployeesFile.xml"); ↑ Static method Console.WriteLine(employees2); // Display document. } }`
该代码产生以下输出:
<Employees> <Name>Bob Smith</Name> <Name>Sally Jones</Name> </Employees>
创建 XML 树
在前面的例子中,您看到了可以通过使用构造函数为XDocument和XElement在内存中创建 XML 文档。在两个构造函数的情况下
- The first parameter is the name of the object.
- The second and following parameters contain the nodes of the XML tree. The second parameter of the constructor is a
paramsparameter, so there can be any number of parameters.
例如,下面的代码生成一个 XML 树,并使用Console.WriteLine方法显示它:
` using System; using System.Xml.Linq; // This namespace is required.
class Program { static void Main( ) { XDocument employeeDoc = new XDocument( // Create the document. new XElement("Employees", // Create the root element. new XElement("Employee", // First employee element new XElement("Name", "Bob Smith"), new XElement("PhoneNumber", "408-555-1000") ),
new XElement("Employee", // Second employee element new XElement("Name", "Sally Jones"), new XElement("PhoneNumber", "415-555-2000"), new XElement("PhoneNumber", "415-555-2001") ) ) ); Console.WriteLine(employeeDoc); // Displays the document } }`
该代码产生以下输出:
<Employees> <Employee> <Name>Bob Smith</Name> <PhoneNumber>408-555-1000</PhoneNumber> </Employee> <Employee> <Name>Sally Jones</Name> <PhoneNumber>415-555-2000</PhoneNumber> <PhoneNumber>415-555-2001</PhoneNumber> </Employee> </Employees>
使用 XML 树中的值
当您遍历 XML 树并检索或修改值时,XML 的威力变得显而易见。表 19-2 显示了用于检索数据的主要方法。
关于表 19-2 中的方法,需要了解的一些重要事项如下:
Nodes: TheNodesmethod returns an object of typeIEnumerable<object>, because the returned nodes may be of different types, such asXElement,XCommentand so on. You can use the type parameterization methodOfType<*type*>to specify what type of node to return. For example, the following line of code only retrieves theXCommentnode:IEnumerable<XComment> comments = xd.Nodes().OfType<XComment>();Elements: Since searchingXElementsis such a common requirement, there is a shortcut for expressionNodes().OfType<XElement>()-methodElements.
- Use the
Elementsmethod without parameters to return all childrenXElement- Use the
Elementsmethod with a single name parameter to return only the childXElements with that name. For example, the following line of code returns all childXElementnodes named phonenumber .IEnumerable<XElement> empPhones = emp.Elements("PhoneNumber");Element: This method only takes the first child nodeXElementof the current node. Like theElementsmethod, it can be called with one parameter or without parameters. If there is no parameter, it will get the first child nodeXElement. Using a single name parameter, it gets the first child nodeXElementof the name.DescendantsandAncestors: These methods work in a similar way toElementsandParentmethods, but they do not return direct child elements or parent elements, but include elements below or above the current node, regardless of the nesting level.
下面的代码说明了Element和Elements方法:
` using System; using System.Collections.Generic; using System.Xml.Linq;
class Program { static void Main( ) { XDocument employeeDoc = new XDocument( new XElement("Employees", new XElement("Employee", new XElement("Name", "Bob Smith"), new XElement("PhoneNumber", "408-555-1000")), new XElement("Employee", new XElement("Name", "Sally Jones"), new XElement("PhoneNumber", "415-555-2000"), new XElement("PhoneNumber", "415-555-2001")) ) ); Get first child XElement named "Employees" ↓ XElement root = employeeDoc.Element("Employees"); IEnumerable employees = root.Elements();
foreach (XElement emp in employees) { Get first child XElement named "Name" ↓ XElement empNameNode = emp.Element("Name"); Console.WriteLine(empNameNode.Value); Get all child elements named "PhoneNumber" ↓ IEnumerable empPhones = emp.Elements("PhoneNumber"); foreach (XElement phone in empPhones) Console.WriteLine(" {0}", phone.Value); } } }`
该代码产生以下输出:
Bob Smith 408-555-1000 Sally Jones 415-555-2000 415-555-2001
添加节点和操作 XML
您可以使用Add方法向现有元素添加子元素。Add方法允许您在单个方法调用中添加任意多的元素,而不管您添加的节点类型。
例如,下面的代码创建一个简单的 XML 树并显示它。然后,它使用Add方法向根元素添加一个节点。接下来,它第二次使用Add方法添加三个元素——两个XElement和一个XComment。注意输出中的结果:
` using System; using System.Xml.Linq;
class Program { static void Main() { XDocument xd = new XDocument( // Create XML tree. new XElement("root", new XElement("first") ) );
Console.WriteLine("Original tree"); Console.WriteLine(xd); Console.WriteLine(); // Display the tree.
XElement rt = xd.Element("root"); // Get the first element.
rt.Add( new XElement("second")); // Add a child element.
rt.Add( new XElement("third"), // Add three more children. new XComment("Important Comment"), new XElement("fourth"));
Console.WriteLine("Modified tree"); Console.WriteLine(xd); // Display modified tree. } }`
这段代码产生以下输出:
`
`Add方法将新的子节点放在现有子节点之后,但是您也可以使用AddFirst、AddBeforeSelf和AddAfterSelf方法将节点放在子节点之前和之间。
表 19-3 列出了操作 XML 的一些最重要的方法。请注意,有些方法应用于父节点,有些方法应用于节点本身。
处理 XML 属性
属性给出了关于一个XElement节点的附加信息。它们被放在 XML 元素的开始标记中。
当您在函数上构造 XML 树时,您可以通过在XElement构造函数的范围内包含XAttribute构造函数来添加属性。XAttribute构造函数有两种形式;一个接受名称和值,另一个接受对已经存在的XAttribute的引用。
下面的代码向root添加了两个属性。请注意,XAttribute构造函数的两个参数都是字符串;第一个指定属性的名称,第二个给出值。
` XDocument xd = new XDocument( Name Value new XElement("root", ↓ ↓ new XAttribute("color", "red"), // Attribute constructor new XAttribute("size", "large"), // Attribute constructor new XElement("first"), new XElement("second") ) );
Console.WriteLine(xd);`
这段代码产生以下输出。请注意,属性放在元素的开始标记内。
<root color="red" size="large"> <first /> <second /> </root>
要从XElement节点检索属性,使用Attribute方法,提供属性的名称作为参数。下面的代码创建了一个 XML 树,它的节点有两个属性— color和size。然后,它检索属性值并显示它们。
` static void Main( ) { XDocument xd = new XDocument( // Create XML tree. new XElement("root", new XAttribute("color", "red"), new XAttribute("size", "large"), new XElement("first") ) );
Console.WriteLine(xd); Console.WriteLine(); // Display XML tree.
XElement rt = xd.Element("root"); // Get the element.
XAttribute color = rt.Attribute("color"); // Get the attribute. XAttribute size = rt.Attribute("size"); // Get the attribute.
Console.WriteLine("color is {0}", color.Value); // Display attr. value. Console.WriteLine("size is {0}", size.Value); // Display attr. value. }`
该代码产生以下输出:
`
color is red size is large`
要删除一个属性,您可以选择该属性并使用Remove方法,或者对其父属性使用SetAttributeValue方法并将属性值设置为null。下面的代码演示了这两种方法:
` static void Main( ) { XDocument xd = new XDocument( new XElement("root", new XAttribute("color", "red"), new XAttribute("size", "large"), new XElement("first") ) );
XElement rt = xd.Element("root"); // Get the element.
rt.Attribute("color").Remove(); // Remove the color attribute. rt.SetAttributeValue("size", null); // Remove the size attribute.
Console.WriteLine(xd); }`
该代码产生以下输出:
<root> <first /> </root>
要向 XML 树添加属性或更改属性的值,可以使用SetAttributeValue方法,如下面的代码所示:
` static void Main( ) { XDocument xd = new XDocument( new XElement("root", new XAttribute("color", "red"), new XAttribute("size", "large"), new XElement("first")));
XElement rt = xd.Element("root"); // Get the element.
rt.SetAttributeValue("size", "medium"); // Change attribute value. rt.SetAttributeValue("width", "narrow"); // Add an attribute.
Console.WriteLine(xd); Console.WriteLine(); }`
该代码产生以下输出:
<root color="red" size="medium" width="narrow"> <first /> </root>
其他类型的节点
在前面的例子中使用的另外三种类型的节点是XComment、XDeclaration和XProcessingInstruction。下面几节将对它们进行描述。
xccomment
XML 中的注释由位于<!--和-->标记之间的文本组成。XML 解析器会忽略标记之间的文本。您可以使用XComment类在 XML 文档中插入文本,如下面的代码行所示:
new XComment("This is a comment")
这段代码在 XML 文档中生成以下行:
<!--This is a comment-->
扩展澄清
XML 文档以一行开始,这一行包括使用的 XML 版本、使用的字符编码类型以及文档是否依赖于外部引用。这是关于 XML 的信息,所以它实际上是关于元数据的元数据!这被称为 XML 声明,并使用XDeclaration类插入。下面显示了一个XDeclaration语句的示例:
new XDeclaration("1.0", "utf-8", "yes")
这段代码在 XML 文档中生成以下行:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
表达辛司酮的修饰
XML 处理指令用于提供关于如何使用或解释 XML 文档的附加数据。最常见的是,处理指令用于将样式表与 XML 文档相关联。
您可以使用XProcessingInstruction构造函数包含一个处理指令,该构造函数接受两个字符串参数——一个目标和一个数据字符串。如果处理指令采用多个数据参数,这些参数必须包含在XProcessingInstruction构造函数的第二个参数字符串中,如下面的构造函数代码所示。请注意,在本例中,第二个参数是一个逐字字符串,字符串中的文字双引号由两个连续的双引号表示。
new XProcessingInstruction( "xml-stylesheet", @"href=""stories"", type=""text/css""")
这段代码在 XML 文档中生成以下行:
<?xml-stylesheet href="stories.css" type="text/css"?>
下面的代码使用了所有三种结构:
static void Main( ) { XDocument xd = new XDocument( new XDeclaration("1.0", "utf-8", "yes"), new XComment("This is a comment"), new XProcessingInstruction("xml-stylesheet", @"href=""stories.css"" type=""text/css"""), new XElement("root", new XElement("first"), new XElement("second") ) ); }
这段代码在输出文件中产生以下输出。然而,使用xd的WriteLine不会显示声明语句,即使它包含在文档文件中。
`
`用 LINQ 对 XML 使用 LINQ 查询
您可以将 LINQ XML API 与 LINQ 查询表达式结合起来,产生简单而强大的 XML 树搜索。
下面的代码创建了一个简单的 XML 树,将其显示在屏幕上,然后保存到一个名为SimpleSample.xml的文件中。虽然这段代码中没有什么新内容,但是我们将在下面的例子中使用这个 XML 树。
` static void Main( ) { XDocument xd = new XDocument( new XElement("MyElements", new XElement("first", new XAttribute("color", "red"), new XAttribute("size", "small")), new XElement("second", new XAttribute("color", "red"), new XAttribute("size", "medium")), new XElement("third", new XAttribute("color", "blue"), new XAttribute("size", "large"))));
Console.WriteLine(xd); // Display XML tree. xd.Save("SimpleSample.xml"); // Save XML tree. }`
该代码产生以下输出:
<MyElements> <first color="red" size="small" /> <second color="red" size="medium" /> <third color="blue" size="large" /> </MyElements>
下面的示例代码使用一个简单的 LINQ 查询从 XML 树中选择一个节点子集,然后以几种方式显示它们。该代码执行以下操作:
- It selects only those elements whose names have five characters from the XML tree. Because the names of the elements are first , second and third , only the node names first and third meet the search criteria, so these nodes are selected.
- Displays the name of the selected element.
- Format and display the selected nodes, including node names and attribute values. Note that the attribute is retrieved using the
Attributemethod, and the attribute value is retrieved using theValueattribute.
` static void Main( ) { XDocument xd = XDocument.Load("SimpleSample.xml"); // Load the document. XElement rt = xd.Element("MyElements"); // Get the root element.
var xyz = from e in rt.Elements() // Select elements whose where e.Name.ToString().Length == 5 // names have 5 chars. select e;
foreach (XElement x in xyz) // Display the Console.WriteLine(x.Name.ToString()); // selected elements.
Console.WriteLine(); foreach (XElement x in xyz) Console.WriteLine("Name: {0}, color: {1}, size: {2}", x.Name, x.Attribute("color").Value, x.Attribute("size") .Value); ↑ ↑ } Get the attribute. Get the attribute’s value.`
该代码产生以下输出:
`first third
Name: first, color: red, size: small Name: third, color: blue, size: large`
下面的代码使用一个简单的查询来检索 XML 树的所有顶级元素,并为每个元素创建一个匿名类型的对象。第一次使用WriteLine方法显示了匿名类型的默认格式。第二个WriteLine语句显式格式化匿名类型对象的成员。
` using System; using System.Linq; using System.Xml.Linq;
static void Main( ) { XDocument xd = XDocument.Load("SimpleSample.xml"); // Load the document. XElement rt = xd.Element("MyElements"); // Get the root element.
var xyz = from e in rt.Elements() select new { e.Name, color = e.Attribute("color") }; ↑ foreach (var x in xyz) Create an anonymous type. Console.WriteLine(x); // Default formatting
Console.WriteLine(); foreach (var x in xyz) Console.WriteLine("{0,-6}, color: {1, -7}", x.Name, x.color.Value); }`
这段代码产生以下输出。前三行显示了匿名类型的默认格式。最后三行显示了在第二个WriteLine方法的格式字符串中指定的显式格式。
`{ Name = first, color = color="red" } { Name = second, color = color="red" } { Name = third, color = color="blue" }
first , color: red second, color: red third , color: blue`
从这些例子中,您可以看到,您可以轻松地将 XML API 与 LINQ 查询工具结合起来,以产生强大的 XML 查询功能。