【我与接口】C# IEnumerable、IQueryable 与 LINQ
序
学生时期,有过小组作业,当时分工一人做那么两三个页面,然而在前端差不多的时候,我和另一个同学发生了争执。当时用的是简单的三层架构(DLL、BLL、UI),我个人觉得各写各的吧,到时候合并,而他觉得应该把底层先写好,他好直接调用中间层的方法。
到出来工作之后,接触接口,想整理一下这个:接口到底是个什么概念呢?
需要说明一点的是,我这里说的接口,不是API那个接口,而是“暂时没实现”那个接口。
刚接触接口类型的时候,还不太熟练,看到返回接口类型的方法,总在奇怪,这个返回的对象怎么知道它取哪个实现?可以看一个简单的例子:
报错 (无法创建抽象类或接口的实例) |
var test = new ITestInterface(); |
正确 |
ITestInterface infa = new TestInterface(); infa.Func1(); |
也即,返回的类型总是具类,是确定的,方法已经实现的。
ITestInterface infa = new TestInterface();
其中的 ITestInterface 更像一个模具,对应这个模具造型的内容,由TestInerface提供。
那么,接口到底如何使用?
接口的使用,要这样看:“具备某种特征(功能)”。
例如看 ITestInterface infa = new TestInterface(); 其中,TestInterface具备有ITestInterface的特征,而ITestInterface作为有某种特征(功能)的标记,它对具体如何达到这种特征(功能)是不感兴趣的,有标记就有特征。这种标记的体现,在C#里面就是继承。
说到这里,老朋友IEnumerable是一定要介绍的。
一、迭代器 IEnumerable
集合这种数据结构是很常见的,通常的操作是对集合的内容做筛选,或排序。IEnumerable接口描述的是返回可循环访问集合的枚举数,继承这个接口,需要实现 public IEnumerator GetEnumerator() {} 方法。
那么,IEnumerator是个什么er?继承这个接口之后,IDE提示需要实现的方法——
public class Iterator : IEnumerator { public object Current => throw new NotImplementedException(); public bool MoveNext() { … } public void Reset() { … } }
有一个当前对象,一个是否能指向下一个的判断,还有一个重置。那么,可以想象迭代器应该是这样用的:
Iterator iterator = new Iterator(); while (iterator.MoveNext()) { // Get iterator.Current to do something.. Console.WriteLine(iterator.Current.ToString()); }
但这看起来,并不太聪明,或者这样使用比较“合理”:
是不是get到了某种真相?foreach里面接受的是IEnumerable对象,并且会在此处调用到GetEnumerator去得到Enumerator。那么到底public IEnumerator GetEnumerator(){}要怎么实现呢,C# 2已经提供了yield语句简化迭代器。
public class IterationSample : IEnumerable { public IEnumerator GetEnumerator() { for (int index = 0; index < values.Length; index++) { yield return values[(index + startingPoint) % values.Length]; } } public object[] values; public int startingPoint; public IterationSample(object[] values, int startingPoint) { this.values = values; this.startingPoint = startingPoint; } }
再来使用Enumerator:
object[] objs = new object[]{"a", "b", "c", "d"}; IterationSample sam = new IterationSample(objs, 0); foreach (var str in sam) { // do something.. }
可以想象,yield是个怎么样的存在,“一次一次返回”这是我对yield的第一印象描述。但总觉得还是有些说不清楚,这种时候还是得看看书:
“yield return 语句指表示 ’暂时地’ 退出方法——事实上,可以把它当做暂停”,
既然有这种说法,那还得给出个demo[1],关于怎么个“暂停”。
(这里悄咪咪用C# 6的新语法using static System.Console; 实在懒得打 Console.WriteLine();)
class Program { static void Main(string[] args) { IEnumerable<int> iterable = CreateEnumerable(); IEnumerator<int> iterator = iterable.GetEnumerator(); WriteLine("Starting to iterate"); while (true) { WriteLine("Calling MoveNext().."); bool result = iterator.MoveNext(); WriteLine($"MoveNext result = {result}"); if (!result) break; WriteLine("Fetching Current.."); WriteLine($"..Current result = {iterator.Current.ToString()}"); } ReadLine(); } static readonly string Padding = new string(' ', 30); static IEnumerable<int> CreateEnumerable() { WriteLine("Start of CreateEnumerable()"); for (int i = 0; i < 2; i++) { WriteLine($"{Padding} About to yield {i}"); yield return i; WriteLine($"{Padding} After yield"); } WriteLine($"{Padding} Yielding final value"); yield return -1; WriteLine($"{Padding} End of CreateEnumerable"); } }
此处可以留意“After yield”是什么时候出现的,就会发现[1]:
l 在第一次调用MoveNext之前,CreateEnumerable中的代码不会被调用;
l 当调用MoveNext时,Current也同时变化;
l 在yield return的位置,代码就停止执行,在下一次调用MoveNext时又继续执行(再return一次)
yield的故事还没有完,此处就简短介绍。
yield return提供了逐个返回的条件,对于仅是取集合当中符合筛选条件的一项,用yield是方便的,逐个返回的情况下,不会占用过多的存储空间。但如果涉及到排序(或者比大小、最值)的问题,那必然要求集合当中的所有数据处于可用状态,这里也出现了一些传值的概念。
yield return属于延迟执行(Deferred Execution),延迟执行再区分为惰性求值(Lazy Evaluation)和热情求值(Eager Evaluation)。
Deferred but eager execution |
Deferred and lazy execution |
IEnumerable<int> GetComputation(int maxIndex) { var result = new int[maxIndex]; for(int i = 0; i < maxIndex; i++) { result[i] = Computation(i); } foreach(var value in result) { yield return value; } } |
IEnumerable<int> GetComputation(int maxIndex) { for(int i = 0; i < maxIndex; i++) { yield return Computation(i); } } |
详见:https://stackoverflow.com/questions/2515796/deferred-execution-and-eager-evaluation
下面这个例子,是惰性求值,迭代器返回的值受lambda表达式控制,并且是在每一次访问到这一个“点”的时候,再去返回 “点”的处理结果。热情求值是直接返回“点”,没有再过处理。两相比较,还得看具体的编程情况以作选择,此处不赘述。
static void Main(string[] args) { var sequence = Generate(10, () => DateTime.Now); foreach (var value in sequence) WriteLine($"{value:T}"); } static IEnumerable<TResult> Generate<TResult>(int number, Func<TResult> generator) { for (var i = 0; i < number; i++) { Sleep(400); yield return generator(); } }
(为了逻辑上的全面性,)与延迟执行相对的是立即执行(Immediately Execution),是一次返回就完成函数的操作。
二、迭代器 IQueryable
LINQ to Object 是针对本地数据存储(local data store)来执行查询的,系统会根据lambda表达式里面的逻辑创建匿名的委托,并执行代码;
LINQ to SQL 针对的是在数据库执行的,会把查询条件解析成T-SQL,并且把SQL语句发送给数据库引擎。
关于,自动生成SQL语句这一点,可以做个尝试,例如:创建了一个EF,调试监控连接数据库后返回的变量类型。
var dbcontext = new CM_FORTESTEntities(); var tb1 = dbcontext.tblEmployees; var tb2 = dbcontext.tblEmployees.Where(a => a.Id == 1); var tb3 = dbcontext.tblEmployees.Where(a => a.Gender == "Male").OrderByDescending(a => a.Id);
咋一看,怎么还能是不同类型?但是再看类成员,会发现一些端倪:
public abstract class DbSet : DbQuery, IInternalSetAdapter public abstract class DbQuery : IOrderedQueryable, IQueryable, IEnumerable, IListSource, IInternalQueryAdapter public interface IOrderedQueryable : IQueryable, IEnumerable
好了,终于引入到这个朋友——IQueryable,IQueryable有些什么必要实现的方法呢?
public class QueryableSample : IQueryable { public Expression Expression => throw new NotImplementedException(); public Type ElementType => throw new NotImplementedException(); public IQueryProvider Provider => throw new NotImplementedException(); public IEnumerator GetEnumerator() { throw new NotImplementedException(); } }
IQueryable是IEnumerable的孩子(IQueryable : IEnumerable),它是一个有自己花样的迭代器。这个花样如何体现呢?关键还在于Expression、IQueryProvider上。
从字面上来看,Expression是查询条件的表达式树;那么Provider就是提供数据的成员了。
public class QueryableSample : IQueryable { public Expression Expression { get; } public Type ElementType => typeof(ModelItem); public IQueryProvider Provider { get; } public IEnumerator GetEnumerator() { return Provider.Execute<IEnumerable>(Expression).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public QueryableSample(IQueryProvider provider, Expression expression) { if (provider == null) throw new ArgumentNullException("provider"); if (expression == null) throw new ArgumentNullException("expression"); Provider = provider; Expression = expression; } }
预感中,Provider会是个重要角色:
public class QueryProvider : IQueryProvider |
|
IQueryable CreateQuery(Expression expression) |
return new QueryableSample(this, expression); |
IQueryable<TElement> CreateQuery<TElement>(Expression expression) |
return (IQueryable<TElement>) new QueryableSample(this, expression); |
object Execute(Expression expression) |
return QueryResult.Execute(expression, false); |
TResult Execute<TResult>(Expression expression) |
bool IsEnumerable = (typeof(TResult).Name == "IEnumerable`1"); return (TResult)QueryResult.Execute(expression, IsEnumerable); |
public class QueryProvider : IQueryProvider { public IQueryable CreateQuery(Expression expression) { return new QueryableSample(this, expression); } public IQueryable<TElement> CreateQuery<TElement>(Expression expression) { return (IQueryable<TElement>) new QueryableSample(this, expression); } public object Execute(Expression expression) { return QueryResult.Execute(expression, false); } public TResult Execute<TResult>(Expression expression) { bool IsEnumerable = (typeof(TResult).Name == "IEnumerable`1"); return (TResult)QueryResult.Execute(expression, IsEnumerable); } } public sealed class QueryResult { public static object Execute(Expression expression, bool isEnumerable) { // 利用expression得到数据结果,设其为records QueryableSample records = null; if (isEnumerable) return records.Provider.CreateQuery(expression); else return records.Provider.Execute(expression); } }
在github上找到了个详尽些的QueryableDemo可以看: https://github.com/andreychizhov/NestQueryableProvider
三、IEnumerable 与 IQueryable
下面以一个例子比较二者最大的区别[2]:
var q = from c in dbContext.Customers where c.City == "London" select c; var finalAnswer = from c in q orderby c.Name select c;
|
使用IQueryable<T>所内置的LINQ to SQL机制。 (LINQ to SQL程序库会把相关的查询操作合起来执行,仅向数据库发出一次调用,即where和orderby都是在同一次SQL查询中完成。) |
var q = (from c in dbContext.Customers where c.City == "London" select c).AsEnumerable(); var finalAnswer = from c in q orderby c.Name select c;
|
把数据库对象强制转换成IEnumerable形式的序列,并把排序等工作放在本地完成。 (即会把where字句后得到的结果转换成IEnumerable<T>的序列,再采用LINQ to Objects机制完成后续,排序是通过委托在本地执行。) |
注意:
两种不同的数据处理方式,依循着两套完全不同的流程。无论是用lambda表达式来撰写查询逻辑还是以函数参数的形式来表示这些逻辑,针对IEnumerable<T>所设计的那些扩展方法都将其视为委托。反之,针对IQueryable<T>的那些扩展方法用的则是表达式树。【表达式树 可以把各种逻辑合并起来成一条SQL语句。】
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)
如果使用IEnumerable<T>,则必须在本地进行。系统把lambda表达式编译到方法里,在本地计算机上运行,这意味着无论有待处理的数据在不在本地,都必须先获取过来才行。
同时,用来支持IQueryable的那些Provider未必能够完全解析每一种查询,通常这些Provider只能解读几种固定的(.NET Framework已经实现)的运算符(方法),如果要在查询操作里面调用除此之外的其它方法,那可能就得把序列当成IEnumerable来查询。
吐槽 :emmmmmm,,,本来是想写我与接口二三事,结果竟然如此跑偏,太多细节能扣啦,知识点冥冥间也有关联,慢慢捋吧~
立Flag:本月开启机器学习,今年要把C#基础篇搞定。
注释:
[1] 自《深入理解C#》(第3版)Jon Skeet 著 姚琪琳 译
[2] 自《Effective C#》(第3版) 比尔·瓦格纳 著