21. 介绍LINQ

21.1 什么是LINQ

LINQ是集成到C#和Visual Basic.NET这 些语言中用于提供查询数据能力的一个新我。

在关系型数据库系统,数据被组织放入规范化很好的表中,并且通过简单而又强大的语言SQL来进行访问。SQL可以和数据库中的任何数据配合使用,因为数据被放入表中,并遵从一些严格的规则。

然而,在程序中却与数据库相反,保存在类对象或结构中的数据差异很大。因此,没有通用的查询语言来从数据结构中获取数据。自C#3.0引入LINQ之后,该语言便有了查询对象集合的能力。如下是LINQ的重要高级特性。

  • LINQ(发音为link)代表语言集成查询(Language Integrated Query)。
  • LINQ是.NET框架的扩展,它允许我们以数据库查询的方式查询数据集合。
  • C# 3.0包含整合LINQ到语言中的一些扩展,允许我们从数据库、程序对象集合以及XML文档中查询数据。
如下代码演示了一个最简单的使用LINQ的示例。在这段代码中,被查询的数据源是简单的int数组。语句中查询的定义就是from和select关键词。尽管查询在语句中被定义,但是查询其实是在最后的foreach语句中进行的。

        #region 示例
        static void Main(string[] args)
        {
            int[] numbers = { 2, 12, 5, 15 }; //数据源

            IEnumerable<int> lowNums =       //定义并存储查询
                                from n in numbers
                                where n < 10
                                select n;

            foreach (var x in lowNums)        //执行查询
                Console.Write("{0}", x); ;
        }
        #endregion




21.2 LINQ提供程序

 在之前的示例中,数据源是简单的int数组,它是程序在内存中的对象。然而,LINQ还可以和各种类型的数据源一起工作,比如SQL数据库、XML文档,等等。然而,对于每一种数据源类型,在其背后一定有根据该数据源类型实现LINQ查询的代码模块。这些代码模块被叫做LINQ提供程序(provider)。有关LINQ提供程序的要点如下:
    口微软提供常见数据源类型的一些LINQ提供程序,如图21-1所示。
    口我们可以使用任何支持LINQ的语言(在这里是C#)来查询有LINQ提供程序的数据源类
    型。
    口第三方在不断提供针对各种数据源类型的LINQ提供程序。



 匿名类型

在介绍LINQ查询特性的细节之前,让我们首先学习一下C# 3.0的允许我们创建无名类类型的特性。不足为奇,这被叫做匿名类型(anonymous type)。

 在第5章中,我们介绍了对象初始化器,它允许我们在使用对象创建表达式时初始化新类实例的字段和属性。只是提醒一下,这种形式的对象创建表达式由三部分组成:new关键词、类名或构造函数以及对象初始化器。对象初始化器在一组花括号内包含了逗号分隔的成员初始化列表。 创建匿名类型的变量使用相同的形式,但是没有列名和构造方法。如下的代码行演示了匿名类型对象创建的形式:



需要了解的有关匿名类型的重要事项如下:
口匿名类型只能和局部变量配合使用,不能用于类成员。
口由于匿名类型没有名字,我们必须使用var关键词作为变量类型。

        当编译器遇到匿名类型的对象初始化器时,它创建了一个有名字的新类类型。对于每一个成员初始化器,它推断其类型并在新的类中创建这个类型的私有变量,然后创建用于访问这个变量的读写属性。属性和成员初始化器具有相同的名字。匿名类型被构造后,编译器创建了这个类型的对象。

    除了对象初始化器的赋值形式,匿名类型的对象初始化器还有其他两种允许的形式:简单标识符和成员访问表达式。这两种形式叫做投影初始化器(projection initializer)。下面的变量声明演示了所有的三种形式。第一个成员初始化器是赋值形式,第二个是标识符形式,第三个是成员访问表达式。
    var student =new {Age=19,Major,Other.Name};
  例如,如下代码使用了所有的三种类型。注意,投影初始化器定义在匿名类型声明之前。Majorj是一个局部变量,Name是Other类的静态字段。

    刚才演示的映射初始化器形式和这里给出的赋值形式的结果一样:
    var student暑new{Age - Ae,hlae,Other.Name,Hajor.Hajor},
    尽管在代码中看不到匿名类型,对象浏览器却能看到。如果编译器遇到了另一个具有相同参数名、相同引用类型名和相同顺序的匿名类型,它会重用这个类型并直接创建新的实例,而不会创建新的匿名类型。



21.3 查询语法和方法语法

    有两种形式的语法可供我们在写LINQ查询时使用——查询语法和方法语法。
    口查询语法(query syntax)是声明形式的,看-:去和SQL语句很相似。查询语法使用查询表达式形式书写。
    口方法语法(method syntax)是命令形式的,它使用的是标准的方法调用。方法是一组叫做标准查询运算符的方法,本章稍后会介绍。
    口在一个查询中也可以组合两种形式。
    微软推荐使用查询语法,因为它更易读,能更清晰地表明查询意图,因此也更不容易出错。然而,有一些运算符必须使用方法语法来书写。


说明  查询表达式使用的查询语法会被c撑编译器转换为方法调用的形式.这两种形式在运行时性能上没有区别。
    如下代码演示了所有的三种查询形式。对于方法语法的那部分代码,你可能觉得Where方法的参数看起来很奇怪.这是lambda表达式(第1 5章也介绍过),在本章后面还会介绍它在LINQ中的使用。

        static void Main(string[] args)
        {
            int[] numbers = { 2, 5, 38, 31, 17, 16, 42 };

            var numsQuery = from n in numbers  //查询语法
                            where n < 20
                            select n;

            var numsMethod = numbers.Where(x => x < 20);  //方法语法

            int numsCount = (from n in numbers      //两种形式的组合
                             where n < 20
                             select n).Count();

            foreach (var x in numsQuery)
                Console.Write("{0}", x);
            Console.WriteLine();

            foreach( var x in numsMethod)
                Console.WriteLine("{0}",x);

            Console.WriteLine(numsCount);
        }




21.4 查询变量

LINQ查询可以返回两种类型的结果——一个枚举,它列出了满足查询参数的项列表;一个叫做标量(scalar)的单一值,它是满足查询条件的结果的某种摘要形式。语句执行查询并调用一个方法(Count)来返回从查询返回的项的总数。

    等号左边的变量叫做查询变量。尽管在前面的语句中查询变量的类型被显式定义了,我们还是可以使用var关键词替代变量名称来让编译器自行推断查询变量的类型。
    理解查询变量的内容很重要。在执行前面的代码后,lowNums查询变量不会包含查询的结果,它包含的是IEnumerable<int>类型的对象。查询变量numCount包含的是真实的整数值,它只能通过真实运行查询后获得。


    区别在于查询执行的时间,可以总结如下:
    口如果查询表达式返回枚举,查询一直到处理枚举时才会被执行。如果枚举被处理多次,查询就会被执行多次。
    口如果查询表达式返回标量,查询立即执行,并且把结果保存在查询变量中。




21.5 查询表达式的结构

    如图21-3所示,查询表达式由查询体后的from子句组成口有关查询表达式需要了解的一些重要事项如下:

  •  子句必须按照一定的顺序出现。
    • from子句和select_group子句这两部分是必须的。
    • 其他子句是可选的。
  • 在LINQ查询表达式中,select子句在表达式最后。这与SQL的SELECT语句在查询的开始处不一样。 C#这么做的原因之一是让Visual Studio智能感应能在我们输入代码时给我们更 多选项。
  • 可以有任意多的from.let...where子句,如图21-3所示。



from子句

from子句指定了要作为数据源使用的数据集合。它也引入了迭代变量。有关from子句的要点如下所示:

  • 迭代变量有序表示数据源的每一个元素。
  • from子句的语法如下:
    • Type是集合中元素的类型。这是可选的,因为编译器可以从集合来推断类型。
    • Item是迭代变量的名字。
    • Item是要查询的集合的名字。集合必须是可枚举的。

例:From Type Item in Items

迭代变量item会表现数组中的每一个元素,并且会被之后的where和select子句选择或丢弃。

        static void Main(string[] args)
        {
            int[] numbers = { 2, 5, 38, 31, 17, 16, 42 };

            var numsQuery = from n in numbers  //定义迭代变量n
                            where n < 20       //使用迭代变量
                            select n;          //使用迭代变量

            foreach (var x in numsQuery)
                Console.Write("{0}", x);
            Console.WriteLine();
        }

尽管LINQ的from子句和foreach语句非常相似,但是主要的不同点如下:

口foreach语句在遇到代码时就执行其主体,而from子句什么也不执行。它创建一个用于保存查询变量的可枚举对象。查询本身会在之后的代码中被执行或不被执行。

口foreach语句明确指定集合中的项需要按照次序,从第一个到最后一个。而from子句只是 声明性地规定了必须考虑集合中的每一个项,不规定其顺序。

join子句

    LINQ中的join子句和SQL中的JOIN子句很相似。如果你熟悉SQL中的联结,从概念上来说,LINQ中的联结对你来说应该不是新鲜事。不同的是,我们现在不但可以在数据库的表上这么做,而且还可以在集合对象上进行这个操作。如果你不熟悉联结或需要重新了解它,那么下面内容可能会帮你理清思路。


    需要先了解的有关联结的语法如下:

    口联结操作接受两个集合然后创建一个临时的对象集合,每一个对象包含原始集合对象中的所有字段。

    口使用连接来结合两个或更多集合中的数据。

    联结的语法如下,它指定了用于与之前子句中集合进行连接的第二个集合。



例: var query = from s in students 

join c in studentsInCourses on s.STID equals c.STID

什么是联结

LINQ中的联结接受两个集体然后创建一个临时的对象集合,每一个元素包含两个原始集合中的原始成员。



查询主体中的from....let...where片段

可选的from.....let....where部分是查询主体的第一部分,可以由任意数量的三个子句来组合—from子句、let子句和where子句。

1.from子句

查询表达式必须从from子句开始,后面跟的是查询主体。主体本身可以从任何数量的其他from子句开始,每一个from子句都指定了一个额外的数据集合并引入了要在之后运算的迭代变量,所有from子句的语法和含义都是一样的。

如下代码演示了这各止用法的一个示例。

  • 第一个from子句是查询表达式必须的子句。
  • 第二个from子句是第一个子句的查询主体。
  • select子句创建了一个匿名类型的对象。
        static void Main(string[] args)
        {
            var groupA = new[] {3,4,5,6};
            var groupB = new[] { 6, 7, 8, 9 };

            var someInts = from a in groupA
                           from b in groupB
                           where a > 4 && b <= 8
                           select new { a, b, sum = a + b };

            foreach(var a in someInts)
                Console.WriteLine(a);
        }

2.let子句

let子句接受一个表达式的运算并且把它赋值给一个需要在其他运算中使用的标识符。let子句的语法如下:

let Identifier=Expression

        static void Main()
        {
            var groupA = new[] { 3, 4, 5, 6 };
            var groupB = new[] { 6, 7, 8, 9 };

            var somInts = from a in groupA
                          from b in groupB
                          let sum = a + b       //在新的变量中保存结果
                          where sum == 12
                          select new { a, b, sum };
        }


3.where子句

where子句根据之后的运算来去除不符合指定条件的项。where子句的语法如下:

where BooleanExpression

有关where需要了解的重要事项如下:

  • 只要是在from...let...where部分中,查询表达式可以有任何多个where子句。
  • 一个项必须满足where子句才能避免在之后被过滤。

orderby子句

orderby子句接受一个表达式并根据表达式依次返回结果项。

orderby子句的语法:orderby Expression  [ascending/descending]

可选的ascending和descending关键词设置了排序方向。表达式通常是项的一个字段。

  • orderby子句的默认排序是升序,然而,我们可以使用asending和descending关键词显式地设置元素的排序为升序或降序。
  • 可以有任意多个子句,它们必须使用逗号分隔。
        static void Main()
        {
            var students = new[]{
                new {LName="Jones",FName="Mary",Age=19,Major="History"},
                new {LName="Smith",FName="Bob",Age=20,Major="Comsci"},
                new {LName="Fleming",FName="Carol",Age=21,Major="History"}
            };

            var query = from student in students
                        orderby student.Age  //根据Age排序
                        select student;
            foreach (var s in query)
            {
                Console.WriteLine("{0},{1}:{2}-{3}",s.LName,s.FName,s.Age,s.Major);
            }
        }


select...group子句

有两种类型的子句组成select...group部分—select子句以及group...by子句。select.....group部分之前的子句指定了数据源和要选择的对象,select....group部分的功能如下所示:

  • select子句指定所选对象的哪部分应该被select。它可以指定下面的任意一项:
    • 整个数据项
    • 数据项的一个字段
    • 数据项中几个字段组成的新对象(或类似其他值)。
  • group...by子句是可选的,用来指定选择的项如何被分组。

查询中的匿名类型

查询结果可以由原始集合的项、原始集合中项的一些字段或匿名类型组成。

我们可以以在select子句中把希望在类型中包括的字段以逗号分隔,并以花括号进行包围来创建匿名类型。

例如:要让之前部分的代码只选择学生姓名和主修课,我们可以使用如下的语法:

select new { s.LastName,s.FirstName,s.Major};


group子句

group子句把select的对象根据一些标准进行分组。例如,有了之前示例的学生数组,程序可以根据它们的主修课程进行分组。

有关group子句需要了解的重要事项如下:

  • 如果项包含在查询的结果中,它们就可以根据某个字段的值进行分组。作为分组依据的项叫做键(key)。
  • 和select子句不同,group子句不从原始的数据源中返回可枚举项的可枚举类型,而是返回可以枚举已经形成的项的分组的可枚举类型。
  • 分组本身是可枚举类型,它们可以枚举实际的项。
group子句语法的一个示例如下:

group student by student.Major;

例如,如下代码根据学生的主攸课程进行分组:

  static void Main()
        {
            var students = new[]{
                new {LName="Jones",FName="Mary",Age=19,Major="History"},
                new {LName="Smith",FName="Bob",Age=20,Major="Comsci"},
                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)
            {
                Console.WriteLine("{0}", s.Key);

                foreach (var t in s)
                {
                    Console.WriteLine("{0},{1}:{2}-{3}", t.LName, t.FName, t.Age, t.Major);
                }
            }
        }
这段代码产生了如下的输出:




下图演示了从查询表达式返回对象并保存于查询变量中。

  • 从查询表达式返回的对象是从查询中枚举分组结果的可枚举类型。
  • 每一个分组由一个叫键的字段区分。
  • 每一个分组本身是可枚举类型并且可以枚举它的项。

查询延续

查询延续子句可以接受查询的一部分结果并赋予一个名字,从而可以在查询的另一部分中使用。

例如,如下查询连接了groupA和groupB,并且命名为 groupAandB,然后从groupA和groupB中进行一个简单的select。

        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
                           from c in groupAandB
                           select c;

            foreach (var a in someInts)
                Console.WriteLine("{0} ", a);
        }





21.6 标准查询运算符

标准查询运算符由一系列叫做API的方法组成,它能让我们查询任何.NET数组或集合。有关标准查询运算符的重要特性如下:

  • 被查询的集合对象叫做序列,它必须实现IEnumerable<T>接口,T是类型。
  • 标准查询运算符使用方法语法。
  • 一些运算符返回IEnumerable对象(或其他序列),而其他的一些运算符返回标量。返回标题的运算符立即执行,而返回替代枚举类型对象的值会被延迟迭代。
例如,如下代码演示了Sum和Count运算符的使用,并且返回了int。代码需要注意的地方如下所示:
  • 用作方法的运算符直接作用于主对象,在这里就是numbers数组。
  • 返回类型不是IEnumerable对象,而是int。

 class Program
    {
        static int[] numbers = new int[] { 2, 4, 6 };

        static void Main()
        {
            int total = numbers.Sum();
            int howMany = numbers.Count();

            Console.WriteLine("Total:{0},Count:{1}", total, howMany); ;
        }
    }


47个标准查询运算符可以分成败14个不同的 分类,下面列出了这些分类。

标准查询运算符的分类
名字运算符数量描述
限制1根据选择的标准返回序列对象的子集
映射2选择最终返回序列对象的哪些部分
分隔4从序列路过或返回对象
联结2根据一些标准,返回联结两个序列的IEnumerable对象
合成1从两个产昴的序列产生一个序列
排序2根据提供的标准排序序列
分组1根据提供的标准分组序列
设置4在序列上进行设置操作
转换7把序列转化为数组、列表以及字典等各种形式。
判等1判断两个序列是否相等
元素9返回序列中的特定元素
生成3生成序列
量化3返回一个指定某个谓词对于序列是否为true的布尔值
聚合7返回一个表示序列特性的单一值。

查询表达式和标准查询运算符

每一个查询表达式还可以使用带有标准查询运算符的方法语法来编写。标准查询运算符是进行查询的一组方法。编译器把每一个查询表达式翻译成标准查询运算符形式。

很明显,由于所有查询表达式被翻译成标准查询运算符——运算符可以执行由查询表达式完成的任何操作,而且运算符还有查询表达式形式所不能提供的附加功能。例如,在之前示例中使用的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();
            Console.WriteLine("Count:{0}",howMany);
        }


标准查询运算符的签名

System.Linq.Enumerable类声明了标准查询运算符方法。然而,这些方法不仅仅是一些方法,它们是扩展了IEnumerable<T>泛型的扩展方法。

关于扩展方法,要记住的最重要的事情就是它们是公共的静态方法 ,尽管定义在一个类中,但目的是为另一类增加功能——所列的和第一个形参。参数前必须有关键词this。

例如,如下是三个运算符的签名:Count、First和Where。乍看上去运算符的签名很吓人。注意下面有关签名的事项:

  • 由于运算符是泛型方法,它们有与它们的名字相关联的泛型参数(T)。
  • 由于运算符是扩展IEnumerable的扩展方法,它们必须满足下面的语法条件:
    • 它们必须声明为public和static
    • 它们必须在第一个参数前有this扩展指标器
    • 它们必须是IEnumerable<T>作为第一个参数。

例如,如下代码演示了使用Count和First运算符。两个运算符都接受一个参数——IEnuerable<T>对象的引用。

  • Count运算符返回序列中所有元素数量的单一值。
  • First运算符返回序列中的第一个元素。
        前两次代码中使用的运算符都是直接调用的,和普通方法差不多,传入数组的名字作为第一个参数。然而,之后的两行代码使用扩展方法语法来调用,就好像它们是数组(enumerable)的方法成员一样。注意,在这里没能指定参数,而是数组名称从参数列表中移到了方法名称之前,用起来就好像它包含了方法的声明一样。
        直接语法调用和扩展语法和是完全相等的——除了语法不同之处。

例:

  static void Main()
        {
            var numbers = new int[] { 2, 6, 4, 8, 10 };

            var count1 = Enumerable.Count(numbers);     //直接调用
            var firstNum1 = Enumerable.First(numbers);  //直接调用

            var count2 = numbers.Count();       //扩展方法调用
            var firstNum2 = numbers.First();    //扩展方法调用

        }




委托作为参数

在之前的部分内容中我们已经看到了,每一个运算符的第一个参数是IEnumerable<T>对象的引用,之后的参数可以是任何类型。很多运算符接受泛型委托作为参数。有关把泛型委托作为参数需要了解的最重要事项:泛型委托用于给运算符提供用户自定义的代码。

为了解决该事项,我们首先从一个演示Count运算符的几种使用方式的示例开始。Count运算符被重载并且有两种形式。第一种形式在之前的示例中用过,如下所示,它有一个参数:

public static int Count<T>(this IEnumerable<T>  source);

和所有扩展方法差不多,我们可以使用标准静态方式形成或在一个扩展的类上使用实例方法的形式,如下面两行代码所示:

var count1 = Linq.Enumerable.Count(intArray); //静态方法形式

var count2 = intArray.Count(); //实例方法形式

在这两个实例中,查询编译给定的整数数组中的int的总数。然而,假设我们希望看看数组中奇数元素的总数,我们必须指定Count方法,检测整数是否是奇数的方法才能实现。

我们可能需要使用Count方法的第二种形式才能实现,如下所示。它有一个泛型委托作为其第二个参数。调用时,我们必须提供一个接受单个T类型的输入参数并返回布尔值的委托对象。委托代码的返回值必须指定元素是否包含在总数中。

public static int Count<T> (this IEnumerable<T> source,Func<T,bool> predicate);

例如,如下代码使用了第二种形式的Count运算符并指导它来包含那些奇数值。它通过提供一个lambda表达式来实现这个表达式在输入值是奇数时返回true,否则返回false。对于集合的每次遍历,Count调用这个方法并把输入作为当前值。如果输入的是奇数,方法返回true,Count会把这个元素包含在总数中。



        static void Main()
        {
            int[] intArray = new int[] { 3, 4, 5, 6, 7, 9 };

            var countOdd = intArray.Count(n => n % 2 == 1); //寻找奇数的Lambda表达式。

            Console.WriteLine("Count of odd numbers :{0}",countOdd);
        }


LINQ预定义的委托类型

和前面示例中的Count运算符差不多,很多LINQ运算符需要我们提供代码来指示运算符如何执行它的操作。我们通过把委托对象作为参数来实现。

在15章中我们把委托对象认为是一个包含指定参数和返回值的方法或方法列表的对象。当委托被调用时,包含它的方法会被连续调用。

LINQ定义了一套5种泛型委托类型与标准查询运算符一起使用。它们就是Func委托。

  • 我们用作实参的委托对象必须是这5类型或5种形式之一。
  • TR代表返回值,并且总是在类型参数列表中的最后一个。
在这里列出了5个泛型委托对象。第一种形式没有接受方法参数,返回一个object作为返回值。第二个接受单个方法参数并且返回一个值,依此推类。
  • public delegate TR Func<TR> ();
  • public delegate TR Func<T1,TR> (T1 a1);
  • public delegate TR Func<T1,T2,TR> (T1 a1,T2 a2);
  • public delegate TR Func<T1,T2,T3,TR> (T1 a1,T2 a2,T3 a3);
  • public delegate TR Func<T1,T2,T3,T4,T$> (T1 a1,T2 a2,T3 a3,T4 a4);
知道了这些,如果我来看一下Count的声明。我们可以发现第二个参数必须是委托对象,它接受单个T类型的参数作为方法参数并且返回一个bool类型的值。

public static int count<T>(thisIEnumerable<T> source,Func<T,bool> predicate);

产生一个布尔值的参数委托叫做谓词。


使用委托参数的示例

既然我们已经对Count签名以及LINQ中泛型委托参数有了更深入的理解,我们就可以更好地理解整个示例了。

如下代码先声明了IsOdd方法,它接受单个int类型的参数,并且返回表示输入参数是否是奇数的bool值。Main方法做了如下的事情:

  • 它声明了int数组作为数据源。
  • 它创建了一个类型为Func<int,bool>名称为MyDel的委托对象,并且使用IsOdd方法来初始化委托对象。注意,我们不需要声明Func代理类型,因为LINQ已经预定义了。
  • 它使用委托对象调用Count。

 class Program
    {
        static bool IsOdd(int x)    //委托对象使用的方法
        {
            return x % 2 == 1;
        }

        static void Main()
        {
            int[] intArray = new int[] { 3,4,5,6,7,9};

            Func<int, bool> myDel = new Func<int, bool>(IsOdd);   //委托对象
            var countOdd = intArray.Count(myDel);

            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};

            var countOdd = intArray.Count(x=>x%2==1);

            Console.WriteLine("Count of odd numbers:{0}",countOdd);             
        }
    }



21.7 LINQ to XML

在之前的几年里,XML已经变成一个保存和交换数据的重要方法。C# 3.0语言增加了一些特性来使我们与XML交互的工作比之前的XPath和XSLT等类似的方式更简单。如果你熟悉这些方式,可能会很愿意听到LINQ to XML在很多方面简化了XML的创建、遍历和操作,包括如下方面:

  • 我们可以使用单一语句自顶向下创建XML树。
  • 我们可以不使用包含树的XML文档在内存中操作XML。
  • 我们可以不命名用Text子节点来创建和操作字符串节点。

标记语言

标记语言(markup language)是文档中的一组标签,它提供有关文档的信息。也就是说,标记标签不是文档的数据——它们包含有关数据的数据。有关数据的数据称为元数据。
标记语言是被定义的一组标签,旨在在传递有关文档内容的特定类型的元数据。例如,HTML是众所周知的标记语言。标签中的元数据包含了Web页面如何在浏览器中呈现以及如何使用超链接在页面中导航的信息。
大多数标记语言包含一组预定义的标签,而XML只包含少量预定义的标签,其他由程序员来定义,来表示特定文档类型需要的任何元数据。只要数据的读者和编写者都知道标签的含义,标签就可以包含任何设计者希望的有用信息。

XML基础

XML文档中的数据包含了一个XML树,它主要由嵌套元素组成。

元素是XML树的基本要求。每一个元素都有名字并且包含数据,一些元素还可以包含其他被嵌套的元素。元素由开始和关闭标签进行划分。任何元素包含的数据都必须介于开始和关闭标签之间。

  • 开始标签从一个小于号开始,后面跟元素名,紧接着是可选的任何特性,最后是大于号。<PhoneNumber>
  • 关闭标签从一个小于号开始,后面是斜杠,然后是元素名和大于号。</PhoneNumber>
  • 没有内容的元素可以直接由单个标签表示,从小于号开始,后面是元素名和斜线,并使用大于号结束<PhoneNumber />
其他需要的有关XML的重要事项如下:
  • XML文档必须有一个根元素来包含所有其他元素。
  • XML标签必须合理嵌套。
  • 与HTML标签不同,XML标签是区分大小写的。
  • XML特性是名字/值的配对,它包含了元素的额外元数据。特性的值部分必须包含在引号内,可以是单引号也可以是双引号。
  • XML文档中的空格是有效的。这与把空格作为单个空格输出的HTML不同。

XML类

LINQ to XML可以以两种方式和XML配合用。第一种方式是作为简化的XML操作API,第二种是使用本章前面看到的LINQ查询工具。
LINQ to XML API 由很多表示XML树组件的类组成。会使用的最重要的类包括XElement、XAttribute和XDocuemnt。当然,还有其他类,但这些是主要的。

从下方的图中我们可以看到,XML树是一组被嵌套元素。显示了如下内容:

  • 可作为XDocuemnt节点的直接子节点:
    • 大多数情况下,下面的第一个节点类型各有一个:XDeclaration节点、XDocuemnt节点以及XElement节点。
    • 任何数量的XProcessingInstruction节点。
  • 如果在XDocuemnt中有最高级别的XElement节点,那么它就是XML树中其他元素的根。
  • 根元素就可以包含任意数量的嵌套XElement、XComment和XProcessingInstruction 节点,在任何级别上嵌套。

除了XAttribute类,大多数用于创建XML树的类都从一个叫做XNode的类继承。


1.创建、保存、加载和显示XML文档


using System;
using System.Xml.Linq; //需要使用的命名空间

namespace ConsoleApplication5
{
    class Program
    {
        static void Main()
        {
            XDocument employees1 = new XDocument(       //创建XML文档
                        new XElement("Employees",       //创建根元素
                               new XElement("Name", "Bob Smith"), //创建无素
                                new XElement("Name", "Sally Jones")  //创建元素
                        )
             );

            employees1.Save("EmployeesFile.xml");       //保存到文件

            //将保存的文档加载到新变量中
            XDocument employee2 = XDocument.Load("EmployeesFile.xml");

            Console.WriteLine(employee2);               //显示文档
        }
    }
}

2. 创建XML树

在之前的救命中,我们已经知道了能通过使用XDocuemnt和XElement的构造函数在内在中创建一个XML文档。在这里,两个构造函数:

  • 第一个参数都是对象名。
  • 第二个参数以及之后的参数包含了XML树的节点。构造函数的第二个参数是一个params参数,也就是说可以有任意多的参数。

使用XML树的值

当我们遍历XML树来获取或修改值时才体现了XML的强大。以表给出了用于获取数据的主要方法。

查询XML的方法
方法名称返回类型描述
NodesXDocuemnt
XElement
IEnumerable<object>返回当前节点的所有子节点(不管是什么类型)
ElementsXDocuemnt
XElement
IEnumerable<XElement>返回当前节点的XElement子节点,或所有具有鞭个名字的子节点。
ElementXDocuemnt
XElement
XElement返回当前节点的第一个XElement子节点,或具有某个名字的子节点。
DescendantsXElementIEnumerable<XElement>返回所有的XElement子代节点,或所有具有某个名字的XElement子代节点,不管它们处于当前节点下嵌套的什么层次。
DesendantsAndSelfXElementIEnumerable<XElement>和Descendants一样,但是包括当前节点
AncestorsXElementIEnumerable<XElement>返回所有上级XElement节点,或者所有具有某个名字的上级XElemnt节点。
AncestorsAndSefXElemntIEnumerable<XElement>和Ancestors一样,但是包括当前节点。
ParentXElementXElement返回当前节点的父节点。

关于上表,需要了解的一些重要事项如下:

  • Nodes:Nodes方法返回IEnumerable<object>类型的对象,因为返回的节点可能是不同的类型,比如XElement、XComment等。我们可以使用以类型作为参数的方法ofType(type)来指定返回某个类型的节点。例如,如下代码只获取XComment节点。

IEnumerable<XComment> comments = xd.Nodesp[.ofType<XComment>();

  • Elements:由于获取XElements是一个非常普遍的要求,就出出Nodes.OfType(XElement)()表达式的简短形式——Elements方法。
    • 使用无参数的Elements方法返回所有子XElements。
    • 使用单个name参数的Elements方法只返回具有这个名字的子XElements。例如,如下代码行的所有具有名字PhoneNumber的子XElement节点。

IEnumerable<XElement> empPhones = emp.Elements("PhoneNumber");


  • Element:这个方法只获取当前节点的第一个子XElemnt。与Elements相似,它可以带一个参数或不带参数主周用。如果没有参数,获取第一个子XElement节点。如果速一个参数,它获取第一个具有那个名字的子XElement。
  • Descendants和Ancestors:这些方法和Elements以及Parent方法差不多,只不过它们不返回直接的子元素或父元素,而是忽略嵌套级别,包括所有之下或者之上的节点。
如下代码演示了Elemnt和Elemnts的方法:
using System;
using System.Collections.Generic;
using System.Xml.Linq; //需要使用的命名空间

namespace ConsoleApplication5
{
    class Program
    {
        static void Main()
        {
            XDocument employeeDoc = new XDocument(       //创建XML文档
                        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", "408-555-123"),
                                   new XElement("PhoneNumber", "408-555-1042"))

                        )
             );

            XElement root = employeeDoc.Element("Employees");  //获取第一个名为"Employees"的XElement
            IEnumerable<XElement> employees = root.Elements();

            foreach (XElement emp in employees)
            {
                XElement empNameNode = emp.Element("Name");  //获取第一个名为"Name"的XElement
                Console.WriteLine(empNameNode.Value);

                IEnumerable<XElement> empPhones = emp.Elements("PhoneNumber");
                foreach (XElement phone in empPhones)
                    Console.WriteLine(" {0}", phone.Value);
            }       
        }
    }
}


增加节点以及操作XML
我们可以add方法为现有增加子元素。Add方法允许我们在一次调用中, 增加的节点的类型什么,增加希望的任意多一个元素。
例如,台下的代码创建了XML树并显示它,然后使用Add()方法为根元素增加单个节点。之后,它再次使用add方法来增加三个元素——两个XElements和一个XComment。


using System;
using System.Collections.Generic;
using System.Xml.Linq;

namespace ConsoleApplication5
{
    class Program
    {
        static void main()
        {
            XDocument xd = new XDocument(
                                new XElement("root", new XElement("first")
                                )
            );

            Console.WriteLine("Original tree");
            Console.WriteLine(xd); Console.WriteLine();

            XElement rt = xd.Element("root");

            rt.Add(new XElement("second")); //添加子元素

            rt.Add(new XElement("thrid"),
                    new XComment("Importtant Comment"),
                    new XElement("fourth"));

            Console.WriteLine("Modified tree");
            Console.WriteLine(xd);
        }
    }
}


Add方法把新的子节点放在既有子节点之后,如果我们希望把节点放在子节点之前或者之间也是可以的,使用AddFirst、AddBeforeSelf和AddAfterSelf方法即可。

下表列出了最重要的一些操作XML的方法。注意,某些主法针对父节点而其他一些方法针对节点本身。

操作XML的方法
方法名称从哪里调用描述
AddParent在当前节点的既有子节点后增加新的子节点
AddFirstParent在当前节点的既有子节点前增加新的子节点
AddBeforeSelfNode在同级别的当前节点之 前增加新的节点
AddAfterSelfNode在同级别的当前节点之后增加新的节点
RemoveNode删除当前所选的节点及其内容
RemoveNodesNode删除当前所选的XElement及其内容。
SelfElementParent设置节点的内容
ReplaceContentNode替换节点的内容。



使用XML属性
属性提供了有关XElemet节点的额外信息,它放在XML元素的开始标签中。
当我们以函数方法构造XML树时,可以只需要在XElement的构造函数中包含XAttribute构造函数来增加特性。XAttribute构造函数有两种形式,一种是接受name和value,另一种是接受现有XAttribute的引用。

如下代码为root增加了两个特性。注意,提供给XAttribute构造函数的两个参数都是字符串,第一个指定了特性名,而第二个指定了值。


        static void main()
        {
            XDocument xd = new XDocument(
                                new XElement("root", 
                                    new XAttribute("color","red"),
                                    new XAttribute("size","large"),
                                    new XElement("first"),
                                    new XElement("second")
                                )
            );

           
        }

要从一个XElement节点获取特性可以使用Attribute方法,提供特性名作为参数发邓可。下面的代码创建了一个在一个节点中有两个特性(color和size)的XML树,然后从特性获取值并且显示它们。


  static void main()
        {
            XDocument xd = new XDocument(
                                new XElement("root", 
                                    new XAttribute("color","red"),
                                    new XAttribute("size","large"),
                                    new XElement("first"),
                                    new XElement("second")
                                )
            );

            XElement rt = xd.Element("rrot");

            XAttribute color = rt.Attribute("color");
            XAttribute size = rt.Attribute("size");

            Console.WriteLine("color is {0}",color.Value);
            Console.WriteLine("size is {0}", size.Value);
           
        }


要移除我,我们可以选择一个我然后使用Remove方法,或在它的父节点中使用SetAttributeValue方法把特性值设置为null。下面是两种方法的演示。
        static void main()
        {
            XDocument xd = new XDocument(
                                new XElement("root", 
                                    new XAttribute("color","red"),
                                    new XAttribute("size","large"),
                                    new XElement("first"),
                                    new XElement("second")
                                )
            );

            XElement rt = xd.Element("rrot");
            rt.Attribute("color").Remove();
            rt.SetAttributeValue("size", null);

            Console.WriteLine(xd);
        }

要向XML树增加一个我或改变特性的值,我们可以使用SetAttributeValue方法,如下代码所示:

        static void main()
        {
            XDocument xd = new XDocument(
                                new XElement("root", 
                                    new XAttribute("color","red"),
                                    new XAttribute("size","large"),
                                    new XElement("first"),
                                    new XElement("second")
                                )
            );

            XElement rt = xd.Element("rrot");
            
            rt.SetAttributeValue("size", "medium"); //改变特性值
            rt.SetAttributeValue("width", "narrow");//添加特性

            Console.WriteLine(xd);
        }



节点的其他类型

前面示例中使用的其他三个类型的节点是XComment、XDecalration以及XProcessingInstruction,见下面部分的描述。

1.XComment

XML注释由<!--和-->记号之间的文本组成。记号之间的文本会被XML解释器忽略。我们可以使用XComment类向一个XML文档插入文本,如下面的代码行所示:

new XComment("This is a comment")


2.XDeclaration

XML文档从包含XML使用的版本号、使用的字符编码类型以及文档是否依赖外部引用的一行开始。这叫做XML声明,可以使用XDeclaration类来插入,如下代码给出了一个XDeclaration语句的示例。

new XDeclaration("1.0","utf-8","yes")


3.XProcessingInstruction

XML处理指令用于提供XML文档如何被使用和翻译的额外数据,最常见的就是把处理指令用于关联XML文档和一个样式表。

我们可以使用XProcessInstruction构造函数来包含处理指令。它接受两个字符串参数——目标和数据源。如是要处理指令接受多个数据参数,这些参数必须包含在XProcessingInstruction构造函数的第二个字符串参数中,如下面的构造方法代码所示。注意,在这里的示例中,第二个参数是一个字符串,在字符串中的双引号文本使用两个连续的双引号来表现。

new XProcessingInstruction("xml-stylesheet",@"href"=""stories"",type=""text/css""")


使用LINQ to XML的LINQ查询

现在,我们可以把LINQ XML API 和LINQ查询表达式组合在一地卢来产生简单而强大的XML树搜索。

下面的代码创建了一个简单的XML树,并把它显示在了屏幕上,然后把它保存在一个叫做SimpleSample.xml的文件中。尽管代码没有什么新内容,但是我们会将这个XML树用于之后的示例。



using System;
using System.Collections.Generic;
using System.Xml.Linq;

namespace ConsoleApplication5
{
    class Program
    {
        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);
            xd.Save("SimpleSample.xml");
        }
    }
}

如下示例代码使用了简单的LINQ查询来从XML中查询节点的子集,然后以各种方式进。这段代码做了如下的事情:
  • 它从XML树中选择那些名字有5个字符的元素。由于这些元素的名字是first、second和third,只有first和third这两个名字符合搜索标准,因此这些节点被选中。
  • 它显示了所选元素的名字。
  • 它格式化并显示了所选节点,包括节点名以及特性值。注意, 特性使用Attribute方法来获取,并且特性的值使用Value属性来获取。
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;

namespace ConsoleApplication5
{
    class Class1
    {
        static void main()
        {
            XDocument xd = XDocument.Load("SimpleSample.xml");  //加载文档
            XElement rt = xd.Element("MyElements");

            var xyz = from e in rt.Elements()
                      where e.Name.ToString().Length == 5
                      select e;
                      
            foreach ( XElement x  in xyz)
                Console.WriteLine(x.Name.ToString());
            
            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);
        }
    }
}


如下代码使用了一个简单的查询来获取XML树的所有顶层元素,并且为每一个元素创建了一个匿名类型的对象。第一次使用WriteLine方法来显示匿名类型的默认格式,第二次使用WriteLine语句来显示格式化匿名类型对象的成员。

        static void main()
        {
            XDocument xd = XDocument.Load("SimpleSample.xml");  //加载文档
            XElement rt = xd.Element("MyElements");

            var xyz = from e in rt.Elements()
                      select new { e.Name,color=e.Attribute("color")};
                      
            foreach ( var x  in xyz)
                Console.WriteLine(x);
            
            Console.WriteLine();
            foreach (var x in xyz)
                Console.WriteLine("{0,-6},      color:{1,-7}", x.Name, x.color.Value);
        }


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值