文/黃忠成
序曲: LINQ 的架構與程式語言 Microsoft於新一代的.NET Framework 3.5中增加了幾個新功能,其中之一就是LINQ,與其它新功能不同,架構上,LINQ是一個Framework方式呈現,理論上可以使用於任何的.Net Language中,但她的真正威力必須要程式語言配合才能夠完全的發揮,圖1為LINQ的架構概觀圖。
[圖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 xxx where xxx == xxx>的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所示:
="FONT-SIZE: 11pt">圖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;
n>
foreach (var s in p)
Console.WriteLine(s);
}
程式碼中,斜體字部份就是C#所提供的LINQ Expression語法,意思是從list這個字串陣列中,取出一個列舉物件(IEnumerable),放到p變數中,讀者們應該已發覺到,p變數是以var方式宣告的,var是C# 3.0的新關鍵字,意指其型態是由右方運算式所指定,本文後面會詳述其用法及限制,在此處,請將她視為是由編譯器依據右方運算式的傳回值所決議的型別。此程式執行後的結果如圖3。
[圖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]
lid; WIDTH: 418.1pt; PADDING-TOP: 0cm; BORDER-BOTTOM: black 1pt solid" valign="top" width="557">
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) {
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]
cm; BORDER-LEFT: black 1pt solid; WIDTH: 418.1pt; PADDING-TOP: 0cm; BORDER-BOTTOM: black 1pt solid" valign="top" width="557">
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();
E: 9pt"> }
}
}
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;
: 9pt">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) {
Console.WriteLine(obj.ToString());
t"> }
}
}
請注意程式中Test這個Extension Method的定義,她是一個generic method,一般來說,在呼叫generic method時,我們必需指定type parameter,譬如程式11片段。
[程式11]
Test<string>()
但此處卻在未提供type parameter的情況下呼叫此Extension Method,而C# 編譯器也接受了這種寫法,這是為何呢?答案就是Extension Method會進行一種type parameter assumption的動作,也就是由呼叫端假設被呼叫端的 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) {
Console.WriteLine(obj.ToString());
}
}
}
這個例子中,呼叫Test2函式時是透過一個GenericResolverTest陣列,
依據generic type assumption的規則,我們很直覺的設想T應該是被推算為GenericResolverTest陣列,但事實並非如此,請注意Extension Method的宣告,其針對的是IEnumerable<T>型態,因此此時的type parameter會變成IEnumerable<T>,而C#中的陣列實作了IEnumerable介面,以本例來說,呼叫Test2函式時,呼叫端的型別被視為是IEnumerable<GenericResolverTest>,也就是說Extension Method中的T將被替換為GenericResolverTest,最後結果如程式13。
[程式13]
void Test2<T> (IEnumerable<T> obj)
void Test2<GenericResolverTest>(IEnumerable<GenericResolverTest> obj)
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) {
return o == "2222";
});
[程式15]
var p = new string[] { "1111", "2222",
"3333" }.Where<string>(
l => l == "2222");
很明顯的,Lamba Expression確實簡化了程式碼(少打了許多字不是?),不過老實說,筆者初次看到Lamba Expression時,的確對其語法很不習慣,直到筆者寫下了程式16的Lamba Expression對於Lamba Expression的不適感才稍減許多。
[程式16]
…………
namespace CSharpNew {
public class TestLamba {
public delegate int Sum(int x, int y);
public void 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]
ign="top" width="557">
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]
none; BORDER-BOTTOM: medium none; BORDER-COLLAPSE: collapse" cellspacing="0" cellpadding="0" border="1">
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>();
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,
Func<TSource, TResult> selector)
{
return null;
}
}
public class Persons<T> {
private List<T> _list = new List<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函式,所以控制權自然回到我們手中了。
(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後的運作流程。
[圖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的語法做詳細的介紹,各位讀者們下次見了。