C#图解教程 第十九章 LINQ

Posted 沉香

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C#图解教程 第十九章 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);
        }
    }
}

输出:6

标准查询运算符


标准查询运算符由一系列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);
    }
}

第十九章枚举集合

第十九章 Django框架——Admin组件

实战:第十九章:存入Long类型对象,在代码中使用Long类型接收,结果报类型转换错误

实战:第十九章:存入Long类型对象,在代码中使用Long类型接收,结果报类型转换错误

WPF学习第十九章 控件类

第十九章 函数的高级话题