序 學生時期,有過小組作業,當時分工一人做那麼兩三個頁面,然而在前端差不多的時候,我和另一個同學發生了爭執。當時用的是簡單的三層架構(DLL、BLL、UI),我個人覺得各寫各的吧,到時候合併,而他覺得應該把底層先寫好,他好直接調用中間層的方法。 到出來工作之後,接觸介面,想整理一下這個:介面到底是個 ...
序
學生時期,有過小組作業,當時分工一人做那麼兩三個頁面,然而在前端差不多的時候,我和另一個同學發生了爭執。當時用的是簡單的三層架構(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; } }View Code
預感中,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); } }View Code
在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版) 比爾·瓦格納 著