.NET深入解析LINQ框架(一:LINQ優雅的前奏)


閱讀目錄:

  • 1.LINQ簡述
  • 2.LINQ優雅前奏的音符
    • 2.1.隱式類型 (由編輯器自動根據表達式推斷出對象的最終類型)
    • 2.2.對象初始化器 (簡化了對象的創建及初始化的過程)
    • 2.3.Lambda表達式 (對匿名方法的改進,加入了委托簽名的類型推斷並很好的與表達式樹的結合)
    • 2.4.擴展方法 (允許在不修改類型的內部代碼的情況下為類型添加獨立的行為)
    • 2.5.匿名類型 (由對象初始化器推斷得出的類型,該類型在編譯后自動創建)
    • 2.6.表達式目錄樹(用數據結構表示程序邏輯代碼)
  • 3.LINQ框架的主要設計模型
    • 3.1.鏈式設計模式(以流水線般的鏈接方式設計系統邏輯)
    • 3.2.鏈式查詢方法(逐步加工查詢表達式中的每一個工作點)
  • 4.LINQ框架的核心設計原理
    • 4.1.托管語言之上的語言(LINQ查詢表達式)
    • 4.2.托管語言構造的基礎(LINQ依附通用接口與查詢操作符對應的方法對接)
    • 4.3.深入IEnumerable、IEnumerable<T>、Enumerable(LINQ to Object框架的入口)
    • 4.4.深入IQueryable、IQueryable<T>、Queryable(LINQ to Provider框架的入口)
    • 4.5.LINQ針對不同數據源的查詢接口
  • 5.動態LINQ查詢(動態構建Expression<T>表達式樹)
  • 6.DLR動態語言運行時(基於CLR之上的動態語言運行時)

1】.LINQ簡述

LINQ簡稱語言集成查詢,設計的目的是為了解決在.NET平台上進行統一的數據查詢。

微軟最初的設計目的是為了解決對象/關系映射的解決方案,通過簡單的使用類似T-SQL的語法進行數據實體的查詢和操作。不過好的東西最終都能良性的發展演化,變成了如今.NET平台上強大的統一數據源查詢接口。

我們可以使用LINQ查詢內存中的對象(LINQ to Object)、數據庫(LINQ to SQL)、XML文檔(LINQ to XML),還有更多的自定義數據源。

使用LINQ查詢自定義的數據源需要借助LINQ框架為我們提供的IQueryable、IQueryProvider兩個重量級接口。后面的文章將講解到,這里先了解一下。

在LINQ未出現之前,我們需要掌握很多針對不同數據源查詢的接口技術,對於OBJECT集合我們需要進行重復而枯燥的循環迭代。對於數據庫我們需要使用諸多T-SQL\PL-SQL之類的數據庫查詢語言。對於XML我們需要使用XMLDOM編程接口或者XPATH之類的東西,需要我們掌握的東西太多太多,即費力又容易忘。

那么LINQ是如何做到對不同的數據源進行統一的訪問呢?它的優雅不是一天兩天就修來的,歸根到底還得感謝C#的設計師們,是他們讓C#能如此完美的演變,最終造就LINQ的優雅。

下面我們來通過觀察C#的每一次演化,到底在哪里造就了LINQ的優雅前奏。

2】.LINQ優雅前奏的音符

  • 2.1.隱式類型(由編輯器自動根據表達式推斷出對象的最終類型)

隱式類型其實是編輯器玩的語法糖而已,但是它在很大程度上方便了我們編碼。熟悉JS的朋友對隱式類型不會陌生,但是JS中的隱式類型與這里的C#隱式類型是有很大區別的。盡管在語法上是一樣的都是通過var關鍵字進行定義,但是彼此最終的運行效果是截然不同。

JS是基於動態類型系統設計原理設計的,而C#是基於靜態類型系統設計的,兩者在設計原理上就不一樣,到最后的運行時更不同。

這里順便推薦一本C#方面比較深入的書籍《深入解析C#》,想深入學習C#的朋友可以看看。這書有兩版,第二版是我們熟悉的姚琪琳大哥翻譯的很不錯。借此謝謝姚哥為我們翻譯這么好的一本書。這本書很詳細的講解了C#的發展史,包括很多設計的歷史淵源。來自大師的手筆,非常具有學習參考價值,不可多得的好書。

我們通過一個簡短的小示例來快速的結束本小節。

View Code
 1 List<Order> OrderList = new List<Order>() 
 2             { 
 3                 new Order(){ Count=1}, 
 4                 new Order(){ Count=2}, 
 5                 new Order(){ Count=3} 
 6             }; 
 7             foreach (Order order in OrderList) 
 8             { 
 9                 Console.WriteLine(order.Count); 
10             }

這里我定義了一個List<Order>對象並且初始化了幾個值,然后通過foreach迭代數據子項。其實這種寫法很正常,也很容易理解。但是從C#3起加入了var關鍵字,編輯器對var關鍵字進行了自動分析類型的支持,請看下面代碼。

View Code
 1 var OrderList = new List<Order>() 
 2             { 
 3                 new Order(){ Count=1}, 
 4                 new Order(){ Count=2}, 
 5                 new Order(){ Count=3} 
 6             }; 
 7             foreach (var order in OrderList) 
 8             { 
 9                 Console.WriteLine(order.Count); 
10             }

編輯器可以智能的分析出我們定義是什么類型,換句話說在很多時候我們確實需要編輯器幫我們在編譯時確定對象類型。這在LINQ中很常見,在你編寫LINQ查詢表達式時,你人為的去判斷對象要返回的類型是很不現實的,但是由編譯器來自動的根據語法規則進行分析就很理想化了。由於LINQ依賴於擴展方法,進行鏈式查詢,所以類型在編寫時是無法確定的。后面的文章將詳細的講解到,這里先了解一下。

  • 2.2.對象初始化器(簡化了對象的創建及初始化的過程)

其實對象初始化器是一個簡單的語法改進,目的還是為了方便我們進行對象的構造。(所謂萬事俱備只欠東風,這個東風就是LINQ的方案。所以必須得先萬事俱備才行。)

那么對象初始化器到底有沒有多大的用處?我們還是先來目睹一下它的語法到底如何。

View Code
1 var order = new Order() { Count = 10, OrderId = "123", OrderName = "采購單" };//屬性初始化
2 
3 var OrderList = new List<Order>() 
4             { 
5                 new Order(){ Count=1, OrderId="1",OrderName="采購單"}, 
6                 new Order(){ Count=2, OrderId="2",OrderName="采購單"}, 
7                 new Order(){ Count=3, OrderId="3",OrderName="采購單"} 
8             };//集合初始化

注意:對象初始化器只能用在屬性、公共字段上。

屬性初始化用這種語法編寫的效果和直接用(order.Count=10;order.OrderId="123";order.OrderName="采購單";)是相等的。

集合初始化使用大括號的多行語法也很容易理解。類不具體的子對象的數據賦值是相同的。

我想對代碼有追求的朋友都會很喜歡這種語法,確實很優美。

  • 2.3.Lambda表達式(對匿名方法的改進,加入了委托簽名的類型推斷並很好的與表達式樹的結合)

我想沒有朋友對Lambda表達式陌生的,如果你對Lambda表達式陌生的也沒關系,這里照看不誤。后面再去補習一下就行了。

在LINQ的查詢表達式中,到處都是Lambda造就的優雅。通過封裝匿名方法來達到強類型的鏈式查詢。

Lambda是函數式編程語言中的特性,將函數很簡單的表示起來。不僅在使用時方便,查找定義也很方便。在需要的時候很簡單定義就可以使用了,避免了在使用委托前先定義一個方法的繁瑣。Lambda表達式與匿名委托在語法上是有區別的,當然這兩者都是對匿名函數的封裝。但是他們的出現是匿名委托早於Lambda。所以看上去還是Lambda顯得優雅。

下面我們來看一個小示例,簡單的了解一下Lambda的使用原理,最重要的是它優於匿名委托哪里?

View Code
 1 /// <summary> 
 2         /// 按照指定的邏輯過濾數據 
 3         /// </summary> 
 4         public static IEnumerable<T> Filter<T>(IEnumerable<T> ObjectList, Func<T, bool> FilterFunc) 
 5         { 
 6             List<T> ResultList = new List<T>(); 
 7             foreach (var item in ObjectList) 
 8             { 
 9                 if (FilterFunc(item)) 
10                     ResultList.Add(item); 
11             } 
12             return ResultList;  
13         }

我們定義一個用來過濾數據的通用方法,這是個泛型方法,在使用時需要指定類型實參。方法有兩個參數,第一個是要過濾的數據集合,第二個是要進行過濾的邏輯規則封裝。

我們看一下調用的代碼:

View Code
1 int[] Number = new int[5] { 1, 2, 3, 4, 5 }; 
2 IEnumerable<int> result = Filter<int>(Number, (int item) => { return item > 3; });
3 
4 foreach (var item in result) 
5             { 
6                 Console.WriteLine(item); 
7             }

我們這里定義的邏輯規則是,只要大於3的我就把提取出來並且返回。很明顯這里的(int item) => { return item > 3; }語法段就是Lambda表達式,它很方便的封裝了方法的邏輯。從這點上看Lambda明顯要比匿名委托強大很多,最重要的是它還支持泛型的類型推斷特性。

那么什么是泛型的類型推斷?

其實泛型的類型推斷說簡單點就是類型實參不需要我們顯示的指定,編輯器可以通過分析表達式中的潛在關系自動的得出類型實參的類型。

說的有點空洞,我們還是看具體的代碼比較清晰。

View Code
1 int[] Number = new int[5] { 1, 2, 3, 4, 5 }; 
2 var result = Filter(Number, (int item) => { return item > 3; });

我將上面的代碼修改成了不需要顯示指定泛型類型實參調用,這里也是可以的。

我們在定義Filter<T>泛型方法時將Func<T,bool>泛型委托中的T定義為匿名函數的參數類型,所以在我們使用的時候需要指定出類型實參(int item)中的item來表示委托將要使用的類型參數形參。在編輯器看來我們在定義泛型方法Filter時所用的泛型占位符T也恰巧是Filter方法的形參數據類型Func<T,bool>中使用的調用參數類型,所以這里的語法分析規則能准確的推斷出我們使用的同一種泛型類型實參。(這里要記住目前IDE編輯器只支持方法調用的泛型類型推斷,也就是說其他方面的泛型使用是不支持隱式的類型推斷,還是需要我們手動加上類型實參。)

這里順便提一下關於延遲加載技術,延遲加載技術在集合類遍歷非常有用,尤其是在LINQ中。很多時候我們對集合的處理不是實時的,也就是說我獲取集合的數據不是一次性的,需要在我需要具體的某一個項的時候才讓我去處理關於獲取的代碼。我稍微的改動了一下Filter代碼:

View Code
 1 /// <summary> 
 2         /// 按照指定的邏輯過濾數據。具有延遲加載的特性。 
 3         /// </summary> 
 4         public static IEnumerable<T> FilterByYield<T>(IEnumerable<T> ObjectList, Func<T, bool> FilterFunc) 
 5         { 
 6             foreach (var item in ObjectList) 
 7             { 
 8                 if (FilterFunc(item)) 
 9                     yield return item; 
10             } 
11         }

這里使用了yield關鍵字,使用它我們可以在方法內部形成一個自動的狀態機結構。簡單點講也就是說系統會幫我們自動的實現一個繼承了IEnumerable<T>接口的對象,在之前我們需要自己去實現迭代器接口成員,很費時費力而且性能不好。用這種方式定義的方法后,我們只有在遍歷具體的集合時方法才會被調用,也算是一個很大的性能提升。

泛型類型推斷的不足之處;

當然類型推斷還存在不足的地方,這里可以順便參見一下我們老趙大哥的一篇文章:“C#編譯器對泛型方法調用作類型推斷的奇怪問題”;我在實際工作中也遇到過一個很頭疼問題,這里順便跟大家分享一下。按照常理說我在泛型方法的形參里面定義一個泛型的委托,他們的形參類型都是一樣的占位符,但是如果我使用帶有形參的方法作為委托的參數的話是無法進行類型推斷的,然后使用無參數的方法作為委托參數是完全沒有問題的。然后必須使用Lambda表達式才能做正確的類型推斷,如果直接將帶有參數的某個方法作為委托的參數進行傳遞是無法進行真確的類型推斷,這里我表示很不理解。貼出代碼與大家討論一下這個問題。

我定義兩個方法,這兩個方法沒有什么意義,只是一個有參數,一個沒有參數。

無參數的方法:

View Code
1 public static List<Order> GetOrderList() 
2 { 
3 return new List<Order>(); 
4 }

有參數方法:

View Code
1 public static List<Order> GetOrderListByModel(Order model) 
2 { 
3 return new List<Order>(); 
4 }

Order對象只是一個類型,這里沒有什么特別意義。

兩個帶有Func委托的方法,用來演示泛型的類型推斷:

View Code
1 public static TResult GetModelList<TResult>(Func<TResult> GetFunc) 
2 { 
3 return default(TResult); 
4 } 
5 public static TResult GetModelList<TSource, TResult>(Func<TSource, TResult> GetFunc) 
6 { 
7 return default(TResult); 
8 }

這里的問題是,如果我使用GetOrderList方法作為GetModelList<TResult>(Func<TResult> GetFunc)泛型方法的參數是沒有任何問題的,編輯器能真確的推斷出泛型的類型。但是如果我使用GetOrderListByModel作為GetModelList<TSource, TResult>(Func<TSource, TResult> GetFunc)重載版本的泛型方法時就不能真確的推斷出類型。其實這里的Func中的TResult已經是方法的返回類型,TSource也是方法的參數類型,按道理是完全可以進行類型推斷的。可是我嘗試了很多種方式就是過不起。奇怪的是如果我使用帶有參數和返回類型的Lambda表達式作為GetModelList<TSource, TResult>(Func<TSource, TResult> GetFunc)方法的參數時就能正確的類型推斷。

方法調用的圖例:

在圖的第二行代碼中,就是使用才有參數的方法調用GetModelList方法,無法進行真確的類型推斷。

小結:按照這個分析,似乎對於方法的泛型類型推斷只限於Lambda表達式?如果不是為什么多了參數就無法進行類型推斷?我們先留着這個疑問等待答案吧;

  • 2.4.擴展方法(允許在不修改類型的內部代碼的情況下為類型添加獨立的行為)

擴展方法的本意在於不修改對象內部代碼的情況下對對象進行添加行為。這種方便性大大提高了我們對程序的擴展性,雖這小小的擴展性在代碼上來看不微不足道,但是如果使用巧妙的話將發揮很大的作用。擴展方法對LINQ的支撐非常重要,很多對象原本構建與.NET2.0的框架上,LINQ是.NET3.0的技術,如何在不影響原有的對象情況下對對象進行添加行為很有挑戰。 

那么我們利用擴展方法就可以無縫的嵌入到之前的對象內部。這樣的需求在做框架設計時很常見,最為典型的是我們編寫了一個.NET2.0版本的DLL文件作為客戶端程序使用,那么我們有需要在服務端中對.NET2.0版本中的DLL對象加以控制。比如傳統的WINFORM框架,我們可以將ORM實體作為窗體的控件數據源,讓ORM實體與窗體的控件之間形成自然的映射,包括對賦值、設置值都很方便。但是這樣的實體經過序列化后到達服務層,然后經過檢查進入到BLL層接着進入到DAL層,這個時候ORM框架需要使用該實體作相應的數據庫操作。那么我們如何使用.NET3.0的特性為ORM添加其他的行為呢?如果沒有擴展方法這里就很無賴了。有了擴展方法我們可以將擴展方法構建與.NET3.0DLL中,在添加對.NET2.0DLL的友元引用,再對ORM實體進行擴展。

我們來看一個小例子,看看擴展方法如果使用;

View Code
 1 public class OrderCollection 
 2 { 
 3   public  List<Order> list = new List<Order>(); 
 4 } 
 5 public class Order 
 6 { 
 7     public int Count; 
 8     public string OrderName; 
 9     public string OrderId; 
10 }

這里僅僅是為了演示,比較簡單。我定義了一個Order類和一個OrderCollection類,目前看來OrderCollection沒有任何的方法,下面我們通過添加一個擴展方法來為OrderCollection類添加一寫計算方法,比如匯總、求和之類的。

如何定義擴展方法?

擴展方法必須是靜態類中的靜態方法,我們定義一個OrderCollection類的擴展方法Count。

View Code
1 public static class OrderExtend 
2 { 
3     public static int Count(this OrderCollection OrderCollectionObject) 
4     { 
5         return OrderCollectionObject.list.Count; 
6     } 
7 }

擴展方法的第一個參數必須是this 關鍵開頭然后經跟要擴展的對象類型,然后是擴展對象在運行時的實例對象引用。如果沒有實例對象的引用我想擴展方法也毫無意識。所以這里我們使用Count方法來匯總一共有多少Order對象。通過OrderCollectionObject對象引用我們就可以拿到實例化的OrderCollection對象。

View Code
1 OrderCollection orderCollection = new OrderCollection(); 
2 orderCollection.Count();

還有一個需要大家注意的是,如果我們定義的擴展方法在另外的命名空間里,我們在使用的時候一定要在當前的CS代碼中應用擴展方法所在的命名空間,要不然編輯器是不會去尋找你目前在使用的對象的擴展方法的,切忌。這里還有一點是需要我們注意的,當我們在設計后期可能會被擴展方法使用的對象時需要謹慎的考慮對象成員訪問權限,如果我們將以后可能會被擴展方法使用的對象設計成受保護的或者私有的,那么可能會涉及到無法最大力度的控制。

  • 2.5.匿名類型(由對象初始化器推斷得出的類型,該類型在編譯后自動創建)

匿名類型其實也是比較好理解的,顧名思義匿名類型是沒有類型定義的類型。這種類型是由編輯器自動生成的,僅限於當前上下文使用。廢話少說了,我們還是看例子吧;

View Code
1 var Student1 = new { Name = "王清培", Age = 24, Sex = "", Address = "江蘇淮安" }; 
2 var Student2 = new { Name = "陳玉和", Age = 23, Sex = "", Address = "江蘇鹽城" };

定義匿名類型跟普通的定義類型差不多,只不過在new之后是一對大括號,然后經跟着你需要使用到的屬性名稱和值。

匿名類型的作用域;

匿名類型在使用上是有它先天性缺點的,由於缺乏顯示的類型定義,所以無法在方法之間傳遞匿名類型。要想獲取匿名類型的各屬性值只能通過反射的方式動態的獲取運行時的屬性對象,然后通過屬性對象去獲取到屬性的值。匿名類型在使用的時候才會被創建類型,所以它在運行時存在着完整的對象定義元數據,所以通過反射獲取數據是完全可以理解的。

下面我們使用上面定義的類型來獲取它的各個屬性。

View Code
 1 PrintObjectProperty(Student1, Student2);
 2 
 3 public static void PrintObjectProperty(params object[] varobject) 
 4 { 
 5     foreach (object obj in varobject) 
 6     { 
 7         foreach (System.Reflection.PropertyInfo property in obj.GetType().GetProperties()) 
 8         { 
 9             Console.WriteLine(string.Format("PropertyName:{0},PropertyValue:{1}", 
10                 property.Name, property.GetValue(obj, null))); 
11         } 
12     } 
13 }

圖例:

通過反射的方式我們就可以順利的獲取到匿名類型的屬性成員,然后通過屬性信息在順利的獲取到屬性的值。

  • 2.6.表達式目錄樹(用數據結構表示邏輯代碼)

表達式目錄樹是LINQ中的重中之重,優雅其實就體現在這里。我們從匿名委托到Lambda拉姆達表達式在到現在的目錄樹,我們看到了.NET平台上的語言越來越強大。我們沒有理由不去接受它的美。那么表達式目錄樹到底是啥東西,它的存在是為了解決什么樣的問題又或者是為了什么需求而存在的?

我們上面已經講解過關於Lambda表示式的概念,它是匿名函數的優雅編寫方式。在Lambda表達式里面是關於程序邏輯的代碼,這些代碼經過編譯器編譯后就形成程序的運行時路徑,根本無法作為數據結構在程序中進行操作。比如在Lambda表達式里面我編寫了這樣一段代碼 :(Student Stu)=>Stu.Name=="王清培",那么這段代碼經過編譯器編譯后就變成了大家耳熟能詳的微軟中間語言IL。那么在很多時候我們需要將它的運行特性表現為數據結果,我們需要人為的去解析它,並且轉變為另外一種語言或者調用方式。那么為什么在程序里面需要這樣的多此一舉,不能用字符串的方式表達Lambda表達式等價的表達方式呢?這樣的目的是為了保證強類型的操作,不會導致在編譯時無法檢查出的錯誤。而如果我們使用字符串的方式來表達邏輯的結構,那么我們只能在運行時才能知道它的正確性,這樣的正確性是很脆弱的,不知道在什么樣的情況下會出現問題。所以如果有了強類型的運行時檢查我們就可以放心的使用Lambda這樣的表達式,然后在需要的時候將它解析成各種各樣的邏輯等式。

在.NET3.5框架的System.Linq.Expression命名空間中引入了以Expression抽象類為代表的一群用來表示表達式樹的子對象集。這群對象集目的就是為了在運行時充分的表示邏輯表達式的數據含義,讓我們可以很方便的獲取和解析這中數據結構。為了讓普通的Lambda表達式能被解析成Expression對象集數據結構,必須得借助Expression<T>泛型類型,該類型派生自LambdaExpression,它表示Lambda類型的表達式。通過將Delegate委托類型的對象作為Expression<T>中的類型形參,編輯器會自動的將Lambda表達式轉換成Expression表達式目錄樹數據結構。我們看來例子;

View Code
1 Func<int> Func = () => 10; 
2 Expression<Func<int>> Expression = () => 10;

編輯器對上述兩行代碼各采用了不同的處理方式,請看跟蹤對象狀態。

不使用Expression<T>作為委托類型的包裝的話,該類型將是普通的委托類型。

如果使用了Expression<T>作為委托類型的包裝的話,編譯器將把它解析成繼承自System.Linq.Expression.LambdaExpression類型的對象。一旦變成對象,那么一切就好辦多了,我們可以通過很簡單的方式獲取到Expression內部的數據結構。

表達式目錄樹的對象模型;

上面簡單的介紹了一下表達式目錄樹的用意和基本的原理,那么表達式目錄樹的繼承關系或者說它的對象模型是什么樣子的?我們只有理清了它的整體結構這樣才能方便我們以后對它進行使用和擴展。

下面我們來分析一下它的內部結構。

(Student stu)=>stu.Name=="王清培",我定義了一個Lambda表達式,我們可以視它為一個整體的表達式。什么叫整體的表達式,就是說完全可以用一個表達式對象來表示它,這里就是我們的LambdaExpression對象。表達式目錄樹的本質是用對象來表達代碼的邏輯結構,那么對於一個完整的Lambda表達式我們必須能夠將它完全的拆開才能夠進行分析,那么可以將Lambda表達式拆分成兩部分,然后再分別對上一次拆開的兩部分繼續拆分,這樣遞歸的拆下去就自然而然的形成一顆表達式目錄樹,其實也就是數據結構里面的樹形結構。那么在C#里面我們很容易的構造出一個樹形結構,而且這顆樹充滿着多態。

(Student stu)=>stu.Name="王清培",是一個什么樣子的樹形結構呢?我們來看一下它的運行時樹形結構,然后在展開抽象的繼承圖看一下它是如何構造出來的。

上圖中的第一個對象是Expression<T>泛型對象,通過跟蹤信息可以看出,Expression<T>對象繼承自LambdaExpression對象,而LambdaExpression對象又繼承自Expression抽象類,而在抽象里重寫了ToString方法,所以我們在看到的時候是ToString之后的字符串表示形式。

Lambda表達式對象主要有兩部分組成,從左向右依次是參數和邏輯主題,也就對應着Parameters和Body兩個公開屬性。在Parameters是所有參數的自讀列表,使用的是System.Collection.ObjectModel.ReadOnlyCollection<T>泛型對象來存儲。

這里也許你已經參數疑問,貌似表達式目錄樹的構建真的很完美,每個細節都有指定的對象來表示。不錯,在.NET3.5框架中引入了很多用來表示表達式樹邏輯節點的對象。這些對象都是直接或間接的繼承自Expression抽象類,該類表示抽象的表達式節點。我們都知道表達式節點各種各樣,需要具體化后才能直接使用。所以在基類Expression中只有兩個屬性,一個是public ExpressionType NodeType { get; },表示當前表達式節點的類型,還有另外一個public Type Type { get; },表示當前表達式的靜態類型。何為靜態類型,就是說當沒有變成表達式目錄樹的時候是什么類型,具體點講也就是委托類型。因為在委托類型被Expression<T>泛型包裝后,編譯器是把它自動的編譯成表達式樹的數據結構類型,所以這里需要保存下當前節點的真實類型以備將來使用。

小結:到了這里其實已經把LINQ的一些准備工作講完了,從一系列的語法增強到.NET5.0的類庫的添加,已經為后面的LINQ的到來鋪好了道路。下面的幾個小結將是最精彩的時刻,請不要錯過哦。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM