首页 > 代码库 > C#图解教程 第十九章 LINQ

C#图解教程 第十九章 LINQ

LINQ
什么是LINQ
LINQ提供程序
匿名类型
方法语法和查询语法
查询变量
查询表达式的结构
from子句
join子句
什么是联结
查询主体中的from…let…where片段
from子句
let子句
where子句
orderby子句
select…group子句
查询中的匿名类型
group子句
查询延续:into子句
标准查询运算符
标准查询运算符的签名
查询表达式和标准查询运算符
将委托作为参数
LINQ预定义的委托类型
使用委托参数的示例
使用Lamba表达式参数的示例
LINQ to XML
标记语言
XML基础
XML类
创建、保存、加载和显式XML文档
创建XML树
使用XML树的值
增加节点以及操作XML
使用XML特性
节点的其他类型
XComment
XDeclaration
XProecssingInstruction
使用LINQ to XML的LINQ 查询

LINQ

什么是LINQ


在关系型数据库系统中,数据被组织放入规范化很好的表中,并且通过简单且强大的SQL语言来进行访问。因为数据在表中遵从某些严格的规则,所以SQL可以和它们很好的配合使用。
然而,在程序中却与数据库相反,保存在类对象或结构中的数据差异很大。因此,没有通用的查询语言来从数据结构中获取数据。从对象获取数据的方法一直都是作为程序的一部分而设计的。然而使用LINQ可以很轻松地查询对象集合。
如下是LINQ的重要高级特性。

  • LINQ(发音link)代表语言集成查询(Language Integrated Query)
  • LINQ是.NET框架的扩展,它允许我们以使用SQL查询数据库的方式来查询数据集合
  • 使用LINQ,你可以从数据库、程序对象集合以及XML文档中查询数据

例:LINQ示例

class Program
{
    static void Main()
    {
        int[] numbers={2,12,5,15};
        IEnumerable<int> lowNums=
                           from n in numbers
                           where n<10
                           select n;
        foreach(var x in lowNums)
        {
            Console.WriteLine(x);
        }
    }
}

技术分享

LINQ提供程序


在之前的示例中,数据源只是int数组,它是程序在内存中的对象。然而,LINQ还可以和各种类型的数据源一起工作。然而,对于每种数据源类型,在其背后一定有根据该数据源类型实现LINQ查询的代码模块。这些代码模块叫做LINQ提供程序(provider)。
有关LINQ提供程序的要点如下

  • 微软为一些常见的数据源类型提供了LINQ Provider
  • 第三方在不断提供针对各种数据源类型的LINQ Provider

技术分享

本章中,我们主要介绍LINQ并解释如何将其用于程序对象(LINQ to Object)和XML(LINQ to XML),其他细节和用法不做讨论。

匿名类型

在介绍LINQ查询特性的细节前,我们先学习一个允许我们创建无名类类型的特性。匿名类型(anonymous type)经常用于LINQ查询的结果中。
第6章介绍了对象初始化语句,它允许我们在使用对象创建表达式时初始化新类实例的字段和属性。提醒一下,这种形式的对象创建表达式由三部分组成:new关键字、类名或构造函数以及对象初始化语句。对象初始化语句在一组大括号内包含了以逗号分隔的成员初始化列表。
创建匿名类型的变量使用相同的形式,但是没有类名和构造函数。如下的代码行演示了匿名类型的对象创建表达式:

没有类名
   ↓
new {FieldProp=InitExpr,FieldProp=InitExpr,...}
              ↑
        成员初始化语句

例:创建和使用匿名类型的示例。

class Program
{
    static void Main()
    {
     必须使用var
         ↓
        var student=new{Name="Mary Jones",Age=19,Major="History"};
        Console.WriteLine("{0},Age {1},Major: {2}",student.Name,student.Age,studeng.Major);
    }
}

技术分享

需要了解的有关匿名类型的重要事项如下。

  • 匿名类型只能和局部变量配合使用,不能用于类成员
  • 由于匿名类型没有名字,我们必须使用var关键字作为变量类型
  • 不能设置匿名类型对象的属性。编译器为匿名类型创建的属性是只读的

当编译器遇到匿名类型的对象初始化语句时,它创建一个有名字的新类类型。低于每个成员初始化语句,它推断其类型并创建一个只读属性来访问它的值。属性和成员初始化语句具有相同名字。匿名类型被构造后,编译器创建了这个类型的对象。
除了对象初始化语句的赋值形式,匿名类型的对象初始化语句还有其他两种允许的形式:简单标识符和成员访问表达式。这两种形式叫做投影初始化语句(projection initializer)。下面的变量声明演示了3种形式。

var student=new{Age=19,Other.Name,Major};

例:使用3总初始化语句。注意,投影初始化语句必须定义在匿名类型声明之前。

class Other
{
    static public string Name="Mary Jones";
}
class Program
{
    static void Main()
    {
        string Major="History";
        var student=new{Age=19,Other.Name,Major};
        Console.WriteLine("{0},Age {1},Major: {2}",student.Name,student.Age,studeng.Major);
    }
}

如果编译器遇到了另一个具有相同的参数名、相同的推断类型和相同顺序的匿名类型,它会重用这个类型并直接创建新的实例,不会创建新的匿名类型。

方法语法和查询语法


我们在写LINQ查询时可以使用两种形式的语法:方法语法和查询语法。

  • 方法语法(method syntax)使用标准的方法调用。这些方法是一组标准查询运算符的方法
  • 查询语法(query syntax)看上去和SQL语句相似
  • 在一个查询中可以组合两种形式

方法语法是命令式(imperative)的,它指明了查询方法调用的顺序。
查询语法是声明式(declarative)的,即查询描述的是你想返回的东西,但并么有指明如何执行这个查询。
编译器会将使用查询语法表示的查询翻译为方法调用的形式。这两种形式在运行时没有性能上的差异。
微软推荐使用查询语法,因为它更易读,能更清晰地表明查询意图,因此也更不容易出错。然而,有些运算符必须使用方法语法来书写。

例:方法语法和查询语法演示

class Program
{
    static void Main()
    {
        int[] numbers={2,5,28,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.Write("{0}, ",x);
        }
        Console.WriteLine();
        Console.WriteLine(numsCount);
    }
}

技术分享

查询变量


LINQ查询可以返回两种类型的结果–可以是一个枚举(可枚举的一组数据,不是枚举类型),它满足查询参数的项列表;也可以是一个叫做标量(scalar)的单一值,它是满足查询条件的结果的某种摘要形式。

例:查询变量示例

int[] numbers={2,5,28};
IEnumerable<int> lowNums=from n in numbers //返回枚举数
                         where n<20
                         select n;
int numsCount=(from n in numbers           //返回一个整数
               where n<20
               select n).Count();

理解查询变量的用法很重要。在执行前面的代码后,lowNums查询变量不会包含查询的结果。相反,编译器会创建能够执行这个查询的代码。
查询变量numCount包含的是真实的整数值,它只能通过真实运行查询后获得。
区别在于查询执行的时间,可总结如下:

  • 如果查询表达式返回枚举,查询直到处理枚举时才会执行
  • 如果枚举被处理多次,查询就会执行多次
  • 如果在进行遍历后,查询执行之前数据有改动,则查询会使用新的数据
  • 如果查询表达式返回标量,查询立即执行,并且把结果保存在查询变量中

查询表达式的结构


查询表达式由查询体后的from子句组成。有关查询表达式需要了解一些重要事项:

  • 子句必须按照一定顺序出现
  • from子句和select…group子句这两部分是必需的
  • LINQ查询表达式中,select子句在表达式最后。C#这么做的原因之一是让Visual Studio智能感应能在我们输入代码时给我们更多选项
  • 可以有任意多的from…let…where子句

技术分享

from子句

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

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

下图演示了from子句的语法。类型说明符是可选的。可以有任意多个join子句。
技术分享

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

  • foreach语句命令式地指定了从第一个到最后一个按顺序地访问集合中的项。而from子句则声明式地规定集合中的每个项都要被访问,但并没有假定以什么样的顺序
  • foreach语句在遇到代码时就执行其主体,而from子句什么也不执行。只有在程序的控制流遇到访问查询变量的语句时,才会执行查询

join子句

LINQ中的join子句和SQL中的JOIN(联结)子句相似。不同的是,我们现在不但可以在数据库的表上进行联结,还可以在集合对象上进行该操作。如果你不熟悉联结,那么下面的内容会帮你理清思路。
需要先了解有关联结的语法:

  • 使用联结来结合两个多多个集合中的数据
  • 联结操作接受两个集合然后创建一个临时的对象集合,每个对象包含原始集合对象中的所有字段

联结语法如下

关键字        关键字           关键字      关键字
 ↓              ↓              ↓           ↓
join Identifier in Collection2 on Field1 equals Field1
                       ↑
              指定另外的集合和ID引用它
var query=from s in students
          join c in studentsInCourses on s.StID equals c.StID

什么是联结

LINQ中的join接受两个集合然后创建一个新的集合,每个元素包含两个原始集合中的原始成员。

例:联结示例
技术分享

class Program
{
    public class Student
    {
        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"},
    };
    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()
    {
        var query=from s in students
                  join c in studentsInCourses on s.StID equals c.STID
                  where c.CourseName=="History"
                  select.LastName;
        foreach(var q in query)
        {
            Console.WriteLine("Student taking History:{0}",q);
        }
    }
}

技术分享

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

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

from子句

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

例:from子句示例

class Program
{
    static void Main()
    {
        var groupA=new[]{3,4,5,6};
        var groupA=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);
        }
    }
}

技术分享

let子句

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

let Identifier=Expression

例:let子句示例

class Program
{
    static void Main()
    {
        var groupA=new[]{3,4,5,6};
        var groupA=new[]{6,7,8,9};
        var someInts=from a in groupA
                     from b in groupB
                     let sum=a+b         //在新的变量中保存结果
                     where sum==12
                     select new{a,b,sum};
        foreach(var a in someInts)
        {
            Console.WriteLine(a);
        }
    }
}

技术分享

where子句

where子句根据之后的运算来筛选指定项。
只要是在from…let…where部分中,查询表达式可以有多个where。

例:where子句示例

class Program
{
    static void Main()
    {
        var groupA=new[]{3,4,5,6};
        var groupA=new[]{6,7,8,9};
        var someInts=from a in groupA
                     from b in groupB
                     let sum=a+b         
                     where sum>=11            ←条件1
                     where a==4               ←条件2
                     select new{a,b,sum};
        foreach(var a in someInts)
        {
            Console.WriteLine(a);
        }
    }
}

技术分享

orderby子句

orderby子句根据表达式按顺序返回结果项。
orderby子句语法如下图。可选的ascending和descending关键字设置了排序方向。表达式通常是项的一个字段。该字段不一定非得是数值字段,也可以是字符串这样的可排序类型。

  • orderby子句默认是升序
  • 可以有任意多子句,它们必须用逗号分隔

技术分享

例:按照学生年龄排序

class Program
{
    static void Main()
    {
        var students=new[]
        {
            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
                  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子句指定所选对象的哪部分应该被select。它可以指定下面的任意一项
    • 整个数据项
    • 数据项的一个字段
    • 数据项的几个字段组成的新对象(或类似其他值)
  • group…by子句是可选的,用来指定选择的项如何分组

例:select整个数据项

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var students=new[]
        {
            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 s in query)
        {
            Console.WriteLine("{0},{1}: {2} , {3}",s.LName,s.FName,s.Age,s.Major);
        }
    }
}

技术分享

var query=from s in students
          select s.LName;
foreach(var s in query)
{
    Console.WriteLine(s);
}

技术分享

查询中的匿名类型

查询结果可以由原始集合的项、项的某些字段或匿名类型组成。
例:使用select创建一个匿名类型

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var students=new[]
        {
            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};
        foreach(var s in query)
        {
            Console.WriteLine("{0} {1} -- {2} , {3}",s.FName,s.LName,s.Major);
        }
    }
}

技术分享

group子句

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

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

例:根据学士的主修课程进行分组

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var students=new[]
        {
            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
                  group s by s.Major;
        foreach(var s in query)
        {
            Console.WriteLine("{0}",s.Key);
            foreach(var t in s)
            {
                Console.WriteLine("      {0},{1}",t.LName,t.FName);
            }
        }
    }
}

技术分享

技术分享

查询延续:into子句

查询延续子句可以接受查询的一部分结果并赋予一个名字,从而可以在查询的另一部分中使用。
技术分享
例:连接groupA和groupB并命名为groupAandB

class Program
{
    static void Main()
    {
        var groupA=new[]{3,4,5,6};
        var groupA=new[]{6,7,8,9};
        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(a);
        }
    }
}

技术分享

标准查询运算符


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

  • 被查询的集合对象叫做序列,它必须实现IEnumerable<T>接口,T是类型
  • 标准查询运算符使用方法语法
  • 一些运算符返回IEnumerable对象(或其他序列),而其他的一些运算符返回标量。返回标量的运算符立即执行,并返回一个值
  • 很多操作都以一个谓词作为参数。谓词是一个方法,它以对象为参数,根据对象是否满足某条件而返回true或false

例:Sum和Count运算符的使用

class Program
{
    static int[] numbers=new int[]{2,4,6};
    static void Main()
    {
        int total=numbers.Sum();
        int howMany=number.Count();
        Console.WriteLine("Total: {0},Count: {1}",total,howMany);
    }
}

技术分享

标准查询运算符可用来操作一个或多个序列。序列指实现了IEnumerable<>接口的类型,包括List<>、Dictionary<>、Stack<>、Array等。
技术分享

标准查询运算符的签名

System.Linq.Enumerable类声明了标准查询运算符方法。这些方法不仅是一些方法,它们是扩展了IEnumerable<T>泛型类的扩展方法。
第7章和第17章介绍类扩展方法,在本节是学习如何使用扩展方法的好机会。
简单回顾一下。扩展方法是公共的静态方法,尽管定义在一个类中,但目的是为另一个类(第一个形参)增加功能。该参数前必须有关键字this。

例:3个标准查询运算符的签名

始终是public static       名字和泛型参数    第一个参数
     ↓                         ↓             ↓
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,...);

例:直接调用扩展方法和将其作为扩展进行调用的不同

using System.Linq;
...
static void Main()
{
    int[] intArray=new int[]{3,4,5,6,7,9};
    //方法语法
    var count1=Enumerable.Count(intArray);
    var firstNum1=Enumerable.First(intArray)
    //扩展语法
    var count2=intArray.Count();
    var firstNum2=intArrya.First();
    Console.WriteLine("Count: {0},FirstNumber: {1}",count1,firstNum1);
    Console.WriteLine("Count: {0},FirstNumber: {1}",count2,firstNum2);
}

技术分享

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

查询表达式和方法语法可以组合。编译器把每个查询表达式翻译成标准查询运算符的形式。

class Program
{
    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);
    }
}

技术分享

将委托作为参数

前面我们看到,每个运算符的第一个参数是IEnumerable<T>对象的引用,之后的参数可以是任何类型。很多运算符接受泛型委托作为参数(第17章)。泛型委托用于给运算符提供用户自定义代码。

为了解释这一点,我们首先从演示Count运算符的几种使用方式的示例开始。
Count运算符被重载且有两种形式,第一种之前示例中用过,它有一个参数,返回集合中元素的个数。

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

然而,假设我们希望看看数组中奇数元素的总数。Count方法必须能够检测整数是否为奇数。
我们需要使用Count方法的第二种形式。如下所示,它有一个泛型委托作为参数。调用时,我们提供一个接受单个T类型的输入参数并返回布尔值的委托对象。委托代码的返回值必须指定元素是否包含在总数中。

public static int Count<T>(this IEnumerable<T> source,Func<T,bool> predicate);
class Program
{
    static void Main()
    {
        int[] intArray=new int[] {3,4,5,6,7,9};
        var countOdd=intArray.Count(n=>n%2!=0);
        Console.WriteLine("Count of odd numbers: {0}",countOdd);
    }
}

LINQ预定义的委托类型

和前面示例中的Count运算符差不多,很多LINQ运算符需要我们提供代码来指示运算符如何执行它的操作。我们通过委托对象作为参数来实现。
LINQ定义了两套泛型委托类型与标准查询运算符一起使用,即Func委托和Action委托,各有17个成员。

  • 我们用作实参的委托对象必须是这些类型或这些形式之一
  • TR代表返回值,并且总是在类型参数列表中的最后一个
public delegate TR Func<in T1,in T2,out TR>(T1 a1,T2 a2);
                 ↑               ↑              ↑
              返回类型         类型参数        方法参数

注意返回类型参数有out关键字,使之可以协变,即可以接受声明的类型或从这个类型派生的类型。输入参数有in关键字,使之可以逆变,即你可以接受声明的类型或从这个类型派生的类型。

使用委托参数的示例

class Program
{
    static bool IsOdd(int x)
    {
        return x%2!=0;
    }
    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);
    }
}

使用Lamba表达式参数的示例

之前示例使用独立的方法和委托来把代码附加到运算符上。这需要声明方法和委托对象,然后把委托对象传递给运算符。如果下面的条件任意一个成立,这种方法是不错的方案:

  • 方法还必须在程序的其他地方调用,而不仅仅是用来初始化委托对象的地方
  • 函数体中的代码语句多于一条

如果这两个条件都不成立,我们可能希望使用更简洁和更局部化的方法来给运算符提供代码,那就是Lambda表达式。

例:用Lambda表达式修改之前的示例

class Program
{
    static void Main()
    {
        int[] intArray=new int[] {3,4,5,6,7,9};
        var countOdd=intArray.Count(n=>n%2!=0);//Lambda表达式
        Console.WriteLine("Count of odd numbers: {0}",countOdd);
    }
}

我们也可以用匿名方法来替代Lambda表达式。然而,这种方式比较累赘,而且Lambda表达式在语义上与匿名方法完全等价,且更简洁,因此没有理由再去使用匿名方法了。

class Program
{
    static void Main()
    {
        int[] intArray=new int[] {3,4,5,6,7,9};
        Func<int,bool> myDel=delegate(int x)   //匿名方法
                             {
                                 return x%2!=0;
                             };
        var countOdd=intArray.Count(myDel);
        Console.WriteLine("Count of odd numbers: {0}",countOdd);
    }
}

LINQ to XML


可扩展标记语言(XML)是存储和交换数据的重要方法。LINQ为语言增加了一些特性,使得XML用起来比XPath和XSLT容易得多。

  • 可以使用单一语句自顶向下创建XML树
  • 可以不是用包含树的XML文档在内存中创建并操作XML
  • 可以不是用Text子节点来创建和操作字符串节点
  • 搜索XML树时,不需要遍历它。只需要查询树并让它返回想要的结果

尽管本书不会完整介绍XML,但在接受LINQ to XML前,我会先简单介绍一下XML。

标记语言

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

XML基础

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

  • 开始标签 <ElementName>
  • 结束标签 </ElementName>
  • 无内容的单个标签 <ElementName/>

例:

   开始标签        内容        结束标签
      ↓            ↓            ↓
<EmployeeName>Sally Jones</EmployeeName>
<PhoneNumber/>  ←没有内容的元素

有关XML的重要事项:

  • XML文档必须有一个根元素包含所有其他元素
  • XML标签必须合理嵌套
  • 与HTML标签不同,XML标签是区分大小写的
  • XML特性是名字/值的配对,它包含了元素的额外元数据。特性的值部分必须包含在引号内,单引号双引号皆可
  • XML文档中的空格是有效的。这与把空格作为当个空格输出的HTML不同
<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>

技术分享

XML类

LINQ to XML可以以两种方式和XML配合使用。第一种是作为简化的XML操作API,第二种是使用本章前面看到的LINQ查询工具。
我会先介绍API方式。
LINQ to XML API由很多表示XML树组件的类组成。我们主要使用3个类,XElement、XAttribute和XDocument。
下图演示了用于构造XML树的类以及它们如何被嵌套。

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

技术分享

除了XAttribute类,大多数用于创建XML树的类都从一个叫做XNode的类继承,一般在书中也叫做“XNodes”。

创建、保存、加载和显式XML文档

例:创建一个包含Employees节点的XML树

using System;
using System.Xml.Linq;
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 employees2=XDocument.Load("EmployeesFile.xml");
                                       ↑
                                   静态方法
        Console.WriteLine(employees2);         //显式文件
    }
}

技术分享

创建XML树

例:创建XML树

using System;
using System.Xml.Linq;
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","415-555-2000"),
                        new XElement("PhoneNumber","415-555-2001"))
                )
            );
        Console.WriteLine(employeeDoc);
    }
}

技术分享

使用XML树的值

当我们遍历XML树来获取或修改值时才体现了XML的强大。下表给出了用于获取数据的主要方法。
技术分享
关于上表,需要注意的一些事项如下:

  • Nodes Nodes方法返回IEnumerable<object>类型的对象,因为返回的节点可能是不同的类型,比如XElement、XComment等。我们可以使用以类型作为参数的方法OfType(type)来指定返回某类型的节点。例如,如下代码只能获取XComment节点
    • IEnumerable<XComment> comments=xd.Nodes().OfType<XComment>()
  • Elements 由于获取XElement是非常普遍的需求,就出现了`Nodes.OfType(XElement)()``表达式的简短形式–Elements方法
    • 无参数的Elements方法返回所有子XElements
    • 单个name参数的Elements方法返回具有这个名字的子XElements。例如,如下代码返回具有名字PhoneNumber的子XElement节点
    • IEnumerable<XElement> empPhones=emp.Elements("PhoneNumber");
  • Element 这个方法只获取当前节点的第一个子XElement。如果无参数,获取第一个XElement节点,如果带一个参数,获取第一个具有此名字的子XElement
  • Descendants和Ancestors 这些方法和Elements以及Parent方法差不多,只不过它们不返回直接的子元素和父元素,而是忽略嵌套级别,包括所有之下或者之上的节点
using System;
using System.Collections.Generic;
using System.Xml.Linq;
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","415-555-2000"),
                        new XElement("PhoneNumber","415-555-2001"))
                )
            );
        //获取第一个名为“Employees”的子XElement 
        XElement root=employeeDoc.Element("Employees");
        IEnumerable<XElement> employees=root.Elements();
        foreach(XElement emp in employees)
        {
            XElement empNameNode=emp.Element("Name");
            Console.WriteLine(empNameNode.Value);
            IEnumerable<XElement> empPhones=emp.Elements("PhoneNumber");
            foreach(XElement phone in empPhones)
            {
                Console.WriteLine(phone.Value);
            }
        }
    }
}

技术分享

增加节点以及操作XML

我们可以使用Add方法位现有元素增加子元素。

using System;
using System.Xml.Linq;
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("third"),
               new XComment("Important Comment"),
               new XElement("fourth"));
        Console.WriteLine("Modified tree");
        Console.WriteLine(xd);
    }
}

技术分享

下表列出了最重要的一些操作XML的方法。
技术分享

使用XML特性

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

例:为root增加两个特性。

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

技术分享

例:获取特性

class Program
{
    static void Main()
    {
        XDocument xd=new XDocument(
            new XElement("root",
                    new XAttribute("color","red"),
                    new XAttribute("size","large"),
                new XElement("first"),
            )
        );
        Console.WriteLine(xd);
        Console.WriteLine();
        XElement rt=xd.Element("root");
        XAttribute color=rt.Attribute("color");
        XAttribute size=rt.Attribute("size");
        Console.WriteLine("color is {0}",color.Value);
        Console.WriteLine("size is {0}",size.Value);
    }
}

技术分享

例:移除特性

class Program
{
    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");
        rt.Attribute("color").Remove();//移除color特性
        rt.SetAttributeValue("size",null);//移除size特性
        Console.WriteLine(xd);
    }
}

技术分享

例:增加或改变特性的值

class Program
{
    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");
        rt.SetAttributeValue("size","midium");  //改变特性值
        rt.SetAttributeValue("width","narrow"); //添加特性
        Console.WriteLine(xd);
    }
}

技术分享

节点的其他类型

XComment

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

 new XComment("This is a comment")  

这段代码产生如下XML文档:
 <!--This is a comment--> 

XDeclaration

XML文档从包含XML使用的版本号、字符编码类型以及文档是否依赖外部引用的一行开始。这是有关XML的信息,因此它其实是有关数据的元数据。这叫做XML声明,可以使用XDeclaration类来插入,如下代码给出了XDeclaration的示例:
 new XDeclaration("1.0","uff-8","yes")  
这段代码产生如下XML文档:
 <?xml version="1.0" encoding="utf-8 " standalone="yes"?> 

XProecssingInstruction

XML处理指令用于提供XML文档如何被使用和翻译的额外数据,最常见的就是把处理指令用于关联XML文档和一个样式表。
我们可以使用XProecssingInstruction构造函数来包含处理指令。它接受两个字符串参数:目标和数据串。如歌处理指令接受多个数据参数,这些参数必须包含在XProecssingInstruction构造函数的第二个字符串参数中,如下的构造函数代码所示。

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

这段代码产生如下XML文档:
 <?xml-stylesheet href="http://www.mamicode.com/stories.css" type="text/css"?> 

例:

class Program
{
    static void Main()
    {
        XDocument xd=new XDocument(
            new XDeclaration("1.0","uff-8","yes"),
            new XComment("This is a comment"),
            new XProecssingInstruction("xml-stylesheet",@"href=""stories"",type=""text/css"""),
            new XElement("root",
                new XElement("first"),
                new XElement("second")
            )
        );
    }
}

代码会产生如下的输出文件。然而如果使用WriteLine(xd),声明语句不会被打印出来。
技术分享

使用LINQ to XML的LINQ 查询

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

例:创建示例用XML树

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","midium")),
                new XElement("third",
                    new XAttribute("color","blue"),
                    new XAttribute("size","large"))
            )
        );
        Console.WriteLine(xd);
        xd.Save("SimpleSample.xml");
    }
}

技术分享

例:LINQ to XML

class Program
{
    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树的所有顶层元素,并为每个元素创建了匿名类型对象

using System;
using System.Linq;
using System.Xml.Linq;
class Program
{
    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);
        }
    }
}

技术分享

从这些示例我们可以看到,可以轻易地组合XML API和LIQN查询工具来产生强大的XML查询能力。

C#图解教程 第十九章 LINQ