LINQ 首部曲 : LINQ To Object Part 1

<iframe align="top" marginwidth="0" marginheight="0" src="http://www.zealware.com/46860.html" frameborder="0" width="468" scrolling="no" height="60"></iframe>
LINQ 首部曲 : LINQ To ObjectPart 1
文/黃忠成
序曲: LINQ 的架構與程式語言
Microsoft 於新一代的.NET Framework 3.5中增加了幾個新功能,其中之一就是LINQ,與其它新功能不同,架構上,LINQ是一個Framework方式呈現,理論上可以使用於任何的.NET Language中,但她的真正威力必須要程式語言配合才能夠完全的發揮,圖1為LINQ的架構概觀圖。
[ 圖1]
LINQ 首部曲 : LINQ To Object Part 1_第1张图片
如圖1所示,LINQ Framework大致分為三大部份,各自因應不同的資料來源,LINQ To Object Framework用來對物件查詢,LINQ To XML Framework用於查詢XML物件,LINQ To ADO.NET Framework又可細分為三個子集:LINQ To DataSet Framework用來對DataTable、DataRow等物件做查詢,LINQ To SQL Framework則用於對資料庫的查詢,LINQ To Entity Framework則是與ADO.NET Entity Framework整合。在LINQ Framwork之上的,是程式語言編譯器所提供的LINQ Expression語法支援,如同前面所提及的,LINQ Framework本身是一組與程式語言無關的Framework,藉助於編譯器所提供的LINQ Expression支援,讓設計師能更輕鬆的撰寫LINQ應用程式。舉例來說,在C#中可以用<from xxx in where>的LINQ Expression語法來取代對LINQ To Object Framework的函式呼叫<xxx.where>,此處的Where函式是LINQ To Object Framework所提供的,下文會對此有更詳細的介紹。基本上,語言編譯器有義務對於如LINQ To Object、LINQ To XML、LINQ To ADO.NET提供一致性的LINQ Expression語法規則,這可以讓設計師只學習一種語法,就能應用於不同的語言中。LINQ的出現,代表著程式語言將走向下一個階段,正如其全名『Language Integrated Query』所表現的意義,程式語言將與查詢語言整合,為設計師提供更快速、方便的查詢功能,更甚之!LINQ中的LINQ To SQL功能正試圖整合各資料庫廠商所各自為政的SQL語言,其架構中的LINQ Provider機制,允許設計師為不同的資料庫撰寫Provider,將LINQ的語法轉換成該資料庫所能接受的語法,如圖2所示:</xxx.where></from>
[ 圖2]
LINQ 首部曲 : LINQ To Object Part 1_第2张图片
從一個簡單的LINQ程式開始
LINQ 架構中分成了三大部份,LINQ To Object、LINQ TO ADO.NET、LINQ TO XML,因此本系列文章也分成了三個階段,在此階段中,筆者將以LINQ To Object Framework為主軸,為讀者們介紹其基本用法,與其它的文章不同,本文同時會嘗試討論LINQ To Object Framework的幕後機制,將LINQ To Object Framework身上所被的簡潔外衣去除,讓讀者們一窺其設計之巧妙之處,首先從一個簡單的LINQ To Object Framework程式開始。
[ 程式1]
private static void TestSimpleLinq() {
string[] list = new string[] { "1111", "2222", "3333" };
var p = from o in list select o;
foreach (var s in p)
Console.WriteLine(s);
}
程式碼中,斜體字部份就是C#所提供的LINQ Expression語法,意思是從list這個字串陣列中,取出一個列舉物件(IEnumerable),放到p變數中,讀者們應該已發覺到,p變數是以var方式宣告的,var是C# 3.0的新關鍵字,意指其型態是由右方運算式所指定,本文後面會詳述其用法及限制,在此處,請將她視為是由編譯器依據右方運算式的傳回值所決議的型別。此程式執行後的結果如圖3。
[ 圖3]
LINQ 首部曲 : LINQ To Object Part 1_第3张图片
當然,如果只是要列出list陣列中的所有元素,只要以foreach指令來一一擷取即可,何需大費週章寫下from….的指令!是的!但LINQ To Object Framework的能力自然不止於此,請看程式2。
[ 程式2]
private static void TestConditionLinq() {
string[] list = new string[] { "1111", "2222", "3333" };
var p = from o in list where o == "2222" select o;
foreach (var s in p)
Console.WriteLine(s);
}
與程式1不同,程式2中的LINQ Expression中包含了where語句,這意味著LINQ允許設計師以類SQL語法對陣列做查詢,更確切的說是,LINQ允許設計師以類SQL語法對實作了IEnumerable或IQueryable介面的物件做查詢(於LINQ TO SQL時會談到IQueryable介面)。如果你和筆者一樣,常常與SQL為伍,相信你很快會寫下如程式3的程式碼,來測試LINQ Expression的where語句。
[ 程式3]
var p = from o in list where o like "1%" select o;
很不幸的,like條件式並不存在於LINQ Expression的語法規則中,相對的,LINQ To Object Framework允許設計師以函式呼叫的方式來達到類似的結果。
[ 程式4]
var p = from o in list where o.Contains("2") select o;
這段程式結合了string物件的Contains函式來做查詢,這意味著LINQ To Object Framework不僅是程式語言所提供的查詢語法,其與程式語言整合的程度更是異常緊密。雖然LINQ Expression還有許多如Grouping、Orderby、Join等能力,但目前筆者不想耗費太多時間在其語法規則上,將其留待後文再討論,目前先將焦點放在LINQ To Object Framework是如何達到這些效果的課題上。
這是如何辦到的?
C# 3.0 及.NET Framework 3.5在目前是維持在以.NET Framework 2.0為基礎所開發的子集,這代表著C# 3.0所提供的LINQ Expression不會一成不變的出現在MSIL 2.0中,C# 3.0一定會把程式轉換成MSIL 2.0所規範的IL Code,這裡沒有from xxxx in yyy的LINQ Expression,所以如果想知道LINQ To Object Framework如何完成這神奇任務的,第一步就是要知道C# 3.0把我們的程式變成什麼樣子,這有許多工具可以達到,首選的工具自然是陪伴.NET設計師多年的Relfector。
[ 程式5]
private static void TestConditionLinq() {
IEnumerable<string> p = new string[] { "1111", "2222", "3333" }.Where<string>(delegate (string o) {</string></string>
return o == "2222";
});
foreach (string s in p) {
Console.WriteLine(s);
}
Console.ReadLine();
}
咦!何時string陣列有名為Where的成員函式了?不是的,這是C# 3.0的新特色之一:Extension Method,當於Reflector所反組譯的視窗中點選了Where函式後,Reflector會帶我們到System.Linq.Enumerable類別中定義的Where靜態成員函式中。看來了解LINQ To Object Framework前,得先弄清楚C# 3.0所提供的幾個新功能了。
了解LINQ前的準備: C# 3.0 New Feature
C# 3.0 提供了許多新功能,其中與LINQ緊密相關的有四個:Implicit Local Variable、Extension Method、Lamba Expression、Anonymous Type。
C# 3.0 Implicit Local Variable
Implicit Local Variable 就是先前所使用的var型態宣告,她允許設計師指定某個變數為var型態,其真正型態將由編譯器從右方運算式推算而來,程式7演示了Implicit Local Variable的用法。
[ 程式7]
static void TestImplicitLocalVariable(){
var vint = 10;
var vstring = "TEST";
var vint64 = 9029349442;
var vdouble = 9.234;
Console.WriteLine("{0},{1},{2},{3}", vint.GetType().ToString(),
vstring.GetType().ToString(),
vint64.GetType().ToString(),
vdouble.GetType().ToString());
Console.ReadLine();
}
var 是由右方運算式所賦與型別,所以右方運算式也可以是一個函式,規則上var僅能用於Local Variable(區域變數)的宣告,無法使用於class variable、function parameter等其它地方,也就是說程式8的用法皆不符合規則。
[ 程式8]
class Program{
private static var t = 15;
static void TestImplicitLocalVariable(var t) {}
}
C# 3.0 Extension Method
Extension Method 允許設計師宣告一個靜態函式,此函式必須存在於一個靜態類別中,在C# 3.0中,她將會被視為指定型別的靜態成員函式(這只是看起來像是,事實上她仍然是其所在類別的靜態成員函式),前例中LINQ To Object Framework的Where函式其實是位於System.Linq.Enumerable這個靜態類別中。在C# 3.0中可以直接用string[].Where的函式呼叫語法來呼叫此函式,編譯器會將此展開成對System.Linq.Enumerable.Where(IEnumerable…)函式的呼叫(string陣列是實作了IEnumerable介面的物件,所以可以傳入Where函式中)。為了讓讀者們更了解Extension Method,筆者寫了個小程式來演示Extension Method的用法。
[ 程式9]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyExtensionMethod {
public static class TestExtensionMethod {
//extnsion methods must be defined in non generic static class.
public static int WordCount(this string v) {
return v.Length;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MyExtensionMethod;
namespace CSharpNew {
public static class TestExtensionConsumer {
public static void TestExtension() {
string s = "TEST";
Console.WriteLine(s.WordCount());
Console.ReadLine();
}
}
}
Extension Method 必須宣告在一個非泛型的靜態類別中,而且必須要是靜態函式,其參數的第一個就是欲Extension的型別(Type),並且要以this語句做為識別字。使用時,當Extension Method所在的namespace與使用端的namespace不同時,需以using來引入其namespace。
Extension Method Generics assumption
Extension Method 遇上 generics 時, 情況會顯得很有趣,請看程式 10 的例子。
[ 程式 10]
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace FirstLinq {
public class ExtensionMethodAndGenerics {
private static void GenericTypeResovlerTest1() {
GenericTypeResolverTest v = new GenericTypeResolverTest();
v.Value = "TEST2";
v.Test();
}
}
//generic implicit type resolver.
public class GenericTypeResolverTest {
public string Value { get; set; }
public override string ToString() {
return Value.ToString();
}
}
public static class GenericTypeResolverMethodTest {
public static void Test<t>(this T obj)</t> {
Console.WriteLine(obj.ToString());
}
}
}
請注意程式中 Test 這個 Extension Method 的定義,她是一個 generic method ,一般來說,在呼叫 generic method 時,我們必需指定 type parameter ,譬如程式 11 片段。
[ 程式 11]
Test<string>()</string>
但此處卻在未提供 type parameter 的情況下呼叫此 Extension Method ,而 C# 編譯器也接受了這種寫法,這是為何呢?答案就是 Extension Method 會進行一種 type parameter assumptio n 的動作,也就是由呼叫端假設被呼叫端的type parameter,本例中,呼叫Test函式時是透過GenericTypeResolverTest型別的物件,因此C# 編譯器便假設呼叫Test函式時的type parameter為GenericTypeResolverTest型別。基本上,這樣的type parameter assumption可以簡化呼叫Extension Method的動作,也不難理解。但LINQ To Object Framework所應用的技巧就不太好理解了,請看另一個例子:程式12。
[ 程式12]
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace FirstLinq{
public class ExtensionMethodAndGenerics {
private static void GenericTypeResovlerTest1() {
GenericTypeResolverTest[] v2 = new GenericTypeResolverTest[]{
new GenericTypeResolverTest(),
new GenericTypeResolverTest()};
v2.Test2();
}
}
//generic implicit type resolver.
public class GenericTypeResolverTest {
public string Value { get; set; }
public override string ToString() {
return Value.ToString();
}
}
public static class GenericTypeResolverMethodTest {
public static void Test2<t>(this IEnumerable<t> obj)</t></t> {
Console.WriteLine(obj.ToString());
}
}
}
這個例子中,呼叫 Test2 函式時是透過一個 GenericResolverTest 陣列,依據 generic type assumption 的規則,我們很直覺的設想 T 應該是被推算為 GenericResolverTest 陣列,但事實並非如此,請注意 Extension Method 的宣告,其針對的是 IEnumerable<t></t> 型態,因此此時的 type parameter 會變成 IEnumerable<t></t> ,而 C# 中的陣列實作了 IEnumerable 介面,以本例來說,呼叫 Test2 函式時,呼叫端的型別被視為是 IEnumerable<genericresolvertest></genericresolvertest> ,也就是說 Extension Method 中的 T 將被替換為 GenericResolverTes t ,最後結果如程式13。
[ 程式13]
void Test2<t> (IEnumerable<t> obj)</t></t>
void Test2<genericresolvertest>(IEnumerable<genericresolvertest> obj)</genericresolvertest></genericresolvertest>
C# 3.0 Lamba Expression
Lamba Expression 並未出現在Reflector所反組譯的程式碼中,事實上!她是隱含性的存在,Lamba Expression用來簡化C#中指定anonymous delegate的程式碼,程式14的anonymous delegate轉成Lamba Expression後就成為了程式15所示。
[ 程式14]
IEnumerable<string> p = new string[] { "1111", "2222", "3333" }.Where<string>(delegate (string o) {</string></string>
return o == "2222";
});
[ 程式15]
var p = new string[] { "1111", "2222", "3333" }.Where<string>(<strong>l =&gt; l == "2222"</strong>);</string>
很明顯的,Lamba Expression確實簡化了程式碼(少打了許多字不是?),不過老實說,筆者初次看到Lamba Expression時,的確對其語法很不習慣,直到筆者寫下了程式16的Lamba Expression對於Lamba Expression的不適感才稍減許多。
[ 程式16]
…………
namespace CSharpNew {
publicclass TestLamba {
public delegate int Sum(int x, int y);
publicvoid Test2() {
//lamba expression can be more complex.
Sum sFunc = (x, y) => {
var ret = x + y;
DateTime d = DateTime.Now;
Console.WriteLine("sum time is {0}",d.ToShortDateString());
return ret;
};
Console.WriteLine(sFunc(15, 20));
Console.ReadLine();
}
}
}
如你所見,Lamba Expression是簡化了anonymous delegate的宣告,以較簡潔的語法完成,針對單行式的程式碼,Lamba Expression就連{}及return部份都簡化掉了。
Anonymous Type
另一個C# 3.0與LINQ相關的特色就是Anonymous Type,也就是匿名型別,簡略的說,C# 3.0允許設計師以一個簡潔的語法來建立一個類別,如程式17。
[ 程式17]
var p1 = new[]{new {Name = "code6421", Address = "Taipen", Title = "Manager"},
new {Name = "tom", Address = "Taipen", Title = "Manager"},
new {Name = "jeffray", Address = "NY", Title = "Programmer"}};
此例中,編譯器將會自動為我們建立一個擁有Name、Address、Title三個public field的類別,並按照語法賦與其值,請注意!此例中僅會建立一個匿名類別,而非三個!這意味著編譯器在處理匿名類別時,會先行判斷目前所要建立的類別是否已經存在了,若已存在則直接取用,而比對的方式就是語法中所指定的public field數目及名稱,這是效率及程式大小的考量。規格上Anonymous Type中僅允許宣告public field,其它如method、static field等都不允許出現。
PS: ( 在筆者探索LINQ Framework時,曾發生一個小插曲,讓筆者不得不懷疑在C# 3.0的內部版本中,曾經出現允許宣告成員函式的Anonymous Type設計。)
再訪LINQ To Object Framework
OK ,現在可以確定一件事,前面所看到的System.Linq.Enumerable類別就是LINQ To Object Framework的一部份,LINQ To Object Framework是以泛型為本、Extension Method為輔、並用Lamba Expression簡化後的產物,再加上程式語言如C# 3.0、VB.NET 3.0的幫助,才變成了現在所看到的簡潔語法。
以Extension Method為起點
在此節中,我們先將腳步停留在編譯器與LINQ To Object Framework的結合階段,筆者有個小程式要展現給讀者們。
[ 程式18]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace FirstLinqHack {
class TestHacking {
public static void HackLinq() {
Persons<person> list = new Persons<person>();</person></person>
list.Add(new Person { Name = "Code6421", Age = 18, Address = "Taipen" });
var p1 = from o in list select o;
Console.WriteLine(p1[0].Name);
}
}
public sealed class Person {
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
}
public static class PersonsExtension {
public static Persons<tresult> Select<tsource tresult>(this Persons<tsource> source, </tsource></tsource></tresult>
Func<tsource tresult> selector)</tsource>
{
return null;
}
}
public class Persons<t> {</t>
private List<t> _list = new List<t>();</t></t>
public T this[int index] {
get
{
return _list[index];
}
}
public void Add(T item) {
_list.Add(item);
}
}
}
static void Main(string[] args) {
FirstLinqHack.TestHacking.HackLinq();
}
將中斷點設在PersonsExtension.Select函式中,執行後會發現程式會停留在PersonsExtension.Select函式中,為何會如此?很簡單,C# 3.0只是單純的把LINQ Expression轉成object.Select(),基於Extension Method的優先權規則,以Persons<t>為參數的Select函式會被優先考慮,此處並無此函式,因此次要考慮的是以Persons<t>為參數的Extension Method:Select函式,所以控制權自然回到我們手中了。</t></t>
(PS:LINQ TO SQL 對延伸LINQ的功能有更完善的架構,本節只是要驗證LINQ To Object Framework時,與編譯器間的關聯。)
效能的課題:LINQ To Object時的傳回值
從前面的Select、Where等Extension Method的宣告來看,LINQ To Object Framework所提供的函式傳回值多是實作了IEnumerable<t>介面的物件,圖4展現出當對字串陣列使用from xxx in xxx where xxx的LINQ Expression後的運作流程。</t>
[ 圖4]
LINQ 首部曲 : LINQ To Object Part 1_第4张图片
透過編譯器的轉換,LINQ Expression會變成string[].Where的函式呼叫,System.Linq.Enumerable.Where函式會建立一個WhereIterator物件,並於建立時將由編譯器轉換所建立出來的deleage(如where n = “2222”,會變成l => l == "2222",意指建立一個delegate,大概內容是bool generateFunc(string l) { return l == “2222”})及Source Enumerable Object,也就是string陣列傳入,當設計師操作此WhereIterator物件時,例如呼叫其MoveNext函式(foreach會呼叫此函式),WhereIterator將會逐一由Source Enumerable取出其內的元素,然後呼叫於建立WhereIterator時所傳入的delegate函式來決定此元素是否符合條件。了解流程後,就大略可以得知LINQ To Object Framework的效能了,這跟用foreach將元素一一比對放入另一個陣列中的效能相差無幾,只是LINQ Expression比這種做法簡潔多了。
(PS: 與IQueryable結合後的LINQ To ADO.NET Framework,效能就是LINQ Provider的課題了)
後記
下次筆者將針對LINQ Expression的語法做詳細的介紹,各位讀者們下次見了。


Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1875300


你可能感兴趣的:(sql,C++,c,C#,LINQ)