查询表达式和LINQ to Objects
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了查询表达式和LINQ to Objects相关的知识,希望对你有一定的参考价值。
查询表达式实际上是由编译器“预处理”为“普通”的C#代码,接着以完全普通的方式进行编译。这种巧妙的发式将查询集合到了语言中,而无须把语义改得乱七八糟
LINQ的介绍
LINQ中的基础概念
降低两种数据模型之间的阻抗失配过程中,遇到的一个问题就是,通常会涉及创建另一个模型来作为桥梁
序列
它通过IEnumerable和IEnumerable<T>接口进行封装
序列和其他集合数据结构最大的区别:当你从序列读取数据的时候,通常不知道还有多少数据项等待读取,或者不能访问任意的数据项——只能是当前这个
序列是LINQ的基础,一个查询表达式涉及的序列:一开始总是存在至少一个序列,且通常在中间过程会转换为转换为其他序列,也可能和更多的序列连接在一起
序列是数据处理流模型的基础,让我们能够只在需要的时候才对数据进行获取处理
var adultNames=from person in people
where person.Age>=18
select person.Name;
延迟执行和流处理
LINQ的这个特点称为延长处理。在最终结果的第一元素被访问的时候,Select转换才会为它的第一个元素调用Where转换。而Where转化会访问列表中的第一个元素,检查这个谓词是否匹配,并把这个元素返回给Select,最后依次提取出名称作为结果返回。
返回另外一个序列的擦欧总称为延迟执行,返回但一值的运算使用立即执行
标准查询操作符
LINQ的标准查询操作符是一个转换的集合,具有明确的含义
选择元素
以数据源作为开始,以选择作为结束
声明一个数据序列的数据源:from element in source
element只是一个标识符,它前面可以放置一个类型名称
source是一个普通的表达式
select expression
select子句被称为投影
#region 11-1打印出所有用户的袖珍查询
var query = from user in SampleData.AllUsers
select user;
foreach (var user in query)
{
Console.WriteLine(user);
}
#endregion
编译器转译是查询表达式基础的转译
#region 11-2将11-1的查询表达式被转译为一个方法调用
var query = SampleData.AllUsers.Select(user => user);//编译器不要求Select必须为一个方法,或AllUsers必须为属性,只要转换后的代码可以编译就可以了
foreach (var user in query)
{
Console.WriteLine(user);
}
#endregion
在LINQ to Objects中只进行一种调用——任何时候,参数都是委托类型,编译器将用Lambda表达式作为实参,并尽量查找具有合适签名的方法。
语言规范给出了查询表达式模式的细节,必须实现所有查询表达式的查询表达式模式,才能正常工作,不过它并没有如你所期望的那样被定义为一个接口——它通过扩展方法,能够让LINQ应用于IEnumerable<T>这样的接口
编译器转译工作原理:它为Select和Where提供了伪实现,使Select成为一个普通实例方法,而使Where成为一个扩展方法,
#region 11-3编译器转译调用伪LINQ实现中的方法
static class Extensions
{
public static Dummy<T> Where<T>(this Dummy<T> dummy, Func<T, bool> predicate)//声明Where扩展方法
{
Console.WriteLine("Where called");
return dummy;
}
}
class Dummy<T>
{
public Dummy<U> Select<U>(Func<T, U> selector)//声明Select实例方法
{
Console.WriteLine("Select called");
return new Dummy<U>();
}
}
#endregion
#region 11-3
var source = new Dummy<string>();//创建用于查询的数据源
var query = from dummy in source
where dummy.ToString() == "Ignored"
select "Anything";//通过查询表达式来调用方法var query=source.where(dummy=>dummy.ToString()=="Ignored").Select(dummy=>"Anything")
#endregion
所有LINQ提供器都把数据显示为IEnumerable<T>或IQueryable<T>。转译不依赖于任何特定类型而仅仅依赖于方法名称和参数,这是一种鸭子类型的编译形式。和集合初始化程序使用了同样的方式:使用普通重载决策来查找公共方法调用Add,而不是使用包含特定签名的Add方法的接口。查询表达式进一步利用了这种思想——转译发生在编译过程初期,以便让编译器来挑选实例方法或扩展方法。甚至可以认为,转译是在一个独立的预处理引擎中工作
范围变量和重要的投影
上下文关键字很容易理解——它们明确告知编译器我们要对数据进行的处理
数据源表达式也仅仅是普通的C#表达式——在这个例子中是一个属性,不过也可以是一个简单的方法调用或变量
范围变量不像其他种类的变量,在某些方面,它根本就不是变量。它们只能用于查询表达式中,实际代表从一个表达式传递给另一个表达式的上下文信息。它们表达了特定序列中的一个元素,而且它们被用于编译器转译中,以便把其他表达式轻易的转译为Lambda表达式。
SampleData.AllUser.Select(user=>user)
Lambda表达式的左边——提供参数名称的部分来自于范围变量的声明,而右边来自于select子句
#region 11-4仅选择user对象名称的查询
IEnumerable<string> query = from user in SampleData.AllUsers
select user.Name;
foreach (string name in query)
{
Console.WriteLine(name);
}
#endregion
在将Lambda表达式转换为Func<TSource,TResult>的时候,类型推断也发生了作用。它首先根据SampleData.AllUsers的类型推断出TSource为User,这样就知道了Lambda表达式的参数类型,并因此将user.Name作为返回string类型的属性访问表达式,也就可以推断出TResult为string。这就是Lambda表达式允许使用隐式类型参数的原因,也就是会存在如此复杂的类型推断规则的原因:这些都是LINQ引擎的"齿轮"和"活塞"。
Cast,OfType和显示类型的范围变量
范围变量都可以是隐式类型
这两个操作符很相似:都可以处理任意非类型化序列(它们是非泛型IEnumerable类的扩展方法),并返回强类型的序列。Cast通过把每个元素都转换为目标类型来处理,而OfType首先进行一个测试,以跳过任何具有错误类型的元素
#region 11-5使用Cast和OfType来处理弱类型集合
ArrayList list = new ArrayList { "First", "Second", "Third" };
IEnumerable<string> strings = list.Cast<string>();
foreach (string item in strings)
{
Console.WriteLine(item);
}
list = new ArrayList { 1, "First", ‘d‘, "dsds", 3 };
strings = list.OfType<string>();
foreach (string item in strings)
{
Console.WriteLine(item);
}
#endregion
两个操作符都对数据进行流处理,在获取元素的时候才对其进行转换
在你引入了具有显示类型的范围变量后,编译器就调用Cast来保证在查询表达式的剩余部分中使用的序列具有合适的类型
#region 11-6使用显示类型的范围变量来自动调节Cast
ArrayList list = new ArrayList { "First", "Second", "Third" };
var strings = from string entry in list
select entry.Substring(0, 3);//IEnumerable<string> strings = from entry in source.Cast<string>() select entry.Substring(0, 3);
foreach (string start in strings)
{
Console.WriteLine(start);
}
#endregion
编译器转译时改表达式调用了Cast方法,没有这个类型转换,根本就不能调用Select,因为该扩展方法只用于IEnumerable<T>,而不能用于IEnumerable。
重要概念:
- LINQ以数据序列为基础,在任何可能的地方都进行流处理
- 创建一个查询并不会执行它:大部分操作都是延迟执行
- C#3的查询表达式包括一个把表达式转换为普通C#代码的预处理阶段,接着使用类型推断,重载,Lambda表达式等这些常规的规则来恰当的对转换后的代码进行编译
- 在查询表达式中声明的变量的作用:它们仅仅是范围变量,通过它们你可以查询表达式内部一直的引用数据
它们是对编译器转换进行解释的最简单的方法之一,它们总是返回和输入同样类型的序列
使用where子句进行过滤
where 过滤表达式
编译器把这个子句转译为带有Lambda表达式的Where方法调用,它使用合适的范围变量作为这个Lambda表达式的参数,而以过滤表达式作为主体。过滤表达式当作进入数据流的每个元素的谓词,只有返回true的元素才能出现在结果序列中。使用多个where子句,会导致多个链接在一起的Where调用——只有满足所有的谓词的元素才能进入结果序列
#region 11-7使用多个where字句的查询表达式
User tim = SampleData.Users.TesterTim;
var query = from defect in SampleData.AllDefects
where defect.Status != Status.Closed
where defect.AssignedTo == tim
select defect.Summary;
foreach (var summary in query)
{
Console.WriteLine(summary);
}
#endregion
退化的查询表达式
如果我们的select子句什么都不做,只是返回同给定的序列的序列相同的序列。
编译器会删除所有对Select调用,当然,前提是在查询表达式中还有其他操作可执行时才这么做。
from defect in SampleData.AllDefects
select defect
这就是所谓的退化查询表达式。编译器会故意生成一个对Select方法的调用,即使它什么都没有做:
SampleData.AllDefects.Select(defact=>defect)
查询表达式的结果和数据源永远不会是同一个对象
使用orderby子句进行排序
#region 11-8按缺陷严重度的优先级从高到低的顺序排序
User tim = SampleData.Users.TesterTim;
var query = from defect in SampleData.AllDefects
where defect.Status != Status.Closed
where defect.AssignedTo == tim
orderby defect.Severity descending
select defect;
foreach (var defect in query)
{
Console.WriteLine("{0},{1}", defect.Severity, defect.Summary);
}
#endregion
#region 11-9先按严重度排序,而后按最后修改时间排序
User tim = SampleData.Users.TesterTim;
var query = from defect in SampleData.AllDefects
where defect.Status != Status.Closed
where defect.AssignedTo == tim
orderby defect.Severity descending, defect.LastModified
select defect;
foreach (var defect in query)
{
Console.WriteLine("{0},{1}({2:d})", defect.Severity, defect.Summary, defect.LastModified);
}
#endregion
上下文关键字orderby,后面跟一个或多个排序规则。一个排序规则就是一个表达式,后面可以紧跟ascending或descending关键字
OrderBy假设它对排序规则起决定作用,而ThenBy可理解为对之前的一个或多个排序规则起辅助作用。ThenBy只是定义为IOrderdEnumerable<T>扩展方法,这是一个由OrderBy返回的类型
能使用多个orderby子句,每个都会以它自己的OrderBy或OrderByDescending子句作为开始,最后一个才会真正“获胜”
let子句和透明标识符
用let来进行中间计算
let子句只不过引入一个新的范围变量,它的值是基于其他范围变量
let 标识符=表达式
#region 11-10在不使用let子句的情况下,按用户名称长度来排序
var query = from user in SampleData.AllUsers//两次使用了Length
orderby user.Name.Length
select user.Name;
foreach (var name in query)
{
Console.WriteLine("{0}:{1}", name.Length, name);
}
#endregion
#region 11-11使用let子句来消除冗余的计算
var query = from user in SampleData.AllUsers
let length = user.Name.Length//引入length范围变量
orderby length
select new { Name = user.Name, Length = length };
foreach (var name in query)
{
Console.WriteLine("{0}:{1}", name.Name, name.Length);
}
#endregion
连接
使用join子句的内连接
内连接涉及两个序列,一个键选择器表达式应用于第一个序列的每个元素,另一个键选择器应用于第2个序列的每个元素,连接的结果是一个包含所有配对的序列,配对的规则是第一个元素的键与第二个元素的键相同
两个键选择器表达式必须有相同的键类型
#region 11-12根据项目把缺陷和通知订阅连接在一起
var query = from defect in SampleData.AllDefects
join subscription in SampleData.AllSubscriptions
on defect.Project equals subscription.Project
select new { defect.Summary, subscription.EmailAddress };
foreach (var entry in query)
{
Console.WriteLine("{0}:{1}", entry.EmailAddress, entry.Summary);
}
#endregion
在LINQ to Objects的实现中,返回条目的顺序为:先返回使用左边序列中第一个元素的所有成对数据能被返回,接着返回使用左边序列中第二个元素的所有成对数据,以此类推。右边序列被缓存处理,不过左边序列仍然进行流处理,所有如果打算把一个巨大的序列连接到一个极小的序列上,应尽可能把小序列作为右边序列。
使用join....into子句进行分组连接
分组连接结果中的每个元素由左边序列的某个元素和右边序列的所有匹配元素的序列组成。后者用一个新的范围变量表示,该变量由join子句中into后面的标识符指定
#region 11-13使用分组连接把缺陷的订阅连接到一起
var query = from defect in SampleData.AllDefects
join subscription in SampleData.AllSubscriptions
on defect.Project equals subscription.Project
into gtoupedSubscription
select new { Defece = defect, Subscription = gtoupedSubscription };
foreach (var enrty in query)
{
Console.WriteLine(enrty.Defece.Summary);
foreach (var subscription in enrty.Subscription)
{
Console.WriteLine("{0}", subscription.EmailAddress);
}
}
#endregion
使用多个from子句进行交叉连接和合并序列
交叉连接不在序列之间执行任何匹配操作:结果包含了所有可能的元素对。它们可以简单的使用两个(或多个)from子句实现。涉及多个from子句时,其实可认为是在前面两个from子句上执行交叉连接,接着把结果序列和下一个from子句再次进行交叉连接。每个额外的from子句都通过透明标识符添加自己的范围变量
#region 11-15用户和项目的交叉连接
var query = from user in SampleData.AllUsers
from project in SampleData.AllProjects
select new { User = user, Project = project };
foreach (var pair in query)
{
Console.WriteLine("{0}/{1}", pair.User.Name, pair.Project.Name);
}
#endregion
它就像多表查询的笛卡儿积。在任意特定时刻使用的右边序列依赖于左边序列的“当前”值。也就是说,左边序列中的每个元素都用于来生成右边的一个序列,然后左边这个元素与右边新生成序列的每个元素都组成一对
#region 11-16右边序列依赖于左边元素的交叉连接
var query = from left in Enumerable.Range(1, 4)
from right in Enumerable.Range(11, left)
select new { Left = left, Right = right };
foreach (var pair in query)
{
Console.WriteLine("Left={0};Rigth={1}", pair.Left, pair.Right);
}
#endregion
分组和延续
使用group....by子句进行分组
要做查询表达式中队序列进行分组,只需要使用group...by子句
group projection by grouping
改子句和select子句一样,出现在查询表达式末尾
grouping表达式通过其键来决定序列如何分组。整个结果是一个序列,序列中的每个元素本身就是投影后元素的序列,还具有一个key属性,即用于分组的键。这样的组合封装在IGrouping<TKey,TElement>接口中的,还扩展了IEnumerable<TElement>
#region 11-17用分配来分组缺陷——无比简单的投影
var query = from defect in SampleData.AllDefects
where defect.AssignedTo != null//过滤未分配的缺陷
group defect by defect.AssignedTo;//用分配者来分组
foreach (var entry in query)
{
Console.WriteLine(entry.Key.Name);
foreach (var defect in entry)
{
Console.WriteLine("({0}) {1}", defect.Severity, defect.Summary);
}
Console.WriteLine();
}
#endregion
分组无法对结果进行流处理,它会对每个元素应用键选择和投影,并缓存被投影元素的分组序列
#region 11-18按分配者来分组缺陷——投影只保留概要信息
var query = from defect in SampleData.AllDefects
where defect.AssignedTo != null
group defect.Summary 以上是关于查询表达式和LINQ to Objects的主要内容,如果未能解决你的问题,请参考以下文章
在实体框架和 Linq to Entities 中使用规范模式和表达式