查询表达式(LINQ)简介
LINQ是Language Integrated Query的简称,LINQ定义了一组标准查询操作符允许查询作用于所有基于IEnumerable<T>接口的源.
LINQ包括五个部分:LINQ to Objects、LINQ to DataSets、LINQ to SQL、LINQ to Entities、LINQ to XML.
所有 LINQ 查询操作都由以下三个不同的操作组成:
获取数据源。
创建查询。
执行查询。
下面的示例演示如何用源代码表示查询操作的三个部分。为方便起见,此示例将一个整数数组用作数据源;但其中涉及的概念同样适用于其他数据源。
本主题的其余部分也会引用此示例。
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<T> 接口。 这一事实意味着该数据源可以用 LINQ 进行查询。 查询在 foreach 语句中执行,因此,foreach 需要 IEnumerable 或 IEnumerable<T>。支持 IEnumerable<T> 或派生接口 (如泛型 IQueryable<T> 的类型称为"可查询 类型"。
可查询类型不需要进行修改或特殊处理就可以用作 LINQ 数据源。 如果源数据还没有作为可查询类型出现在内存中,则 LINQ 提供程序必须以此方式表示源数据。 例如,LINQ to XML 将 XML 文档加载到可查询的 XElement 类型中:
// Create a data source from an XML document.
// using System.Xml.Linq;
XElement contacts = XElement.Load(@"c:\myContactList.xml");
在 LINQ to SQL 中,首先手动或使用 Object Relational Designer (O/R Designer) 在设计时创建对象关系映射。 针对这些对象编写查询,然后由 LINQ to SQL 在运行时处理与数据库的通信。 在下面的示例中,Customers 表示数据库中的特定表,并且查询结果的类型 IQueryable<T> 派生自 IEnumerable<T>。
Northwnd db = new Northwnd(@"c:\northwnd.mdf");
// Query for customers in London.
IQueryable<Customer> custQuery =
from cust in db.Customers
where cust.City == "London"
select cust;
查询
查询指定要从数据源中检索的信息。查询还可以指定在返回这些信息之前如何对其进行排序、分组和结构化。 查询存储在查询变量中,并用查询表达式进行初始化。 为使编写查询的工作变得更加容易,C# 引入了新的查询语法。
上一个示例中的查询从整数数组中返回所有偶数。该查询表达式包含三个子句:from、where 和 select。 (如果您熟悉 SQL,您会注意到这些子句的顺序与 SQL 中的顺序相反。)from 子句指定数据源,where 子句应用筛选器,select 子句指定返回的元素的类型。目前需要注意的是,在 LINQ 中,查询变量本身不执行任何操作并且不返回任何数据。 它只是存储在以后某个时刻执行查询时为生成结果而必需的信息。
from 子句
查询表达式必须以 from 子句开头。 另外,查询表达式还可以包含子查询,子查询也是以 from 子句开头。 from 子句指定以下内容:
将对其运行查询或子查询的数据源。
一个本地范围变量,表示源序列中的每个元素。
范围变量和数据源都是强类型。from 子句中引用的数据源的类型必须为 IEnumerable、IEnumerable<T> 或一种派生类型(如 IQueryable<T>)。
在上面的示例中,numbers 是数据源,而 num 是范围变量。请注意,这两个变量都是强类型,即使使用了 var 关键字也是如此。
where 子句
where 子句用在查询表达式中,用于指定将在查询表达式中返回数据源中的哪些元素。它将一个布尔条件(“谓词”)应用于每个源元素(由范围变量引用),并返回满足指定条件的元素。 一个查询表达式可以包含多个 where 子句,一个子句可以包含多个谓词子表达式。 如果移除 where 子句,则会返回数据源中的所有数字。 表达式 num < 5 是应用于每个元素的谓词。
在单一 where 子句内,可以使用 && 和 || 运算符根据需要指定任意多个谓词。(where num < 5 && num % 2 == 0)。
where 子句可以包含一个或多个返回布尔值的方法。(where IsEven(num))
where 子句是一种筛选机制。 除了不能是第一个或最后一个子句外,它几乎可以放在查询表达式中的任何位置。 where 子句可以出现在 group 子句的前面或后面,具体情况取决于是必须在对源元素进行分组之前还是之后来筛选源元素。
如果指定的谓词对于数据源中的元素无效,则会发生编译时错误。
select 子句
在查询表达式中,select 子句可以指定将在执行查询时产生的值的类型。 该子句的结果将基于前面所有子句的计算结果以及 select 子句本身中的所有表达式。 查询表达式必须以 select 子句或 group 子句结束。
select 子句产生的序列的类型决定了查询变量的类型。 在最简单的情况下,select 子句仅指定范围变量。 这会使返回的序列包含与数据源具有相同类型的元素。
不转换源数据的查询
下图演示不对数据执行转换的 LINQ to Objects 查询操作。 源包含一个字符串序列,查询输出也是一个字符串序列。
数据源的类型参数决定范围变量的类型。
选择的对象的类型决定查询变量的类型。 此处的 name 为一个字符串。 因此,查询变量是一个 IEnumerable<string>。
在 foreach 语句中循环访问查询变量。 因为查询变量是一个字符串序列,所以迭代变量也是一个字符串。
转换源数据的查询
下图演示对数据执行简单转换的 LINQ to SQL 查询操作。 查询将一个 Customer 对象序列用作输入,并只选择结果中的 Name 属性。 因为 Name 是一个字符串,所以查询生成一个字符串序列作为输出。
数据源的类型参数决定范围变量的类型。
select 语句返回 Name 属性,而非完整的 Customer 对象。 因为 Name 是一个字符串,所以 custNameQuery 的类型参数是 string,而非 Customer。
因为 custNameQuery 是一个字符串序列,所以 foreach 循环的迭代变量也必须是 string。
下图演示稍微复杂的转换。 select 语句返回只捕获原始 Customer 对象的两个成员的匿名类型。
数据源的类型参数始终为查询中的范围变量的类型。
因为 select 语句生成匿名类型,所以必须使用 var 隐式类型化查询变量。
因为查询变量的类型是隐式的,所以 foreach 循环中的迭代变量也必须是隐式的。
让编译器推断类型信息
虽然您应该了解查询操作中的类型关系,但是您也可以选择让编译器为您执行全部工作。 关键字 var 可用于查询操作中的任何局部变量。 下图与前面讨论的第二个示例完全等效。 唯一的区别是编译器将为查询操作中的各个变量提供强类型:
group 子句
group 子句返回一个 IGrouping<TKey, TElement> 对象序列,这些对象包含零个或更多个与该组的键值匹配的项。例如,可以按照每个字符串中的第一个字母对字符串序列进行分组。 在这种情况下,第一个字母是键且具有 char 类型,并且存储在每个 IGrouping<TKey, TElement> 对象的 Key 属性中。 编译器可推断该键的类型。
如果您想要对每个组执行附加查询操作,则可以使用 into 上下文关键字指定一个临时标识符。
使用 into 时,必须继续编写该查询,并最终用一个 select 语句或另一个 group 子句结束该查询,如下面的代码摘录所示:
// Group students by the first letter of their last name
// Query variable is an IEnumerable<IGrouping<char, Student>>
var studentQuery2 =
from student in students
group student by student.Last[0] into g
orderby g.Key
select g;
枚举组查询的结果
由于 group 查询产生的 IGrouping<TKey, TElement> 对象实质上是列表的列表,因此必须使用嵌套的 foreach 循环来访问每一组中的各个项。外部循环用于循环访问组键,内部循环用于循环访问组本身中的每个项。 组可能具有键,但没有元素。 以下是执行上述代码示例中的查询的 foreach 循环:
// Iterate group items with a nested foreach. This IGrouping encapsulates
// a sequence of Student objects, and a Key of type char.
// For convenience, var can also be used in the foreach statement.
foreach (IGrouping<char, Student> studentGroup in studentQuery2)
{
Console.WriteLine(studentGroup.Key);
// Explicit type for student could also be used here.
foreach (var student in studentGroup)
{
Console.WriteLine(" {0}, {1}", student.Last, student.First);
}
}
键类型
组键可以是任何类型,如字符串、内置数值类型、用户定义的命名类型或匿名类型。
按字符串进行分组
上述代码示例使用的是 char。 可以很容易地改为指定字符串键,如完整的姓氏:
// Same as previous example except we use the entire last name as a key.
// Query variable is an IEnumerable<IGrouping<string, Student>>
var studentQuery3 =
from student in students
group student by student.Last;
按布尔进行分组
下面的示例演示使用布尔值作为键将结果划分成两个组。 请注意,该值是由 group 子句中的子表达式产生的。
class GroupSample1
{
// The element type of the data source.
public class Student
{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
public List<int> Scores;
}
public static List<Student> GetStudents()
{
// Use a collection initializer to create the data source. Note that each element
// in the list contains an inner sequence of scores.
List<Student> students = new List<Student>
{
new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores= new List<int> {97, 72, 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> {99, 89, 91, 95}},
new Student {First="Cesar", Last="Garcia", ID=114, Scores= new List<int> {72, 81, 65, 84}},
new Student {First="Debra", Last="Garcia", ID=115, Scores= new List<int> {97, 89, 85, 82}}
};
return students;
}
static void Main()
{
// Obtain the data source.
List<Student> students = GetStudents();
// Group by true or false.
// Query variable is an IEnumerable<IGrouping<bool, Student>>
var booleanGroupQuery =
from student in students
group student by student.Scores.Average() >= 80; //pass or fail!
// Execute the query and access items in each group
foreach (var studentGroup in booleanGroupQuery)
{
Console.WriteLine(studentGroup.Key == true ? "High averages" : "Low averages");
foreach (var student in studentGroup)
{
Console.WriteLine(" {0}, {1}:{2}", student.Last, student.First, student.Scores.Average());
}
}
// Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Low averages
Omelchenko, Svetlana:77.5
O'Donnell, Claire:72.25
Garcia, Cesar:75.5
High averages
Mortensen, Sven:93.5
Garcia, Debra:88.25
*/
按数值范围进行分组
下一个示例使用表达式创建表示百分比范围的数值组键。 请注意,该示例使用 let 作为方法调用结果的方便存储位置,从而无需在 group 子句中调用该方法两次。 另请注意,在 group 子句中,为了避免发生“被零除”异常,代码进行了相应检查以确保学生的平均成绩不为零。
var studentQuery = from student in students let avg = (int)student.Scores.Average() group student by (avg == 0 ? 0 : avg / 10) into g orderby g.Key select g;
按复合键进行分组
当您想要按照多个键对元素进行分组时,可使用复合键。 通过使用匿名类型或命名类型来存储键元素,可以创建复合键。 在下面的示例中,假定已经使用名为 surname 和 city 的两个成员声明了类 Person。 group 子句使得为每组具有相同姓氏和相同城市的人员创建一个单独的组。
group person by new {name = person.surname, city = person.city};
如果必须将查询变量传递给其他方法,请使用命名类型。 使用自动实现的属性作为键来创建一个特殊类,然后重写 Equals 和 GetHashCode 方法。 还可以使用结构;在此情况下,并不绝对需要重写这些方法。
————————————————
版权声明:本文为CSDN博主「62guangye」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/62guangye/article/details/11991685