【我與接口】C# IEnumerable、IQueryable 與 LINQ
序
學生時期,有過小組作業(yè),當時分工一人做那么兩三個頁面,然而在前端差不多的時候,我和另一個同學發(fā)生了爭執(zhí)。當時用的是簡單的三層架構(DLL、BLL、UI),我個人覺得各寫各的吧,到時候合并,而他覺得應該把底層先寫好,他好直接調(diào)用中間層的方法。
到出來工作之后,接觸接口,想整理一下這個:接口到底是個什么概念呢?
需要說明一點的是,我這里說的接口,不是API那個接口,而是“暫時沒實現(xiàn)”那個接口。
剛接觸接口類型的時候,還不太熟練,看到返回接口類型的方法,總在奇怪,這個返回的對象怎么知道它取哪個實現(xiàn)?可以看一個簡單的例子:
|
報錯 (無法創(chuàng)建抽象類或接口的實例) |
var test = new ITestInterface(); |
|
正確 |
ITestInterface infa = new TestInterface(); infa.Func1(); |
也即,返回的類型總是具類,是確定的,方法已經(jīng)實現(xiàn)的。
ITestInterface infa = new TestInterface();
其中的 ITestInterface 更像一個模具,對應這個模具造型的內(nèi)容,由TestInerface提供。
那么,接口到底如何使用?
接口的使用,要這樣看:“具備某種特征(功能)”。
例如看 ITestInterface infa = new TestInterface(); 其中,TestInterface具備有ITestInterface的特征,而ITestInterface作為有某種特征(功能)的標記,它對具體如何達到這種特征(功能)是不感興趣的,有標記就有特征。這種標記的體現(xiàn),在C#里面就是繼承。
說到這里,老朋友IEnumerable是一定要介紹的。
一、迭代器 IEnumerable
集合這種數(shù)據(jù)結構是很常見的,通常的操作是對集合的內(nèi)容做篩選,或排序。IEnumerable接口描述的是返回可循環(huán)訪問集合的枚舉數(shù),繼承這個接口,需要實現(xiàn) public IEnumerator GetEnumerator() {} 方法。
那么,IEnumerator是個什么er?繼承這個接口之后,IDE提示需要實現(xiàn)的方法——
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對象,并且會在此處調(diào)用到GetEnumerator去得到Enumerator。那么到底public IEnumerator GetEnumerator(){}要怎么實現(xiàn)呢,C# 2已經(jīng)提供了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”是什么時候出現(xiàn)的,就會發(fā)現(xiàn)[1]:
l 在第一次調(diào)用MoveNext之前,CreateEnumerable中的代碼不會被調(diào)用;
l 當調(diào)用MoveNext時,Current也同時變化;
l 在yield return的位置,代碼就停止執(zhí)行,在下一次調(diào)用MoveNext時又繼續(xù)執(zhí)行(再return一次)
yield的故事還沒有完,此處就簡短介紹。
yield return提供了逐個返回的條件,對于僅是取集合當中符合篩選條件的一項,用yield是方便的,逐個返回的情況下,不會占用過多的存儲空間。但如果涉及到排序(或者比大小、最值)的問題,那必然要求集合當中的所有數(shù)據(jù)處于可用狀態(tài),這里也出現(xiàn)了一些傳值的概念。
yield return屬于延遲執(zhí)行(Deferred Execution),延遲執(zhí)行再區(qū)分為惰性求值(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(); } }
(為了邏輯上的全面性,)與延遲執(zhí)行相對的是立即執(zhí)行(Immediately Execution),是一次返回就完成函數(shù)的操作。
二、迭代器 IQueryable
LINQ to Object 是針對本地數(shù)據(jù)存儲(local data store)來執(zhí)行查詢的,系統(tǒng)會根據(jù)lambda表達式里面的邏輯創(chuàng)建匿名的委托,并執(zhí)行代碼;
LINQ to SQL 針對的是在數(shù)據(jù)庫執(zhí)行的,會把查詢條件解析成T-SQL,并且把SQL語句發(fā)送給數(shù)據(jù)庫引擎。
關于,自動生成SQL語句這一點,可以做個嘗試,例如:創(chuàng)建了一個EF,調(diào)試監(jiān)控連接數(shù)據(jù)庫后返回的變量類型。
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);
咋一看,怎么還能是不同類型?但是再看類成員,會發(fā)現(xiàn)一些端倪:
public abstract class DbSet : DbQuery, IInternalSetAdapter public abstract class DbQuery : IOrderedQueryable, IQueryable, IEnumerable, IListSource, IInternalQueryAdapter public interface IOrderedQueryable : IQueryable, IEnumerable
好了,終于引入到這個朋友——IQueryable,IQueryable有些什么必要實現(xiàn)的方法呢?
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),它是一個有自己花樣的迭代器。這個花樣如何體現(xiàn)呢?關鍵還在于Expression、IQueryProvider上。
從字面上來看,Expression是查詢條件的表達式樹;那么Provider就是提供數(shù)據(jù)的成員了。
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得到數(shù)據(jù)結果,設其為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
下面以一個例子比較二者最大的區(qū)別[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>所內(nèi)置的LINQ to SQL機制。 (LINQ to SQL程序庫會把相關的查詢操作合起來執(zhí)行,僅向數(shù)據(jù)庫發(fā)出一次調(diào)用,即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;
|
把數(shù)據(jù)庫對象強制轉(zhuǎn)換成IEnumerable形式的序列,并把排序等工作放在本地完成。 (即會把where字句后得到的結果轉(zhuǎn)換成IEnumerable<T>的序列,再采用LINQ to Objects機制完成后續(xù),排序是通過委托在本地執(zhí)行。) |
注意:
兩種不同的數(shù)據(jù)處理方式,依循著兩套完全不同的流程。無論是用lambda表達式來撰寫查詢邏輯還是以函數(shù)參數(shù)的形式來表示這些邏輯,針對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>,則必須在本地進行。系統(tǒng)把lambda表達式編譯到方法里,在本地計算機上運行,這意味著無論有待處理的數(shù)據(jù)在不在本地,都必須先獲取過來才行。
同時,用來支持IQueryable的那些Provider未必能夠完全解析每一種查詢,通常這些Provider只能解讀幾種固定的(.NET Framework已經(jīng)實現(xiàn))的運算符(方法),如果要在查詢操作里面調(diào)用除此之外的其它方法,那可能就得把序列當成IEnumerable來查詢。
吐槽 :emmmmmm,,,本來是想寫我與接口二三事,結果竟然如此跑偏,太多細節(jié)能扣啦,知識點冥冥間也有關聯(lián),慢慢捋吧~
立Flag:本月開啟機器學習,今年要把C#基礎篇搞定。
注釋:
[1] 自《深入理解C#》(第3版)Jon Skeet 著 姚琪琳 譯
[2] 自《Effective C#》(第3版) 比爾·瓦格納 著

浙公網(wǎng)安備 33010602011771號