LINQ 首部曲 : LINQ To Object Part 1 - Using VB.NET 2008

LINQ 首部曲 : LINQ To Object Part 1 - Using VB.NET 2008

/黃忠成

這一篇文章來自於我去年刊登於Run! PC雜誌上的一系列文章,原文是以C#做為語言而撰寫的,鑑於VB.NET 2008LINQ的文章較C#來得少,特別花了時間將此系列文章一一轉換為VB.NET

序曲: LINQ 的架構與程式語言

Microsoft於新一代的.NET Framework 3.5中增加了幾個新功能,其中之一就是LINQ,與其它新功能不同,架構上,LINQ是一個Framework方式呈現,理論上可以使用於任何的.NET Language中,但她的真正威力必須要程式語言配合才能夠完全的發揮,圖1LINQ的架構概觀圖。

[1]

LINQ 首部曲 : LINQ To Object Part 1 - Using VB.NET 2008

<shapetype id="_x0000_t75" stroked="f" filled="f" path="[email protected]@[email protected]@[email protected]@[email protected]@5xe" o:preferrelative="t" o:spt="75" coordsize="21600,21600"><stroke joinstyle="miter"></stroke><formulas><f eqn="if lineDrawn pixelLineWidth 0"></f><f eqn="sum @0 1 0"></f><f eqn="sum 0 0 @1"></f><f eqn="prod @2 1 2"></f><f eqn="prod @3 21600 pixelWidth"></f><f eqn="prod @3 21600 pixelHeight"></f><f eqn="sum @0 0 1"></f><f eqn="prod @6 1 2"></f><f eqn="prod @7 21600 pixelWidth"></f><f eqn="sum @8 21600 0"></f><f eqn="prod @7 21600 pixelHeight"></f><f eqn="sum @10 21600 0"></f></formulas><path o:connecttype="rect" gradientshapeok="t" o:extrusionok="f"></path><lock aspectratio="t" v:ext="edit"></lock></shapetype><shape id="_x0000_i1025" style="WIDTH: 414.75pt; HEIGHT: 216.75pt" o:ole="" type="#_x0000_t75"><imagedata o:title="" src="file:///C:%5CDOCUME~1%5CADMINI~1%5CLOCALS~1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_image001.png"></imagedata></shape>

如圖1所示,LINQ Framework大致分為三大部份,各自因應不同的資料來源,LINQ To Object Framework用來對物件查詢,LINQ To XML Framework用於查詢XML物件,LINQ To ADO.NET Framework又可細分為三個子集:LINQ To DataSet Framework用來對DataTableDataRow等物件做查詢,LINQ To SQL Framework則用於對資料庫的查詢,LINQ To Entity Framework則是與ADO.NET Entity Framework整合。在LINQ Framwork之上的,是程式語言編譯器所提供的LINQ Expression語法支援,如同前面所提及的,LINQ Framework本身是一組與程式語言無關的Framework,藉助於編譯器所提供的LINQ Expression支援,讓設計師能更輕鬆的撰寫LINQ應用程式。舉例來說,在VB.NET 2008中可以用<From xxx In xxx Where xxx == xxx>LINQ Expression語法來取代對LINQ To Object Framework的函式呼叫<xxx.Where(……)>,此處的Where函式是LINQ To Object Framework所提供的,下文會對此有更詳細的介紹。基本上,語言編譯器有義務對於如LINQ To ObjectLINQ To XMLLINQ To ADO.NET提供一致性的LINQ Expression語法規則,這可以讓設計師只學習一種語法,就能應用於不同的語言中。LINQ的出現,代表著程式語言將走向下一個階段,正如其全名『Language Integrated Query』所表現的意義,程式語言將與查詢語言整合,為設計師提供更快速、方便的查詢功能,更甚之!LINQ中的LINQ To SQL功能正試圖整合各資料庫廠商所各自為政的SQL語言,其架構中的LINQ Provider機制,允許設計師為不同的資料庫撰寫Provider,將LINQ的語法轉換成該資料庫所能接受的語法,如圖2所示:

[2]

LINQ 首部曲 : LINQ To Object Part 1 - Using VB.NET 2008

<shape id="_x0000_i1028" style="WIDTH: 414.75pt; HEIGHT: 404.25pt" o:ole="" type="#_x0000_t75"><imagedata o:title="" src="file:///C:%5CDOCUME~1%5CADMINI~1%5CLOCALS~1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_image003.png"></imagedata></shape>

從一個簡單的LINQ程式開始

LINQ架構中分成了三大部份,LINQ To ObjectLINQ TO ADO.NETLINQ TO XML,因此本系列文章也分成了三個階段,在此階段中,筆者將以LINQ To Object Framework為主軸,為讀者們介紹其基本用法,與其它的文章不同,本文同時會嘗試討論LINQ To Object Framework的幕後機制,將LINQ To Object Framework身上所被的簡潔外衣去除,讓讀者們一窺其設計之巧妙之處,首先從一個簡單的LINQ To Object Framework程式開始。

[程式1]

Sub TestSimpleLinq()

Dim list() As String = {"1111", "2222", "3333"}

Dim p = From o In list Select o

For Each s In p

Console.WriteLine(s)

Next

Console.ReadLine()

End Sub

程式碼中,斜體字部份就是VB.NET 2008所提供的LINQ Expression語法,意思是從list這個字串陣列中,取出一個列舉物件(IEnumerable),放到p變數中,此程式執行後的結果如圖3

[3]

LINQ 首部曲 : LINQ To Object Part 1 - Using VB.NET 2008

<shape id="_x0000_i1026" style="WIDTH: 261.75pt; HEIGHT: 85.5pt" o:ole="" type="#_x0000_t75"><imagedata o:title="" src="file:///C:%5CDOCUME~1%5CADMINI~1%5CLOCALS~1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_image005.png"></imagedata></shape>

當然,如果只是要列出list陣列中的所有元素,只要以For Each指令來一一擷取即可,何需大費週章寫下From….的指令!是的!但LINQ To Object Framework的能力自然不止於此,請看程式2

[程式2]

Sub TestConditionLinq()

Dim list() As String = {"1111", "2222", "3333"}

Dim p = From o In list Where o = "2222" Select o

For Each s In p

Console.WriteLine(s)

Next

Console.ReadLine()

End Sub

與程式1不同,程式2中的LINQ Expression中包含了Where語句,這意味著LINQ允許設計師以類SQL語法對陣列做查詢,更確切的說是,LINQ允許設計師以類SQL語法對實作了IEnumerableIQueryable介面的物件做查詢(LINQ TO SQL時會談到IQueryable介面)。如果你和筆者一樣,常常與SQL為伍,相信你很快會寫下如程式3的程式碼,來測試LINQ Expressionwhere語句。

[程式3]

Dim p = From o In list Where o Like "1*" Select o

這段程式碼在VB.NET 2008是可以正常執行的,但在C#中卻沒有Like這個關鍵字,拜VB.NET 2008編譯器之福,在LIKE語句上,VB.NET設計師比C#設計師更直覺。程式3的寫法,透過VB.NET 2008編譯器的展開,會形成程式4的模樣。

[程式4]

Dim p = From o In list Where

Microsoft.VisualBasic.CompilerServices.LikeOperator.LikeString(o, "1*", _

CompareMethod.Binary) Select o

這段程式結合了VB.NET 2008 Runtime所提供的LikeOperator物件之LikeString函式來做查詢,這意味著LINQ To Object Framework不僅是程式語言所提供的查詢語法,其與程式語言整合的程度更是異常緊密。雖然LINQ Expression還有許多如Group ByOrder ByJoin等能力,但目前筆者不想耗費太多時間在其語法規則上,將其留待後文再討論,目前先將焦點放在LINQ To Object Framework是如何達到這些效果的課題上。

這是如何辦到的?

VB.NET 2008.NET Framework 3.5在目前是維持在以.NET Framework 2.0為基礎所開發的子集,這代表著VB.NET 2008所提供的LINQ Expression不會一成不變的出現在MSIL 2.0中,VB.NET 2008一定會把程式轉換成MSIL 2.0所規範的IL Code,這裡沒有From xxxx In yyyLINQ Expression,所以如果想知道LINQ To Object Framework如何完成這神奇任務的,第一步就是要知道VB.NET 2008把我們的程式變成什麼樣子,這有許多工具可以達到,首選的工具自然是陪伴.NET設計師多年的Relfector

[程式5]

Public Shared Sub TestLikeConditionLinq()

Dim list As String() = New String() { "1111", "2222", "3333" }

Dim p As IEnumerable(Of String) = list.Where(Of String)( _

New Func(Of String, Boolean)(AddressOf Module1._Lambda$__4)).Select( _

Of String, String)(New Func(Of String, String)(AddressOf Module1._Lambda$__5))

Dim s As String

For Each s In p

Console.WriteLine(s)

Next

Console.ReadLine

End Sub

咦!何時string陣列有名為Where的成員函式了?不是的,這是VB.NET 2008的新特色之一:Extension Method(擴充方法),當於Reflector所反組譯的視窗中點選了Where函式後,Reflector會帶我們到System.Linq.Enumerable類別中定義的Where靜態成員函式中。看來了解LINQ To Object Framework前,得先弄清楚VB.NET 2008所提供的幾個新功能了。

了解LINQ前的準備: VB.NET 2008 New Feature

VB.NET 2008提供了許多新功能,其中與LINQ緊密相關的有三個:Extension Method(擴充方法)Lambda ExpressionAnonymous Type(匿名型別)

VB.NET 2008 Extension Method

Extension Method允許設計師宣告一個函式或程序於Module中,她將會被視為指定型別的成員函式(這只是看起來像是,事實上她仍然是其所在Module的成員函式),前例中LINQ To Object FrameworkWhere函式其實是位於System.Linq.Enumerable這個靜態類別中。在VB.NET 2008中可以直接用string().Where的函式呼叫語法來呼叫此函式,編譯器會將此展開成對System.Linq.Enumerable.Where(IEnumerable…)函式的呼叫(string陣列是實作了IEnumerable介面的物件,所以可以傳入Where函式中)。為了讓讀者們更了解Extension Method,筆者寫了個小程式來演示Extension Method的用法。

[程式9]

Imports System.Runtime.CompilerServices

Module MyExtensionMethod

<Extension()> _

Public Function WordCount(ByVal v As String) As Integer

Return v.Length

End Function

End Module

Module Module1

Sub Main()

Dim s As String = "TEST"

s.WordCount()

End Sub

End Module

Extension Method必須宣告在一個Module中,而且必須要在Extension的程序或函式上標上Extension這個Attribute,其第一個參數即是欲Extension的變數型別。

(PS: C#無此限制,Extension Method可宣告於任何類別中,VB.NET則限制於Module)

Extension Method Generics assumption

Extension Method遇上generics時, 情況會顯得很有趣,請看程式10的例子。

[程式10]

Imports System.Runtime.CompilerServices

Module Module1

Sub Main()

Dim s As String = "TEST"

s.Test()

Console.ReadLine()

End Sub

<Extension()> _

Public Sub Test(Of T)(ByVal val As T)

Console.WriteLine(val.ToString())

End Sub

End Module

Public Class GenericTypeResolverTest

Private FValue As String

Public Property Value() As String

Get

Return FValue

End Get

Set(ByVal value As String)

FValue = value

End Set

End Property

Public Overloads Overrides Function ToString() As String

Return Value.ToString()

End Function

End Class

請注意程式中Test這個Extension Method的定義,她是一個generic method,一般來說,在呼叫generic method時,我們必需指定type parameter,譬如程式11片段。

[程式11]

Test<string>()

但此處卻在未提供type parameter的情況下呼叫此Extension Method,而VB.NET編譯器也接受了這種寫法,這是為何呢?答案就是Extension Method會進行一種type parameter assumption的動作,也就是由呼叫端假設被呼叫端的type parameter,本例中,呼叫Test函式時是透過GenericTypeResolverTest型別的物件,因此VB.NET 編譯器便假設呼叫Test函式時的type parameterGenericTypeResolverTest型別。基本上,這樣的type parameter assumption可以簡化呼叫Extension Method的動作,也不難理解。但LINQ To Object Framework所應用的技巧就不太好理解了,請看另一個例子:程式12

[程式12]

Imports System.Runtime.CompilerServices

Imports System.Collections

Module Module1

Sub Main()

Dim s() As GenericTypeResolverTest = { _

New GenericTypeResolverTest() With {.Value = "TEST1"}, _

New GenericTypeResolverTest() With {.Value = "TEST2"}, _

New GenericTypeResolverTest() With {.Value = "TEST3"} _

}

s.Test()

Console.ReadLine()

End Sub

<Extension()> _

Public Sub Test(Of T)(ByVal val As IEnumerable(Of T))

Console.WriteLine(val.ToString())

End Sub

End Module

Public Class GenericTypeResolverTest

Private FValue As String

Public Property Value() As String

Get

Return FValue

End Get

Set(ByVal value As String)

FValue = value

End Set

End Property

Public Overloads Overrides Function ToString() As String

Return Value.ToString()

End Function

End Class

這個例子中,呼叫Test函式時是透過一個GenericResolverTest陣列,依據generic type assumption的規則,我們很直覺的設想T應該是被推算為GenericResolverTest陣列,但事實並非如此,請注意Extension Method的宣告,其針對的是IEnumerable(Of T)型態,因此此時的type parameter會變成IEnumerable(Of T),而VB.NET中的陣列實作了IEnumerable介面,以本例來說,呼叫Test函式時,呼叫端的型別被視為是IEnumerable(Of GenericResolverTest),也就是說Extension Method中的T將被替換為GenericResolverTest,最後結果如程式13

[程式13]

void Test2(Of T) (ByVal obj Of IEnumerable(Of T))