查询表达式实际上是由编译器“预处理”为“普通”的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 sourceelement只是一个标识符,它前面可以放置一个类型名称source是一个普通的表达式 select expressionselect子句被称为投影#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方法的接口。查询表达式进一步利用了这种思想——转译发生在编译过程初期,以便让编译器来挑选实例方法或扩展方法。甚至可以认为,转译是在一个独立的预处理引擎中工作范围变量和重要的投影 #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。重要概念:#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.AllDefectsselect 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来进行中间计算 #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
连接 #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
分组和延续 #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 by defect.AssignedTo;
foreach (var entry in query)
{
Console.WriteLine(entry.Key.Name);
foreach (var summary in entry)
{
Console.WriteLine(" {0}", summary);
}
Console.WriteLine();
}
#endregion
查询延续查询延续提供了一种方法,把一个查询表达式的结果用作另外一个查询表达式初始序列它可以用于group....by和select字句上,语法对于两者是一样的——你只需要使用上下文关键字into,并为新的范围变量提供一个名称就可以了#region 11-19使用另外一个投影来延续分组结果
var query = from defect in SampleData.AllDefects
where defect.AssignedTo != null
group defect by defect.AssignedTo into grouped
select new { Assignee = grouped.Key, Count = grouped.Count() };//在第二部分使用grouped范围变量,不过defect范围变量不在可用——它已经超出了它的作用域
foreach (var entry in query)
{
Console.WriteLine("{0}:{1}", entry.Assignee.Name, entry.Count);
}
#endregion
join....into不是延续 不能形成一个延续的结构,在分组连接中,你任然可以使用所以的早期范围变量,延续会清除之前的范围变量,只有在延续中声明的范围变量才能在供后续使用 #region 11-20在group和select子句之后的查询表达式延续
var query = from defect in SampleData.AllDefects
where defect.AssignedTo != null
group defect by defect.AssignedTo into grouped
select new
{
Assignee = grouped.Key,
Count = grouped.Count()
} into result
orderby result.Count descending
select result;
foreach (var entry in query)
{
Console.WriteLine("{0}:{1}", entry.Assignee.Name, entry.Count);
}
#endregion
在查询表达式和点标记之间作出选择查询表达式在编译之前,先被转译成普通的C#。用普通的C#调用LINQ查询操作符来代替查询表达式,很多开发者称其为点标记 #region 只能用于点标记操作
var que = SampleData.AllUsers.Where(I1 => I1.Name.Length % 2 == 0).Select((I1, I2) => new { I1, I2 });
foreach (var entry in que)
{
Console.WriteLine(entry.I2);
}
Console.WriteLine();
foreach (var entry in que)
{
Console.WriteLine(entry.I1);
}
Console.ReadKey();
#endregion
本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。