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

  • 12675
  • 0
  • LINQ
  • 2009-02-05

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

 

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中,但她的真正威力必須要程式語言配合才能夠完全的發揮,圖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應用程式。舉例來說,在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 Object、LINQ To XML、LINQ To ADO.NET提供一致性的LINQ Expression語法規則,這可以讓設計師只學習一種語法,就能應用於不同的語言中。LINQ的出現,代表著程式語言將走向下一個階段,正如其全名『Language Integrated Query』所表現的意義,程式語言將與查詢語言整合,為設計師提供更快速、方便的查詢功能,更甚之!LINQ中的LINQ To SQL功能正試圖整合各資料庫廠商所各自為政的SQL語言,其架構中的LINQ Provider機制,允許設計師為不同的資料庫撰寫Provider,將LINQ的語法轉換成該資料庫所能接受的語法,如圖2所示:
[圖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]
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]
當然,如果只是要列出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語法對實作了IEnumerable或IQueryable介面的物件做查詢(於LINQ TO SQL時會談到IQueryable介面)。如果你和筆者一樣,常常與SQL為伍,相信你很快會寫下如程式3的程式碼,來測試LINQ Expression的where語句。
[程式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 By、Order By、Join等能力,但目前筆者不想耗費太多時間在其語法規則上,將其留待後文再討論,目前先將焦點放在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 yyy的LINQ 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 Expression、Anonymous Type(匿名型別)。
 
VB.NET 2008  Extension Method
 
 Extension Method允許設計師宣告一個函式或程序於Module中,她將會被視為指定型別的成員函式(這只是看起來像是,事實上她仍然是其所在Module的成員函式),前例中LINQ To Object Framework的Where函式其實是位於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 parameter為GenericTypeResolverTest型別。基本上,這樣的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))
void Test2(Of GenericResolverTest)(IEnumerable(Of GenericResolverTest) obj)
 
VB.NET 2008  Lambda Expression
 
 Lambda Expression並未出現在Reflector所反組譯的程式碼中,事實上!她是隱含性的存在,Lambda Expression用來簡化VB.NET中delegate的程式碼,在VB.NET 2005中,並不存在如C#的anonymous delegate宣告語法,因此我們必須寫下程式14的程式碼,透過Lambda Expression,在VB.NET 2008我們可以寫成程式15。
[程式14]
Module Module1
 
 
    Sub Main()
        Dim p() As String = {"1111", "2222", "3333"}
        Dim result = p.Where(AddressOf Compare)
        Console.WriteLine(result.First())
        Console.ReadLine()
    End Sub
 
    Function Compare(ByVal o As String) As Boolean
        Return o = "2222"
    End Function
End Module
[程式15]
Module Module1
    Sub Main()
        Dim p() As String = {"1111", "2222", "3333"}
        Dim result = p.Where(Function(o) o = "2222")
        Console.WriteLine(result.First())
        Console.ReadLine()
    End Sub
End Module
很明顯的,Lambda Expression確實簡化了程式碼(少打了許多字不是?),不過老實說,筆者初次看到Lambda Expression時,的確對其語法很不習慣,直到筆者寫下了程式16的Lambda Expression,對於Lambda Expression的不適感才稍減許多。
[程式16]
Module Module1
 
    Public Delegate Function Sum(ByVal x As Integer, ByVal y As Integer) As Integer
 
    Sub Main()
        Dim dg As Sum = Function(x, y) x + y
        Console.WriteLine(dg(5, 15))
        Console.ReadLine()
    End Sub
 
End Module
如你所見,Lambda Expression簡化了delegate的宣告,以較簡潔的語法處理單行式delegate之程式碼。
(PS: C# 可以於Lambda Expression中寫下多行的程式碼,而VB.NET 2008不支援此種寫法,僅能處理單行程式碼,當然,呼叫函式之類的也能寫在Lambda Expression中,唯一限制是單行)
 
Anonymous Type
 
 另一個VB.NET 2008與LINQ相關的特色就是Anonymous Type,也就是匿名型別,簡略的說,VB.NET 2008允許設計師以一個簡潔的語法來建立一個類別,如程式17。
[程式17]
Dim p() = {New With {.Name = "code6421", .Age = 18}, _
                    New With {.Name = "code6421", .Age = 18}, _
                   New With {.Name = "code6421", .Age = 18}}
此例中,編譯器將會自動為我們建立一個擁有Name、Age三個公開屬性的類別,並按照語法賦與其值,請注意 !此例中僅會建立一個匿名類別,而非三個!這意味著編譯器在處理匿名類別時,會先行判斷目前所要建立的類別是否已經存在了,若已存在則直接取用,而比對的方式就是語法中所指定的public field數目及名稱,這是效率及程式大小的考量。規格上Anonymous Type中僅允許宣告公開屬性,其它如函式、成員變數等都不允許出現。
 
再訪LINQ To Object Framework
 
 OK,現在可以確定一件事,前面所看到的System.Linq.Enumerable類別就是LINQ To Object Framework的一部份,LINQ To Object Framework是以泛型為本、Extension Method為輔、並用Lambda Expression簡化後的產物,再加上程式語言如C# 3.0、VB.NET 2008的幫助,才變成了現在所看到的簡潔語法。
 
以Extension Method為起點
 
 在此節中,我們先將腳步停留在編譯器與LINQ To Object Framework的結合階段,筆者有個小程式要展現給讀者們。
[程式18]
Imports System.Runtime.CompilerServices
 
Module Module1
 
    <Extension()> _
    Function [Select](Of TSource, TResult)(ByVal source As Persons(Of TSource), _
             ByVal selector As Func(Of TSource, TResult)) As Persons(Of TResult)
        Return Nothing
    End Function
 
 
    Sub Main()
        Dim p As New Persons(Of Person)()
        p.Add(New Person() With {.Name = "code6421", .Age = 18, .Address = "Taipai"})
        Dim result = From s1 In p Select s1
        Console.ReadLine()
    End Sub
 
End Module
 
 
Public NotInheritable Class Person
    Private FName As String
    Private FAge As Integer
    Private FAddress As String
 
    Public Property Name() As String
        Get
            Return FName
        End Get
        Set(ByVal value As String)
            FName = value
        End Set
    End Property
    Public Property Age() As Integer
        Get
            Return FAge
        End Get
        Set(ByVal value As Integer)
            FAge = value
        End Set
    End Property
    Public Property Address() As String
        Get
            Return FAddress
        End Get
        Set(ByVal value As String)
            FAddress = value
        End Set
    End Property
End Class
 
Public Class Persons(Of T)
    Private _list As New List(Of T)()
    Default Public ReadOnly Property Item(ByVal index As Integer) As T
        Get
            Return _list(index)
        End Get
    End Property
 
    Public Sub Add(ByVal item As T)
        _list.Add(item)
    End Sub
End Class
將中斷點設在Select函式中,執行後會發現程式會停留在Select函式中,為何會如此?很簡單,VB.NET 2008只是單純的把LINQ Expression轉成object.Select(),基於Extension Method的優先權規則,以Persons(Of T)為參數的Select函式會被優先考慮,此處並無此函式,因此次要考慮的是以Persons(Of 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(Of T)介面的物件,圖4展現出當對字串陣列使用from xxx in xxx where xxx的LINQ Expression後的運作流程。
[圖4]
透過編譯器的轉換,LINQ Expression會變成string().Where的函式呼叫,System.Linq.Enumerable.Where函式會建立一個WhereIterator物件,並於建立時將由編譯器轉換所建立出來的delegate (如Where n = “2222”,會變成Function(l) l = "2222",意指建立一個delegate,大概內容是:
Function generateFunc(ByVal l As String) As Boolean
 Return l== “2222”
End Function)
及Source Enumerable Object,也就是string陣列傳入,當設計師操作此WhereIterator物件時,例如呼叫其MoveNext函式(For Each會呼叫此函式),WhereIterator將會逐一由Source Enumerable取出其內的元素,然後呼叫於建立WhereIterator時所傳入的delegate函式來決定此元素是否符合條件。了解流程後,就大略可以得知LINQ To Object Framework的效能了,這跟用For Each將元素一一比對放入另一個陣列中的效能相差無幾,只是LINQ Expression比這種做法簡潔多了。
(PS:與IQueryable結合後的LINQ To ADO.NET Framework,效能就是LINQ Provider的課題了)
 
後記
   下次筆者將針對LINQ Expression的語法做詳細的介紹,各位讀者們下次見了。