C#学习笔记 —— LINQ

发布时间 2023-07-23 16:02:05作者: 老菜农

LINQ

1、什么是LINQ

使用LINQ可以轻松查询对象集合

  • LINQ代表语言集成查询

  • LINQ是.NET框架的扩展,允许我们使用SQL查询数据库的类似方式来查询数据集合

  • LINQ可以从数据库、对象集合、XML文档中查询数据

2、LINQ提供程序

  • 对于每一种数据源类型,一定有根据该数据源类型实现LINQ查询的代码模块,叫做LINQ提供程序

  • Micosoft为一些常见数据源提供了LINQ提供程序,如下所示

Visual C#原有LINQ支持 ADO.NET的LINQ支持
LINQ to Objects LINQ to SQL
LINQ to XML LINQ to Datasets
BLINQ(ASP.NET) LINQ to Entities

(X)匿名类型

  • 创建无名类类型的特性

  • 常用于LINQ查询的结果之中

  • 创建匿名类型的变量使用相同的形式,但是没有类名和构造函数

new { FiedlProp = XXXX, FieldProp = XXXX, ...};

下列代码给出了创建和使用匿名类型的实例,创建了应该叫做student1的变量

static void Main(string[] args)
{
    var student = new {Name = "Mary Jones", Age = 19, Major = "History"};
    Console.WriteLine($"{student.Name}, {student.Age}, {student.Major}");
}
X.注意
  • 匿名类型只能用于局部变量,不能用于类成员

  • 由于匿名类型没有名字,我们必须使用var关键字作为变量类型

  • 不能设置匿名类型的对象的属性,编译为匿名类型创建的属性是只读的

  • 当编译器遇到匿名类型的对象初始化语句时,它用它构造的应该私有名称创建一个新类类型

  • 对于每一个成员初始化语句,它推断其类型并创建应该只读属性来访问它的值,属性和成员初始化语句具有相同的名字

  • 匿名类型被创建后,编译器创建这个类型的对象

除了对象初始化语句的复制类型,匿名类型的对象初始化语句还有其他两种形式,被称为投影初始化语句

  • 简单标识符

  • 成员表达式访问符

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

class Other
{
    static public string Name = "Mary Jones"; //成员表达式访问符
}
class Program
{
    static void Main()
    {
        string Major = "History"; //简单标识符
        var student = new {Age = 19, Other.Name, Major};
    }
}

3、方法语法和查询语法

  • 方法语法

    • 使用标准的方法调用,这些方法是一组叫做标准查询运算符的方法

    • 是命令式的,指明了查询方法和方法调用的顺序

  • 查询语法

    • 看上去和SQL语句类似,使用表达式形式书写

    • 查询语法是声明式的,查询描述是你想返回的东西,但没有指明如何执行此查询

  • 在一个查询中可以组合两种形式

  • 这两种形式运行时在性能上没有差异

  • 微软更推荐使用查询语法

static void Main(string[] args)
{
    int[] numbers = {2, 5, 28, 31, 17, 16, 42};
    //查询语法
    var numsQuest = from n in numbers
                    where n < 20
                    select n;
    //方法语法
    var numsMethod = numbers.Where(N => N < 20);
    //两种语法结合
    int numsCount = (from n in numbers
                     where n < 20
                     select n).Count();
    foreach(var x in numsQuest)
    {
        Console.WriteLine($"{x} ");
    } //2 5 17 16 
    Console.WriteLine();
    foreach(var x in numsMethod)
    {
        Console.WriteLine($"{x} ");
    } //2 5 17 16 
    Console.WriteLine(numsCount); // 4
}

4、查询变量

  • LINQ查询可以返回两种类型的结果,可以是应该枚举

    • 它是满足查询参数的项目列表

    • 也可以是应该叫做标量的单一值,是满足查询条件结果的某种摘要形式

int[] numbers = {2, 5, 28};
//指定LINQ查询,返回一个枚举器,枚举查询的结果
IEnumerable<int> lowNums = from n in numbers where n < 20 select n;
//调用LINQ方法计算项的总数Count, 返回一个整数
int numsCount = (from n in numbers where n < 20 select n).Count();
  • LINQ等号左边的变量叫做查询变量

    • 可以使用var关键字代替变量名

  • 在执行前面的代码后,lowNums查询变量不会包含查询的结果

  • 相反编译器会创建能够执行这个查询的代码

  • 查询变量numCount包含的是真实的整数,只能通过真实运行查询后获得

  • 查询执行时间的差异可以总结如下

    • 查询表达式返回枚举,则查询一直到处理枚举时才会执行

    • 枚举被处理多次,查询就会执行多次

    • 如果在进行遍历之后查询执行之前有数据改动,则查询会使用新的数据

    • 如果查询表达式返回标量,查询立即执行,并且把结果保存在查询变量中

5、查询表达式的结构

  • 子句必须按照一定的顺序出现

  • from子句和select...group子句这两部分是必需的

  • 其他子句是可选的

  • select子句在表达式最后,为了让Visual Studio智能感应在输入代码时提供更多选项

  • 可以有任意多的from...let...where

(1)from子句

指定了要作为数据源使用的数据集合,引入了迭代变量

from Type Item in 集合1
  • 迭代变量逐个表示数据源的每一个元素

  • from子句的语法如下

    • Type是集合中类型的类型,这是可选的,因为编译器可以从集合中推断类型

    • Item是迭代变量的名字

    • Items是要查询的集合的名字,集合必须是可枚举的

int[] arr1 = {10, 11, 12, 13};
var query = from item in arr1 where item < 13 select item;
foreach(var item in query)
{
    Console.Write($"{item }");
}
子句
from Type item in Expression 
    join Type item in Expression on Expression equals Expression into item
  • 尽管LINQ的from子句和foreach非常相似,但是主要的不同的如下

    • foreach

      • 语句命令式地指定了要从第一个到最后一个按顺序访问集合中的项

      • 遇到代码时执行其主体

    • from子句

      • 声明式地规定集合中的每个项都要被访问,但并没有假定什么顺序

      • 什么也不执行,创建可以执行查询的后台代码对象,只有在程序控制流遇到访问查询变量的语句时,才会执行查询

(2)join子句

可用在集合对象上进行链接操作

  • 使用联接来结合两个或者更多集合中的数据

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

联合的语法如下, 它指定了第二个集合要和之前子句中的集合进行联接, 注意必须使用上下文关键字equals来比较字段, 不能用==运算符

join Type Item in 集合2 on 字段1 equals 字段2
join Type Item in 集合2 on 字段1 equals 字段2 into Item
例子
var query = from s in students join c in studentsInCourses on s.StID equals c.StID

(3)联结

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

class Student
    {
        public int StID;
        public string LastName;
​
        public Student()
        {
        }
​
        public Student(int stID, string lastName)
        {
            StID = stID;
            LastName = lastName;
        }
    }
class CourseStudent
    {
        public string CourseName;
        public int StID;

        public CourseStudent()
        {
        }

        public CourseStudent(string courseName, int stID)
        {
            CourseName = courseName;
            StID = stID;
        }
    }
		static Student[] students =
        {
            new Student(1, "Carson"),
            new Student(2, "Klassen"),
            new Student(3, "Fleming"),
            
        };
        static CourseStudent[] courseStudents =
        {
            new CourseStudent("Art", 1),
            new CourseStudent("Art", 2),
            new CourseStudent("History", 1),
            new CourseStudent("History", 3),
            new CourseStudent("Physics", 3)
        };
        static void Main(string[] args)
        {
            {
                //查找所有选择了历史课的学生的姓氏
                var query = from s in students
                            join c in courseStudents on s.StID equals c.StID
                            where c.CourseName == "History"
                            select s.LastName;
                foreach (var q in query)
                {
                    Console.WriteLine($"选历史课的有{q}");
                }
            }
        }

(4)查询主体中的from let where片段

  • 可选的from...let...where部分是查询主体的第一部分, 可以由任意数量的三种子句构成

1.from子句
  • 后面跟着查询主体,

  • 主体本身可以从任何数量的其他from子句开始,

  • 每一个from子句都指定了一个额外的源数据集合并引入了要在之后运算的迭代变量

  • 第一个from是查询表达式必须的子句

  • 第二个from是第一个子句的查询主体

  • select子句创建了一个匿名类型的对象

int[] groupA = new int[] { 3, 4, 5, 6 };
int[] groupB = new int[] { 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 x in someInts)
{
    Console.WriteLine($"a大于4 b小于等于8的y元素和{x}");
}
2.let子句
  • let子句接受一个表达式的运算并把它赋值给一个需要在其他运算中使用的标识符

  • 语法如下

    let Identifier = Expression

如下代码中的查询表达式将数组groupA中每一个成员与数组groupB中的每一个成员配对

int[] groupA = new int[] { 3, 4, 5, 6 };
int[] groupB = new int[] { 6, 7, 8, 9 };
//查询在a b中a+b的值等于12的
var someInts = from a in groupA
               from b in groupB
               let sum = a + b
               where sum == 12
               select new { a, b, sum };
foreach (var x in someInts)
{
    Console.WriteLine($"{x}");
}
3.where子句
  • where子句根据之后的运算来去除不符合指定条件的项

  • 语法如下

    where BooleanExpression
  • 只要在from..let...where部分中, 查询表达式可以有任意多个where子句

  • 一个项必须满足所有where子句才能避免在之后被去除

int[] groupA = new int[] { 3, 4, 5, 6 };
int[] groupB = new int[] { 6, 7, 8, 9 };
//查询在a b中a+b的值大于等于12 且a等于4的
var someInt = from a in groupA
              from b in groupB
              let sum = a + b
              where sum >= 11
              where a == 4
              select new { a, b, sum };
foreach (var x in someInt)
{
    Console.WriteLine($"{x}");
}

(5)orderby

  • 接受一个表达式并根据表达式按顺序返回结果项

  • 默认是升序, 可以使用asceding \ descending关键字设置排序正反

  • 可以有任意多个子句, 必须使用逗号分割

  • 语法如下

    orderby Expression ascending, Expression descending, Expression.......
var students = new[]
{
    new{ LName = "Jones", Fname = "Mary", Age = 19, Major = "History"},
    new{ LName = "Smith", Fname = "Bob", Age = 20, Major = "ComputerScience"},
    new{ LName = "Fleming", Fname = "Carol", Age = 20, Major = "History"}
};
//根据学生年龄正序, 根据姓倒序
var query = from student in students
            orderby student.Age ascending, student.LName descending
            select student;
foreach (var q in query)
{
    Console.WriteLine($"{q}");
}
(6)select...group
  • select子句和group...by子句组成

  • select...group之前的子句指定了数据源和要选择的对象

  • 功能如下

    • select指定应该选择所选对象的哪些部分, 可以指定下面任意一项

      • 整个数据项

      • 数据项的一个字段

      • 数据项中几个字段组成的新对象(或类似其他值)

    • group...by子句是可选的, 用来只指定选择的项如何被分组

  • 语法如下

    select Expression
    group Expression1 by Expression2

使用select子句选择整个数据项、项的某个字段、项的某些字段或其他的值组成匿名对象的示例

var students = new[]
 {
    new{ LName = "Jones", Fname = "Mary", Age = 19, Major = "History"},
    new{ LName = "Smith", Fname = "Bob", Age = 20, Major = "ComputerScience"},
    new{ LName = "Fleming", Fname = "Carol", Age = 20, Major = "History"}
};

//整个项
var queryItem = from student in students
                select student;
foreach (var qi in queryItem)
{
    Console.WriteLine($"{qi.LName}, {qi.Fname}, {qi.Age}, {qi.Major}");
}
//项的某个字段
var queryField = from student in students
                 select student.LName;
foreach (var qf in queryField)
{
    Console.WriteLine($"{qf}");
}

(7)查询中的匿名类型

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

  • 可以通过select子句把希望在类型中包括的字段以逗号分隔, 并且以大括号进行包围来创建匿名类型

  • 语法如下

    select new {Item.Field1, Item.Field2, RandomField}
//项的某几个字段组成一个新匿名对象
var queryObject = from student in students
                  select new { student.LName, student.Fname, token = "2023student2023" };
foreach (var qo in queryObject)
{
    //匿名类访问字段
    Console.WriteLine($"{qo.LName}, {qo.Fname}, {qo.token}");
}

(8)group by

  • 根据指定的标准对选择的对象进行分组

  • 如果项包含在查询的结果中, 他们就可以根据某个字段的值进行分组, 作为分组依据的属性叫键

  • group by返回的不是原始数据项中的枚举, 而是返回可以枚举已经形成的项的分组的可枚举类型

  • 分组本身是可枚举类型, 他们可以枚举实际的项

  • 语法如下

    group Type Item by Item.Field
var students = new[]
 {
    new{ LName = "Jones", Fname = "Mary", Age = 19, Major = "History"},
    new{ LName = "Smith", Fname = "Bob", Age = 20, Major = "ComputerScience"},
    new{ LName = "Fleming", Fname = "Carol", Age = 20, Major = "History"}
};
var queryGroup = from student in students
            group student by student.Major;
//枚举分组
foreach (var group in queryGroup)
{
    Console.WriteLine($"——————{group.Key}——————"); //Key分组键
    
    //枚举分组中的项
    foreach (var student in group)
    {
        Console.WriteLine($"{student.LName} · {student.Fname}"); //学生名字·姓氏
    }
    
}
  • 从查询表达式返回的对象是从查询中枚举分组结果的可枚举对象

  • 每一个分组由一个叫做键的字段区分

  • 每一个分组本身是可枚举类型并且可以枚举它的项

(9)查询延续 : into

  • 查询延续子句可以接受查询的一部分的结果并赋予一个名字

  • 从而可以在查询的另一部分中使用

into Item 
join Type Item in Expression 
   			   on Expression equals Expression
               into Expression
into Item 
join Type Item in Expression 
   			   on Expression equals Expression

  • 链接查询groupAgroupB并将结果命名为groupAandB

  • 从结果中进行简单的查询

int[] groupA = new int[] { 3, 4, 5, 6 };
int[] groupB = new int[] { 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 x in someInts)
{
    Console.WriteLine($"a、b中相等的元素为{x}");
}

6、标准查询运算符

  • 标准运算符有一系列API方法组成

  • API能让我们查询任何.NET数组或集合

  • 标准查询运算符具有如下特性

    • 使用方法语法

    • 一些运算符返回IEnumerable对象(或者其他序列), 而其他运算符返回标量.

    • 返回标量的运算符立即执行查询, 并返回一个值, 而不是一个可枚举类型对象

    • ToArray()ToList()ToCollection运算符也会立即执行

    • 很多操作都以一个谓词作为参数, 谓词十一个方法, 它以对象为参数, 根据对象是否满足某个条件而返回True、False

  • 被查询的集合对象叫做序列, 它必须实现IEnumerable<T>接口, 其中T是类型

  • 用作方法的运算符直接作用于序列对象, 在这里就是numbers数组

  • 返回类型不是IEnumerable, 而是int

int sum = numbers.Sum();
int count = numbers.Count();
Console.WriteLine($"和{sum}, 个数{count}");

(0)运算符表

运算符 描述
Where 根据给定的谓词对序列进行过滤
Select 指定要包含一个对象或对象的一部分
SelectMany 一种查询类型, 返回集合的集合, 将这些结果合并为一个单独的集合
Take 接受一个输入参数count, 返回序列中的前count个对象
Skip 接受一个参数, 跳过前参数个对象
TakeWhile 接受一个谓词, 开始迭代序列, 只要对当前项计算结果为true选择该项, 如果为false该项和其余项都被丢弃
SkipWhile 接受一个谓词, 开始迭代序列, 只要对当前项计算结果为true跳过该项, 如果为false该项和其余项都被选择
Join 对两个序列执行内连接
GroupJoin 产生层次结果的连接, 第一个序列中的各个元素都与第二个序列中的元素相关联
Concat 连接两个序列
OrderBy/ThenBy 根据一个或多个键对序列中元素升序排列
Reverse 反转序列中的元素
GroupBy 分组序列中的元素
Distinct 去除序列重复项
Union 返回两个序列的并集
Intersect 返回两个序列的交集
Except 操作两个序列, 返回的是第一个序列中不重复的元素减去同样位于第二个序列中的元素
AsEnumerable 将序列作为IEnumerable<TSource>返回
ToArray 将序列作为数组返回
ToList 将序列作为List<T>返回
ToDictionary 将序列作为Dictionary<TKey, TElectment>返回
ToLookUp 将序列LookUp<Tkey, TElement>返回
OfType 所返回的序列中的元素强制转换为给定的类型
Cast 将序列中所也有的元素强制转换为给定的类型
SequenceEqual 返回一个bool, 指定两个序列是否相等
First 返回序列中第一个与谓词匹配的元素, 如果没有元素与谓词匹配, 就抛出非法操作异常
FirstOrDefault 返回序列中第一个与谓词匹配的元素, 如果没有元素与谓词匹配, 就返回默认值
Last 返回序列中最后一个与谓词匹配的元素, 如果没有元素与谓词匹配, 就抛出非法操作异常
LastOrDefault 返回序列中最后一个与谓词匹配的元素, 如果没有元素与谓词匹配, 就返回默认值
Single 返回序列中与谓词匹配的单个元素, 如果没匹配或多于一个元素, 就抛出异常
SingleOrDefault 返回序列中与谓词匹配的单个元素, 如果没匹配或多于一个元素, 就返回默认值
ElementAt 给定一个参数n, 返回序列中第n+1个元素
ElementAtOrDefault 给定一个参数n, 返回序列中第n+1个元素, 超过索引范围, 返回默认值
DefaultIfEmpty 提供一个在序列为空时的默认值
Range 给定一个start整数和count整数, 该方法返回的序列包含count整型, 其中第一个元素的值为start, 每个后续元素都比前一个大1
Repeat 给定一个T类型的element和一个count整数, 该方法返回的序列具有countelement副本
Empty 返回给定类型T的空序列
Any 返回一个布尔值, 是否存在满足谓词的元素
All 返回一个布尔值, 全部元素是否都满足谓词
Contains 返回一个布尔值, 指明序列中是否包含给定元素
Count 返回序列中元素的个数, 他的重载可以接受一个谓词, 并返回序列中满足谓词的元素个数
Sum 返回序列中值的总和
Min 返回序列中的最小值
Max 返回序列中的最大值
Average 返回序列中值的平均值
Aggregate 连续对序列中的各个元素应用给定的函数

(1)标准查询运算符的签名

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

    • 扩展方法是公有的静态方法

    • 目的是为另一个类(第一个形参)增加功能

    • 该参数前必须有关键字this

  • 如下是三个标准查询运算符的前面

  • 由于运算符是泛型方法, 因此每个方法都具有相关的泛型参数T

  • 由于运算符是扩展IEnumerable类的扩展方法, 必须满足如下语法条件

    • 声明为publicstatic

    • 在第一个参数前有this扩展指示器

    • IEnumerable<T>作为第一个参数类型

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, ...);

演示直接调用扩展方法将其作为扩展进行调用的不同, 两个运算符都接受一个参数

  • Count 运算符返回序列中所有元素的个数

  • First 运算符返回序列中的第一个元素

在代码中, 前两次使用的运算符都是直接调用的,和普通方法差不多, 传入数组的名字作为第一个参数, 然而, 之后的两行代码使用扩展方法语法来调用运算符, 就好像它们是数组方法的成员一样

这里没有指定参数, 而是将数组名称从参数列表中移动到了方法名称之前, 用起来就好像包含了方法的声明一样

int[] intArray = new int[] { 3, 4, 5, 6, 7, 8, 9 };
//方法语法
int count1 = Enumerable.Count(intArray);
int firstNum1 = Enumerable.First(intArray);
//扩展语法
int count2 = intArray.Count();
int firstNum2 = intArray.First();
​
Console.WriteLine($"{count1}, {firstNum1}");
Console.WriteLine($"{count2}, {firstNum2}");

(2)查询表达式和查询标准运算符

  • 标准查询运算符是进行查询的一组方法

  • 每一个查询表达式还可以使用带有标准查询运算符的方法来编写

  • 编译器把每个查询表达式翻译成标准查询运算符的形式

  • 由于所有查询表达式都被翻译为标准查询运算符, 因此运算符可以执行由查询表达式完成的任何操作, 而且运算符还有查询表达式翻译成标准查询运算符的形式

  • 查询表达式和方法语法可以组合

int[] intArray = new int[] { 3, 4, 5, 6, 7, 8, 9 };
//查询表达式组合方法语法
int count = (from n in numbers
             where n < 7
             select n).Count();
Console.WriteLine($"{count}");

(3)将委托作为参数

  • 每一个运算符的第一个参数是IEnumerable<T>对象的引用, 之后的参数可以是任何类型

  • 很多运算符接受泛型委托作为参数

    • 泛型委托用于给运算符提供用户自定义的代码

public static int Count<T>(this IEnumerable<T> source);
//解说一个泛型委托作为第二个参数
public static int Count<T>(this IEnumerable<T> source, 
                           Func<T, bool> predicate);
  • 提供一个Lambda表达式来实现

  • 这个表达式在输入值为奇数返回true, 否则返回false

int[] intArray = new int[] { 3, 4, 5, 6, 7, 8, 9 };
int countOdd = intArray.Count(n => n % 2 == 1);
Console.WriteLine($"{countOdd}");

(4)LINQ预定义的委托类型

  • 很多LINQ运算符需要提供代码来只是运算符如何执行它的操作

  • 通过把委托对象作为参数来实现

  • 把委托对象当作一个包含具有特殊前面和返回类型的方法或方法列表的对象

  • 当委托被调用时, 他包含的方法会被依次调用

  • .NET有两种泛型委托类型来用于标准查询运算符, 可以将他们用在其他地方, 不限于查询运算符

    • Func委托, 19个成员

    • Action委托, 19个成员

 

  • 下面用作实参的委托对象必须是这些类型或这些形式之一

  • TR代表返回值, 并且总是类型参数列表的最后一个

  • 返回类型out可以协变, 可以接受该类及其子类

  • 输入参数in可以逆变, 可以接受该类及其子类

public delegate TR Func<out TR>();
public delegate TR Func<in T1, out1 TR>(T1 a1);
public delegate TR Func<in T1, in T2, out TR>(T1 a1, T2 a2);
public delegate TR Func<in T1, in T2, in T3, out TR>(T1 a1, T2 a2, T3 a3);
  • Count的声明如下, 第二个参数必须是委托对象

  • 这种形式的Func委托称为谓词

public static int Count<T>(this IEnumerable<T> source, 
                           Func<T, bool> predicate);
  • 如下是前4个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);

(5)使用委托参数的示例

  • 创建一个类型为Func<int, bool>, 名称为MyDel的委托对象

  • 使用IsOdd()来初始化委托对象, 不需要声明Func委托类型, 因为.NET框架一定预定义了

  • 使用委托对象调用Count

int[] intArray = new int[] { 3, 4, 5, 6, 7, 8, 9 };
//创建委托对象
Func<int, bool> myDel = new Func<int, bool>(IsOdd);
//使用委托
int countIsOdd = intArray.Count(myDel);
Console.WriteLine($"偶数共有{countIsOdd}个");

(6)使用Lambda表达式参数的示例

  • 如果下面任意一个成立, 则这种方式就是正确的方式

    • 如果方法还必须在程序的其他地方调用, 而不仅是用来初始化委托对象的地方

    • 如果函数题中的代码不止有一两条语句

  • 如果两个条件都不成立, 使用Lambda表达式

int[] intArray = new int[] { 3, 4, 5, 6, 7, 8, 9 };
//使用lambda表达式对象
Func<int, bool> myDel = new Func<int, bool>(x => x%2 == 1);
int countIsOdd1 = intArray.Count(myDel);
//or
int countIsOdd2 = intArray.Count(x => x % 2 == 1);
Console.WriteLine($"偶数共有{countIsOdd1}个");
Console.WriteLine($"偶数共有{countIsOdd2}个");
  • 使用匿名方法

int[] intArray = new int[] { 3, 4, 5, 6, 7, 8, 9 };
//使用lambda表达式对象
Func<int, bool> myDel = delegate (int x)
{
    return x % 2 == 1;
};
int countIsOdd = intArray.Count(myDel);
Console.WriteLine($"偶数共有{countIsOdd}个");

7、LINQ to XML

  • 可以使用单一语言自顶向下创建XML树

  • 可以在不适用包含树的XML文档的情况下在内存中创建并操作XML

  • 可以在不适用TEX子节点的情况下创建和操作字符串节点

  • 在搜索一个XML树时, 不需要在遍历他, 只需要查询树并返回结果

(2)XML基础

//双标签
<EmpName>Bob</EmpName>
//单标签
<PhoneNumber />
  • XML文档必须有一个根元素来包含其他元素

  • XML文档必须合理嵌套

  • XML文档区分大小写

  • 特性是名/值对, 包含了元素的其他元数据, 特性的值部分必须包含在引号内, 可以是单引号也可以是双引号

  • 多少个空格都是有效的, 不是HTML的单个空格

(3)XML类

LINQ to XML有两种

  • 简化的 LINQ to XML API

  • LINQ查询工具

  • API由很多表示XML的树组件的类组成, 我们会使用的3个最重要的类包括

    • XElement

    • XAttribute

    • XDocument

  • XDocument节点可以有以下直接子节点

    • 下面每一个节点类型, 最多有一个: XDeclarationXdocumentTypeXElement

    • 任何数量的XProcessingInstruction节点

  • 如果XDocument下有最好级别的XElement节点, 那么他就是XML树中其他元素的根

  • 根元素可以包含任意数量的嵌套XElementXCommentXProcessingInstructing节点, 并且可以在任何级别上嵌套

除了XAttribute类, 大多数用于创建XML树的类都派生自XNode

1.创建、保存、加载和显示XML文档
  • 树使用一条语句来创建, 并同时在适当的位置创建所有嵌套元素, 这叫做函数式构造

  • 每一个元素由对象创建表达式在适当的位置创建, 使用了节点类型的构造函数

XDocument employees1 = new XDocument( //创建XML文档
    new XElement("Employees",         //创建根元素
        new XElement("Name", "BOB"),  //创建元素
        new XElement("Name", "Sally") //创建元素
    )
);
//保存到文件
employees1.Save("EmployeesFile.xml");
//将保存的文档加载到新变量中
XDocument employees2 = XDocument.Load("EmployeesFile.xml");
//显示文档
Console.WriteLine(employees2); 
2.创建XML树
  • 使用XDucument和XElement的构造函数在内存中创建一个XML文档, 对于这俩构造函数

    • 第一个参数都是对象名

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

XDocument employeeDoc = new XDocument( //创建XML文档
    new XElement("Employees",         //创建根元素
        new XElement("Employee",
            new XElement("Name", "BOB"),  //创建元素
            new XElement("PhoneNumber", "123456") //创建元素
        ),
        new XElement("Employee",
            new XElement("Name", "Sally"),  //创建元素
            new XElement("PhoneNumber", "654321") //创建元素
        )
    )
);
Console.WriteLine(employeeDoc);
3.使用XML树的值

当我们遍历XML树来获取或修改值时, XML的强大就会体现出来, 查询XML的方法:

方法名称 返回类型 描述
Nodes XDocument、XElement IEnumerable<Object> 返回当前节点的所有子节点
Elements XDocument、XElement IEnumerable<XElement> 返回当前节点的XElement子节点, 或所具有某个名字的子节点
Element XXDocument、XElement XElement 返回当前节点的第一个XElement子节点, 或具有某个名字的子节点
Descendants XElement IEnumerable<XElement> 返回所有的XElement子代节点, 或所有具有某个名字的XElement子代节点, 不管他们出于当前节点下什么嵌套级别
DesendantsAdSelf XElement IEnumerable<XElement> 如上, 但是包括当前节点
Ancestors XElement IEnumerable<XElement> 返回所有上级XElement节点, 活着所具有某个名字的上级XElement节点
AncestorsAndSelf XElement IEnumerable<XElement> 如上, 但是包括当前节点
Parent XElement XElement 返回当前节点的父节点
注意事项
  1. Nodes

    1. 因为返回的节点可能是不同的类型, 比如XElement XComment, 所以返回为IEnumerable<object>类型的对象

    2. 可以使用以类型作为参数的方法OfType(type)来指定返回某个类型的节点

      IEnumerable<XComment> comment = xd.Nodes().OfType<XComment>();
  2. Elements

    1. 由于获取XElements十一个非常普遍的需求, 就出现了Nodes.OfType(XElement)(), 表达式的阶段形式——Elements方法

    2. 使用无参数的Elements()返回所有子XElements

    3. 使用单个name参数的Elements方法只返回具有这个名字的子XElements

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

    1. 这个方法值获取当前节点的第一个子XElement. 与Elements方法相似,

    2. 他可以代一个参数也可以不带参数调用,

      1. 如果代参数, 获取第一个具有改参数名字的子XElement

      2. 如果没有参数, 获取第一个子XElement

  4. Descendants和Ancestors

    1. 这些方法与ElementsParent方法差不多

    2. 他们并不返回直接的子元素或父元素, 而是或略嵌套级别, 包括当前节点之下或之上的所有节点

XDocument employeeDoc = new XDocument( //创建XML文档
    new XElement("Employees",         //创建根元素
        new XElement("Employee",
            new XElement("Name", "BOB"),  //创建元素
            new XElement("PhoneNumber", "123456") //创建元素
        ),
        new XElement("Employee",
            new XElement("Name", "Sally"),  //创建元素
            new XElement("PhoneNumber", "654321"), //创建元素
            new XElement("PhoneNumber", "654321222") //创建元素
        )
    )
);
XElement root = employeeDoc.Element("Employees");
IEnumerable<XElement> employees = root.Elements();
foreach (var emp in employees)
{
    XElement empNameNode = emp.Element("Name");
    Console.WriteLine(emp.Value);
    IEnumerable<XElement> empPhones = emp.Elements("PhoneNumber");
    foreach (var phone in empPhones)
    {
        Console.WriteLine($"{phone.Value}");
    }
}
4.增加节点以及操作XML
  • 使用Add方法, 为现有元素增加子元素

  • 允许我们在一次方法调用中增加任意多个元素, 不管增加的节点类型是什么

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("secound"));
//添加3个子元素
rt.Add(new XElement("third"),
    new XComment("Important Comment"),
    new XElement("fourth"));
Console.WriteLine("Modified Tree");
Console.WriteLine(xd);

方法 从那里调用 描述
Add 父节点 在当前节点的该有子节点后增加新的子节点
AddFirst 父节点 在当前节点的既有节点前增加新的子节点
AddBeforeSelf 节点 在通解比的当前节点之前增加新的节点
AddAfterSelf 节点 在同级别的当前节点之后增加新的节点
Remove 节点 在同级别的当前节点之后增加新的节点
Remove 节点 删除当前所选节点及其内容
RemoveNodes 节点 删除当前所选的XElement及其内容
SetElement 父节点 设置节点的内容

(4)使用XML特性

  • 特性提供了有关XElement节点的额外信息, 他放在XML元素的开始标签中

  • 当我们用函数方法构造XML树的时候, 在Element构造函数中包含XAttribute构造函数就可以增加特性

    • XAttribute有两种形式, 一种接受namevalue, 另一种接受现有XAttribute的引用

XDocument xd = new XDocument(
    new XElement("root",
        new XAttribute("color", "red"), //特性构造函数
        new XAttribute("size", "large"), //特性构造函数
        new XElement("first"),
        new XElement("secound")
        )
);
Console.WriteLine(xd);
  • 获取特性可以使用Attribute方法, 提供特性名称作为参数即可

XDocument xd = new XDocument(
    new XElement("root",
        new XAttribute("color", "red"),
        new XAttribute("size", "large"),
        new XElement("first"),
        new XElement("secound")
        )
);
XElement rt = xd.Element("root");
XAttribute color = rt.Attribute("color");
XAttribute size = rt.Attribute("size");
​
Console.WriteLine($"{color.Value}");
Console.WriteLine($"{size.Value}");
  • 移除特性, 选择一个特性然后使用Remove, 或者在他的父节点中使用SetAttributeValue方法把特性值设置为null

XDocument xd = new XDocument(
    new XElement("root",
        new XAttribute("color", "red"),
        new XAttribute("size", "large"),
        new XElement("first"),
        new XElement("secound")
        )
);
XElement rt = xd.Element("root");
rt.Attribute("color").Remove();
rt.SetAttributeValue("size", null);
Console.WriteLine(xd);
  • 向XML树中增加一个特性或者改变特性的值, 可以使用SetAttributeValue

XDocument xd = new XDocument(
    new XElement("root",
        new XAttribute("color", "red"),
        new XAttribute("size", "large"),
        new XElement("first"),
        new XElement("secound")
        )
);
XElement rt = xd.Element("root");
rt.SetAttributeValue("size", "small"); //修改特性
rt.SetAttributeValue("width", "narrow"); //增加特性
Console.WriteLine(xd);

(5)其他类型节点

1.XCommont
  • 产生注释

2.XDeclaration
  • 插入XML文档的元数据:

    • 版本号

    • 使用字符编码类型

    • 文档是否依赖外部引用的一行开始

  • 使用XDeclaration插入

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

3.XProcessingInstruction

  • 使用XProcessingInstruction构造函数来包含处理指令, 处理关于XML文档的使用和解释方式的额外数据

    • 处理指令用于提供管序XML文档的使用和解释方式的额外数据

    • 最常用与关联XML文档和样式表

  • XProcessingInstruction接受两个字符串参数

    • 目标

    • 数据串

    • 如果处理指令接受多个数据参数, 这些参数必须包含在XProcessingInstruction构造函数的第二个字符串参数值

new XProcessingInstruction("xml-stylesheet", 
                           @"href=""stories"", type"=""text/css"""
)
XDocument xd = new XDocument(
    new XDeclaration("1.0", "utf-8", "yes"),
    new XComment("hi"),
    new XProcessingInstruction("xml-stylesheet",
                               @"href=""stories.css"" type=""text/css"""),
    new XElement("root",
        new XElement("first"),
        new XElement("secound")
        )
    );

(6)使用LINQ to XML的LINQ查询

例1
XDocument xd = new XDocument(
    new XElement("root",
        new XElement("first",
            new XAttribute("color", "red"),
            new XAttribute("size", "small")),
        new XElement("second",
            new XAttribute("color", "red"),
            new XAttribute("size", "medium")),
        new XElement("first",
            new XAttribute("color", "blue"),
            new XAttribute("size", "large"))
    )
);
Console.WriteLine(xd);      //显示XML树
xd.Save("SimpleSample.xml"); //保存XML树到文件
例2
XDocument xd = XDocument.Load("SimpleSample.xml");
XElement rt = xd.Element("root");
//选择包含名称超过五个字符的
var xyz = from e in rt.Elements()
          where e.Name.ToString().Length == 5
          select e;
//看看都选了哪些
foreach (var x in xyz)
{
    Console.WriteLine(x.Name.ToString());
}
Console.WriteLine();
foreach (var x in xyz)
{
    //打印特性
    Console.WriteLine($"Name: {x.Name}, " +  
        $"Color: { x.Attribute("color").Value}, " +
        $"Size: { x.Attribute("size").Value}");
}
例3
XDocument xd = XDocument.Load("SimpleSample.xml");
XElement rt = xd.Element("root");
//创建匿名类型
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($"{x.Name}, {x.color.Value}");
}