LINQ Basic Introduction(MSDN)
语言集成查询 (LINQ) 是 Visual Studio 2008 和 .NET Framework 3.5 版中一项突破性的创新,它在对象领域和数据领域之间架起了一座桥梁。
LINQ 数据源是支持泛型 IEnumerable<(Of <(T>)>) 接口或从该接口继承的接口的任意对象。
查询表达式包含三个子句:from、where 和 select。(如果您熟悉 SQL,您会注意到这些子句的顺序与 SQL 中的顺序相反。)from 子句指定数据源,where 子句应用筛选器,select 子句指定返回的元素的类型。目前需要注意的是,在 LINQ 中,查询变量(numQuery)本身不执行任何操作并且不返回任何数据。它只是存储在以后某个时刻执行查询时为生成结果而必需的信息。
class IntroToLINQ
{
static void Main()
{
// The Three Parts of a LINQ Query:
// 1. Data source.
int[] numbers = new int[7] { 0, 1, 2, 3, 4, 5, 6 };
// 2. Query creation.
// numQuery is an IEnumerable<int>
var numQuery =
from num in numbers
where (num % 2) == 0
select num;
// 3. Query execution.
foreach (int num in numQuery)
{
Console.Write("{0,1} ", num);
}
}
}
LINQ 查询变量类型化为 IEnumerable<(Of <(T>)>) 或派生类型,如 IQueryable<(Of <(T>)>)。
IEnumerable<Customer> customerQuery =
from cust in customers
where cust.City == "London"
select cust;
foreach (Customer customer in customerQuery)
{
Console.WriteLine(customer.LastName + ", " + customer.FirstName);
}
如果您愿意,可以使用 var 关键字来避免使用泛型语法。var 关键字指示编译器通过查看在 from 子句中指定的数据源来推断查询变量的类型。下面的示例生成与上一个示例相同的编译代码:
var customerQuery2 =
from cust in customers
where cust.City == "London"
select cust;
foreach(var customer in customerQuery2)
{
Console.WriteLine(customer.LastName + ", " + customer.FirstName);
}
获取数据源
在 LINQ 查询中,第一步是指定数据源。像在大多数编程语言中一样,在 C# 中,必须先声明变量,才能使用它。在 LINQ 查询中,最先使用 from 子句的目的是引入数据源 (customers) 和范围变量 (cust)。
//queryAllCustomers is an IEnumerable<Customer>
var queryAllCustomers = from cust in customers
select cust;
范围变量类似于 foreach 循环中的迭代变量,但在查询表达式中,实际上不发生迭代。执行查询时,范围变量将用作对 customers 中的每个后续元素的引用。因为编译器可以推断 cust 的类型,所以您不必显式指定此类型。其他范围变量可由 let 子句引入。
对于非泛型数据源(如 ArrayList),必须显式类型化范围变量。
筛选
也许最常用的查询操作是应用布尔表达式形式的筛选器。此筛选器使查询只返回那些表达式结果为 true 的元素。使用 where 子句生成结果。实际上,筛选器指定从源序列中排除哪些元素。在下面的示例中,只返回那些地址位于伦敦的 customers。
var queryLondonCustomers = from cust in customers
where cust.City == "London"
select cust;
您可以使用熟悉的 C# 逻辑 AND 和 OR 运算符来根据需要在 where 子句中应用任意数量的筛选表达式。例如,若要只返回位于“伦敦”AND 姓名为“Devon”的客户,您应编写下面的代码:
where cust.City=="London" && cust.Name == "Devon"
若要返回位于伦敦或巴黎的客户,您应编写下面的代码:
where cust.City == "London" || cust.City == "Paris"
排序
通常可以很方便地将返回的数据进行排序。orderby 子句将使返回的序列中的元素按照被排序的类型的默认比较器进行排序。例如,下面的查询可以扩展为按 Name 属性对结果进行排序。因为 Name 是一个字符串,所以默认比较器执行从 A 到 Z 的字母排序。
var queryLondonCustomers3 =
from cust in customers
where cust.City == "London"
orderby cust.Name ascending
select cust;
若要按相反顺序(从 Z 到 A)对结果进行排序,请使用 orderby…descending 子句。
分组
使用 group 子句,您可以按指定的键分组结果。例如,您可以指定结果应按 City 分组,以便位于伦敦或巴黎的所有客户位于各自组中。在本例中,cust.City 是键。
// queryCustomersByCity is an IEnumerable<IGrouping<string, Customer>>
var queryCustomersByCity =
from cust in customers
group cust by cust.City;
// customerGroup is an IGrouping<string, Customer>
foreach (var customerGroup in queryCustomersByCity)
{
Console.WriteLine(customerGroup.Key);
foreach (Customer customer in customerGroup)
{
Console.WriteLine(" {0}", customer.Name);
}
}
在使用 group 子句结束查询时,结果采用列表的列表形式。列表中的每个元素是一个具有 Key 成员及根据该键分组的元素列表的对象。在循环访问生成组序列的查询时,您必须使用嵌套的 foreach 循环。外部循环用于循环访问每个组,内部循环用于循环访问每个组的成员。
如果您必须引用组操作的结果,可以使用 into 关键字来创建可进一步查询的标识符。下面的查询只返回那些包含两个以上的客户的组:
// custQuery is an IEnumerable<IGrouping<string, Customer>>
var custQuery =
from cust in customers
group cust by cust.City into custGroup
where custGroup.Count() > 2
orderby custGroup.Key
select custGroup;
联接
联接运算创建数据源中没有显式建模的序列之间的关联。例如,您可以执行联接来查找符合以下条件的所有客户:位于巴黎,且从位于伦敦的供应商处订购产品。在 LINQ 中,join 子句始终针对对象集合而非直接针对数据库表运行。在 LINQ 中,您不必像在 SQL 中那样频繁使用 join,因为 LINQ 中的外键在对象模型中表示为包含项集合的属性。例如,Customer 对象包含 Order 对象的集合。不必执行联接,只需使用点表示法访问订单:
from order in Customer.Orders...
选择(投影)
select 子句生成查询结果并指定每个返回的元素的“形状”或类型。例如,您可以指定结果包含的是整个 Customer 对象、仅一个成员、成员的子集,还是某个基于计算或新对象创建的完全不同的结果类型。当 select 子句生成除源元素副本以外的内容时,该操作称为“投影”。使用投影转换数据是 LINQ 查询表达式的一种强大功能。
将多个输入联接到一个输出序列
可以使用 LINQ 查询来创建包含多个输入序列的元素的输出序列。下面的示例演示如何组合两个内存中的数据结构,但组合来自 XML 或 SQL 或数据集源的数据时可应用相同的原则。假定下面两种类类型:
class Student
{
public string First { get; set; }
public string Last {get; set;}
public int ID { get; set; }
public string Street { get; set; }
public string City { get; set; }
public List<int> Scores;
}
class Teacher
{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
public string City { get; set; }
}
下面的示例演示该查询:
class DataTransformations
{
static void Main()
{
// Create the first data source.
List<Student> students = new List<Student>()
{
new Student {First="Svetlana",
Last="Omelchenko",
ID=111,
Street="123 Main Street",
City="Seattle",
Scores= new List<int> {97, 92, 81, 60}},
new Student {First="Claire",
Last="O’Donnell",
ID=112,
Street="124 Main Street",
City="Redmond",
Scores= new List<int> {75, 84, 91, 39}},
new Student {First="Sven",
Last="Mortensen",
ID=113,
Street="125 Main Street",
City="Lake City",
Scores= new List<int> {88, 94, 65, 91}},
};
// Create the second data source.
List<Teacher> teachers = new List<Teacher>()
{
new Teacher {First="Ann", Last="Beebe", ID=945, City = "Seattle"},
new Teacher {First="Alex", Last="Robinson", ID=956, City = "Redmond"},
new Teacher {First="Michiyo", Last="Sato", ID=972, City = "Tacoma"}
};
// Create the query.
var peopleInSeattle = (from student in students
where student.City == "Seattle"
select student.Last)
.Concat(from teacher in teachers
where teacher.City == "Seattle"
select teacher.Last);
Console.WriteLine("The following students and teachers live in Seattle:");
// Execute the query.
foreach (var person in peopleInSeattle)
{
Console.WriteLine(person);
}
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
The following students and teachers live in Seattle:
Omelchenko
Beebe
*/
选择各个源元素的子集
选择源序列中的各个元素的子集有两种主要方法:
1. 若要只选择源元素的一个成员,请使用点运算。在下面的示例中,假定 Customer 对象包含几个公共属性,其中包括名为 City 的字符串。在执行此查询时,此查询将生成字符串输出序列。
var query = from cust in Customers
select cust.City;
2. 若要创建包含源元素的多个属性的元素,可以使用具有命名对象或匿名类型的对象初始值设定项。下面的示例演示如何使用匿名类型来封装各个 Customer 元素的两个属性:
var query = from cust in Customer
select new {Name = cust.Name, City = cust.City};
将内存中的对象转换为 XML
通过 LINQ 查询,可以轻松地在内存中的数据结构、SQL 数据库、ADO.NET 数据集和 XML 流或文档之间转换数据。下面的示例将内存中的数据结构中的对象转换为 XML 元素。
class XMLTransform
{
static void Main()
{
// Create the data source by using a collection initializer.
List<Student> students = new List<Student>()
{
new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores = new List<int>{97, 92, 81, 60}},
new Student {First="Claire", Last="O’Donnell", ID=112, Scores = new List<int>{75, 84, 91, 39}},
new Student {First="Sven", Last="Mortensen", ID=113, Scores = new List<int>{88, 94, 65, 91}},
};
// Create the query.
var studentsToXML = new XElement("Root",
from student in students
let x = String.Format("{0},{1},{2},{3}", student.Scores[0],
student.Scores[1], student.Scores[2], student.Scores[3])
select new XElement("student",
new XElement("First", student.First),
new XElement("Last", student.Last),
new XElement("Scores", x)
) // end "student"
); // end "Root"
// Execute the query.
Console.WriteLine(studentsToXML);
// Keep the console open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
此代码生成下面的 XML 输出:
< Root>
<student>
<First>Svetlana</First>
<Last>Omelchenko</Last>
<Scores>97,92,81,60</Scores>
</student>
<student>
<First>Claire</First>
<Last>O'Donnell</Last>
<Scores>75,84,91,39</Scores>
</student>
<student>
<First>Sven</First>
<Last>Mortensen</Last>
<Scores>88,94,65,91</Scores>
</student>
</Root>
标准查询运算符扩展方法
下面的示例演示简单的查询表达式和编写为基于方法的查询的语义上等效的查询。
class QueryVMethodSyntax
{
static void Main()
{
int[] numbers = { 5, 10, 8, 3, 6, 12};
//Query syntax:
IEnumerable<int> numQuery1 =
from num in numbers
where num % 2 == 0
orderby num
select num;
//Method syntax:
IEnumerable<int> numQuery2 = numbers.Where(num => num % 2 == 0).OrderBy(n => n);
foreach (int i in numQuery1)
{
Console.Write(i + " ");
}
Console.WriteLine(System.Environment.NewLine);
foreach (int i in numQuery2)
{
Console.Write(i + " ");
}
// Keep the console open in debug mode.
Console.WriteLine(System.Environment.NewLine);
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
}
/*
Output:
6 8 10 12
6 8 10 12
*/
两个示例的输出是相同的。您可以看到两种形式的查询变量的类型是相同的:IEnumerable<(Of <(T>)>)。
若要了解基于方法的查询,让我们进一步地分析它。注意,在表达式的右侧,where 子句现在表示为对 numbers 对象的实例方法,在您重新调用该对象时其类型为 IEnumerable<int>。如果您熟悉泛型 IEnumerable<(Of <(T>)>) 接口,那么您就会了解,它不具有 Where 方法。但是,如果您在 Visual Studio IDE 中调用 IntelliSense 完成列表,那么您不仅将看到 Where 方法,而且还会看到许多其他方法,如 Select、SelectMany、Join 和 Orderby。下面是所有标准查询运算符。
尽管看起来 IEnumerable<(Of <(T>)>) 似乎已被重新定义以包括这些附加方法,但事实上并非如此。这些标准查询运算符作为一种新的方法(称为“扩展方法”)实现。扩展方法可“扩展”现有类型;可如对类型的实例方法一样调用。标准查询运算符可扩展 IEnumerable<(Of <(T>)>),这就是您可以编写 numbers.Where(...) 的原因。
Lambda 表达式
请注意,在上面的示例中,条件表达式 (num % 2 == 0) 是作为内联参数传递到 Where 方法的:Where(num => num % 2 == 0)。此内联表达式称为 lambda 表达式。将代码编写为匿名方法或泛型委托或表达式树是一种便捷的方法,否则编写起来就要麻烦得多。在 C# 中,=> 是 lambda 运算符,可读为“goes to”。运算符左侧的 num 是输入变量,与查询表达式中的 num 相对应。编译器可推断 num 的类型,因为它了解 numbers 是泛型 IEnumerable<(Of <(T>)>) 类型。lambda 表达式与查询语法中的表达式或任何其他 C# 表达式或语句中的表达式相同;它可以包括方法调用和其他复杂逻辑。“返回值”就是表达式结果。
查询表达式
查询表达式使用类似于 SQL 或 XQuery 的声明性语法来查询 IEnumerable 集合。在编译时,查询语法转换为对 LINQ 提供程序的标准查询运算符扩展方法实现的方法调用。应用程序通过使用 using 指令指定适当的命名空间来控制范围内的标准查询运算符。下面的查询表达式获取一个字符串数组,按字符串中的第一个字符对字符串进行分组,然后对各组进行排序。
var query = from str in stringArray
group str by str[0] into stringGroup
orderby stringGroup.Key
select stringGroup;
隐式类型化变量 (var)
不必在声明并初始化变量时显式指定类型,您可以使用 var 修饰符来指示编译器推断并分配类型,如下所示:
var number = 5;
var name = "Virginia";
var query = from str in stringArray
where str[0] == 'm'
select str;
声明为 var 的变量与显式指定其类型的变量一样都是强类型。通过使用 var,可以创建匿名类型,但它可用于任何局部变量。也可以使用隐式类型声明数组。
匿名类型
匿名类型由编译器构建,且类型名称只可用于编译器。匿名类型提供了一种在查询结果中临时分组一组属性的方便方法,无需定义单独的命名类型。使用新的表达式和对象初始值设定项初始化匿名类型,如下所示:
select new {name = cust.Name, phone = cust.Phone};
扩展方法
扩展方法是一种可与类型关联的静态方法,因此可以像实例方法那样对类型调用它。实际上,此功能使您能够将新方法“添加”到现有类型,而不会实际修改它们。标准查询运算符是一组扩展方法,它们为实现 IEnumerable<(Of <(T>)>) 的任何类型提供 LINQ 查询功能。
自动实现的属性
通过自动实现的属性,可以更简明地声明属性。当您如下面的示例中所示声明属性时,编译器将创建一个私有的匿名支持字段,该字段只能通过属性 getter 和 setter 进行访问。
public string Name {get; set;}
对象和集合初始值设定项(C# 编程指南)
使用对象初始值设定项可以在创建对象时向对象的任何可访问的字段或属性分配值,而无需显式调用构造函数。下面的示例演示如何将对象初始值设定项用于命名类型。请注意,在 Test 类中使用了自动实现的属性。
private class Cat
{
// Auto-implemented properties
public int Age { get; set; }
public string Name { get; set; }
}
static void MethodA()
{
// Object initializer
Cat cat = new Cat { Age = 10, Name = "Sylvester" };
}
下面的代码计算源序列中每个 Student 的总分,然后对该查询的结果调用 Average() 方法来计算班级的平均分。请注意,查询表达式的两边使用了括号。
var studentQuery6 =
from student in students
let totalScore = student.Scores[0] + student.Scores[1] +
student.Scores[2] + student.Scores[3]
select totalScore;
double averageScore = studentQuery6.Average();
Console.WriteLine("Class average score = {0}", averageScore);
标准查询运算符概述
string sentence = "the quick brown fox jumps over the lazy dog";
// Split the string into individual words to create a collection.
string[] words = sentence.Split(' ');
// Using query expression syntax.
var query = from word in words
group word.ToUpper() by word.Length into gr
orderby gr.Key
select new { Length = gr.Key, Words = gr };
// Using method-based query syntax.
var query2 = words.
GroupBy(w => w.Length, w => w.ToUpper()).
Select(g => new { Length = g.Key, Words = g }).
OrderBy(o => o.Length);
foreach (var obj in query)
{
Console.WriteLine("Words of length {0}:", obj.Length);
foreach (string word in obj.Words)
Console.WriteLine(word);
}
对数据进行排序
1. 主要升序排序
string[] words = { "the", "quick", "brown", "fox", "jumps" };
IEnumerable<string> query = from word in words
orderby word.Length
select word;
foreach (string str in query)
Console.WriteLine(str);
2. 主要降序排序
string[] words = { "the", "quick", "brown", "fox", "jumps" };
IEnumerable<string> query = from word in words
orderby word.Substring(0, 1) descending
select word;
foreach (string str in query)
Console.WriteLine(str);
3. 次要升序排序
string[] words = { "the", "quick", "brown", "fox", "jumps" };
IEnumerable<string> query = from word in words
orderby word.Length, word.Substring(0, 1)
select word;
foreach (string str in query)
Console.WriteLine(str);
4. 次要降序排序
string[] words = { "the", "quick", "brown", "fox", "jumps" };
IEnumerable<string> query = from word in words
orderby word.Length, word.Substring(0, 1) descending
select word;
foreach (string str in query)
Console.WriteLine(str);
比较 Set 操作
Distinct
返回的序列包含输入序列的唯一元素。
Except
返回的序列只包含位于第一个输入序列但不位于第二个输入序列的元素。
Intersect
返回的序列包含两个输入序列共有的元素。
Union
返回的序列包含两个输入序列的唯一的元素。
筛选数据Where
筛选指将结果集限制为只包含那些满足指定条件的元素的操作。它又称为选择。
下图演示了对字符序列进行筛选的结果。筛选操作的谓词指定字符必须为“A”。
限定符,联结运算操作
限定符运算返回一个 Boolean 值,该值指示序列中是否有一些元素满足条件或是否所有元素都满足条件。
下图描述了两个不同源序列上的两个不同限定符运算。第一个运算询问是否有一个或多个元素为字符“A”,结果为 true。第二个运算询问是否所有元素都为字符“A”,结果为 true。
法名 | 说明 |
All | 确定是否序列中的所有元素都满足条件。 |
Any | 确定序列中是否有元素满足条件。 |
Contains | 确定序列是否包含指定的元素。 |
Join | 根据键选择器函数联接两个序列并提取值对。 |
GroupJoin | 根据键选择器函数联接两个序列,并对每个元素的结果匹配项进行分组。 |
数据分组
下图显示了对字符序列进行分组的结果。每个组的键是字符。
方法名 | 说明 |
GroupBy | 对共享公共属性的元素进行分组。每个组都由一个 IGrouping<(Of <(TKey, TElement>)>) 对象表示。 |
ToLookup | 根据键选择器函数将元素插入到 Lookup<(Of <(TKey, TElement>)>)(一个一对多字典)中。 |
元素操作
ElementAt | 返回集合中指定索引处的元素。 |
ElementAtOrDefault | 返回集合中指定索引处的元素;如果索引超出范围,则返回默认值。 |
First | 返回集合中的第一个元素或满足条件的第一个元素。 |
FirstOrDefault | 返回集合中的第一个元素或满足条件的第一个元素。如果没有这样的元素,则返回默认值。 |
Last | 返回集合中的最后一个元素或满足条件的最后一个元素。 |
LastOrDefault | 返回集合中的最后一个元素或满足条件的最后一个元素。如果没有这样的元素,则返回默认值。 |
Single | 返回集合中的唯一元素或满足条件的唯一元素。 |
SingleOrDefault | 返回集合中的唯一元素或满足条件的唯一元素。如果没有这样的元素或集合不是正好包含一个元素,则返回默认值。 |
串联运算
串联是指将一个序列追加到另一个序列的运算。
方法名 | 说明 |
Concat | 串联两个序列以组成一个序列。 |
聚合操作
聚合运算从值集合计算单个值。从一个月的日温度值计算日平均温度就是聚合运算的一个示例。
方法名 | 说明 |
Aggregate | 对集合值执行自定义聚合运算。 |
Average | 计算值集合的平均值。 |
Count | 对集合中的元素进行计数,还可以仅对满足某一谓词函数的元素进行计数。 |
LongCount | 对大型集合中的元素进行计数,还可以仅对满足某一谓词函数的元素进行计数。 |
Max | 确定集合中的最大值。 |
Min | 确定集合中的最小值。 |
Sum | 计算集合中值的总和。 |