C#图解教程之LINQ

作者 Zhendong Ho 日期 2019-08-26
C#
C#图解教程之LINQ

什么是LINQ

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

LINQ的重要高级特性:

  • LINQ代表语言集成查询(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("{0} ", x);
}
}
}

输出结果:2,5。

LINQ提供程序

LINQ还可以和各种类型的数据源一起工作,如SQL数据库、XML文档等。然而,对于每一种数据源类型,在其背后一定有根据该数据源类型实现LINQ查询的代码模块。这些代码模块叫做LINQ提供程序(provider)

  • 微软为一些常见的数据源类型提供了LINQ提供程序。
  • 可以使用任何支持LINQ的语言来查询有LINQ提供程序的数据源类型。
  • 第三方在不断提供针对各种数据源类型的LINQ提供程序。

匿名类型

匿名类型(anonymous type)经常用于LINQ查询的结果中。

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

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

输出结果:

匿名类型的重要事项

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

当编译器遇到匿名类型的对象初始化语句时,它创建了一个有名字的新类类型。对于每一个成员初始化语句,它推断其类型创建一个只读属性来访问它的值。属性和成员初始化语句具有相同的名字。匿名类型被构造后,编译器创建了这个类型的对象

除了对象初始化语句的赋值形式,匿名类型的对象初始化语句还有两种形式:简单标识符成员访问表达式。这两种形式叫做投影初始化语句(projection initializer)

例:第一个成员初始化语句是赋值形式,第二个是成员访问表达式,第三个是标识符形式。

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

例:使用三种初始化语句。

class Other
{
static public string Name = "Mary Jones";
}

class Program
{
static void Main()
{
string Major = "History";
var student = { Age = 19, Other.Name, Major }; // 赋值形式、成员访问、标识符
Console.WriteLine("{0}, Age {1}, Major: {2}", student.Name, student.Age, student.Major);
// Mary Jones, Age 19, Major: History
}
}

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

方法语法和查询语法

我们在写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(numsCount);
}
}

输出结果:

查询变量

LINQ查询可以返回两种类型的结果

  1. 一个枚举可枚举的一组数据,不是枚举类型),它满足查询参数的列表。
  2. 一个叫做标量(scalar)的单一值,它是满足查询条件的结果的某种摘要形式。

例:查询变量示例。

class Program
{
static void Main()
{
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();
}
}

numbersnumsCount叫做查询变量。

查询表达式的结构

查询表达式由查询体后的from子句组成。

  • 子句必须按照一定顺序出现。
  • from子句select…group子句是必需的。
  • LINQ查询表达式中,select子句在表达式最后
  • 可以有任何多的from…let…where子句

from子句

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

from Type Item in Items

其中,类型说明符Type是可以省略的。可以有任意多个join子句

join子句

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个子句来组合,即rom子句let子句where子句

from子句

查询表达式必须从from开始,后面跟的是查询主体。主体本身可以跟任何数量的其他from子句,每一个form子句都指定了一个额外的源数据集合并引入了要在之后运算的迭代变量。所有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子句
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子句示例。

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子句根据表达式按顺序返回结果项。可选的ascendingescending关键字设置了排序方向。表达式可以是数值字段,也可以是字符串类型

  • 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子句指定所选对象的哪部分应该被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);
}
}
}

输出结果:

查询中的匿名类型

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

例:使用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);
}
}
}

输出结果:

gourp子句

group子句把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
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对象,而其他的一些运算符返回标量。返回标量的运算符立即执行,并返回一个值。
  • 很多操作都以一个谓词作为参数

例: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); // Total: 12, Count: 3
}
}

注意:标准查询运算符可用来操作一个或多个序列。序列是指实现了IEnumerable<>的类型,包括List<>Dictionary<>Stack<>Array<>等。

标准查询运算符的签名

System.Linq.Enumerable类声明了标准查询运算符方法。

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

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); // 3
}
}

将委托作为参数

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

Count运算符两种重载。第一种之前用过,有一个参数,返回集合中元素的个数。

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

第二种重载,有一个泛型委托作为参数。它接受单个T类型的输入参数,并返回布尔值的委托对象。委托代码的返回值必须指定元素是否包含在总数中。

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

例:使用委托作为参数示例。

class Program
{
static bool IsOdd(int x) // 委托对象使用的方法
{
return x % 2 != 0; // 如果x是奇数,返回true
}
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表达式参数的示例

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

使用匿名方法替代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); // Count of odd numbers: 4
}
}

LINQ to XML

XML类

LINQ to XML可以以两种方式和XML配合使用。第一种方式是作为简化的XML操作API,第二种方式是使用本章前面看到的LINQ查询工具

LINQ to XML API由很多表示XML树组件组成。主要使用3个类XElementXAttributeXDocument

创建、保存、加载和显式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树的值

下表给出了获取数据的主要方法。

例:通过遍历获取节点的值。

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特性

例:为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解析器忽略

new XComment("This is a comment")

这段代码产生如下的XML文档:

<!-- This is a comment -->

XDeclaration

XML文档从包含XML使用的版本号使用的字符编码类型以及文档是否依赖外部引用的一行开始。这叫做XML声明

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

这段代码产生如下的XML文档:

<?xml version="1.0" encoding="utf-8", standalone"yes"?>

XProcessingInstruction

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

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

这段代码产生如下的XML文档:

<?xml-stylesheet href="stories", type="text/css"?>

例:使用3个XML类型构造函数。

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

这段代码产生了如下的输出文件:

注意:如果使用xd的WriteLine,即使声明语句包含在文档文件中也不会显示

使用LINQ to XML的LINQ查询

例:创建示例用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查询来从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 // 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);
}
Console.ReadKey();
}
}

输出结果:

例:获取XML树的所有顶层元素。并为每个元素创建匿名类型的对象。

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);
}
Console.ReadKey();
}
}

输出结果: