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

code6421發表於2008-05-17
 
LINQ首部曲: LINQ To Object Part 2 - Using VB.NET
 
文/黃忠成
 
 續前文,這一篇一樣是發表於Run! PC雜誌,同時收錄於【極意之道-.NET Framework 3.5資料庫開發聖典-ASP.NET篇】一書中,於此我針對VB.NET做了調整。
 
回到起點: LINQ 語法規則
 
 任何語言皆有其基本語法規則存在,LINQ雖然被打造成一個與編譯器無關的Framework,但為了讓設計師能更方便、更直覺的使用LINQ Expression,目前已知的C#、VB.NET都定義了一套專屬的LINQ Expression語法,雖然各個編譯器所提供的語法略有差異,但基本上差距不遠,本節以VB.NET所提供的LINQ Expression為基準,為讀者們介紹LINQ Expression的語法規則及各式各樣的應用,表1是筆者所整理出來的VB.NET LINQ Expression語法規則。
[表1]
u   query-expression
 From <alias> In <source|query-expression> <<where-expression>|<group-by expression>|<join expression>|<order-by-expression>> <select-expression>
u   where-expression
 left-variable <operand> right-variable
n   operand
(= | <> | < | > | >= | <=)
 left-variable <operand> query-expression(single value result)
 query-expression(single value result)   <operand> left-variable
query-expression(single value result)   <operand> query-expression(single value result)
 variable.<function> (Boolean result)
 variable.<function> operand right-variable
 variable.<function> operand variable<function>
 <shared function> operand right-varaible
 <shared function> operand <shared function>
 <function> operand right-varaible
 <function> operand <shared function>
 <VB.NET Expression single line> (Boolean result)
 <VB.NET Expression single line> operand right-variable
 <VB.NET Expression single line> operand <VB.NET Expression single line>
 where-expression < And | Or | AndAlso | OrElse>, where-expression <*>
u   group-by expression
 group <alias | query-expression> by <alias.member|query-expression(single value result)> into <variable>
group <alias | query-expression> by <alias.member| query-expression(single value result)>,<alias.member| query-expression(single value result)> into <variable>
u   join-expression
 join <alias > in <source|queryt-expression> on <where-expression(equal only)>
 join <alias > in <source|queryt-expression> on <where-expression(equal only)> into <variable>
u   order-by-expression
 order by <alias-member> <asc|desc>
 order by <alias-member>,<alias-member> <asc|desc>
u   select-expression
 Select <alias member>
 Select new With {anonymous type declaration}
乍看之下,此表似乎相當複雜,其實她還蠻簡單的,以此表搭配下文的語法分類解說,應能讓讀者們快速的掌握VB.NET中的LINQ Expression語法。
 
Query-Expression
 
 LINQ Expression中至少會包含一個query-expression,表一中對query-expression的定義如下。
From <alias> In <source|query-expression> <<where-expression>|<group-by expression>|<join expression>|<order-by-expression>> <select-expression>
<alias>指的是欲在此query-expression中使用的別名,更明確說是一個變數,編譯器會將In後面的運算式所產生的結果放到此變數中,此處有一點必須特別指出,In後面的<source|query-exprssion>代表著來源,<soruce>可以是一個IEnumerable(Of T)型別的物件,法則上<source>也可以由另一個<query-exprssion>所取代,我們將此稱為Nested query-expression(巢狀Query)。Query-expression中可以包含Where語句<where-expression>、Group By語句<group expression>、Join語句<join expression>、Order By修飾句<order-by-exprssion>及Select語句<select-expression>,因應不同的語句,其內或允許query-exprssion的出現,下例是一個簡單的query-expression。
Dim list() As String = {"1111", "2222", "3333"}
Dim p = From o In list Select o
 
Where-expression
 
 Query-expression中可以包含where-expression,用來對物件作查詢動作,下例是一個簡單的例子。
Dim list() As String = {"1111", "2222", "3333"}
Dim p = From o In list Where o = "2222" Select o
如表一所示,where-expression的left-expression及 right-expression也可以由query-expression所取代,前提是這個query-expression必須傳回單一值,如程式1。
[程式1]
Sub TestComplexWhere2()
        Dim p1() = { _
                     New With {.Name = "code6421", .Address = "Taipai"}, _
                     New With {.Name = "tom", .Address = "Taipai"}, _
                     New With {.Name = "jeffray", .Address = "NY"} _
                   }
        Dim p2() = { _
                     New With {.Name = "code6421", .Title = "Manager"}, _
                     New With {.Name = "tom", .Title = "Director"}, _
                     New With {.Name = "jeffray", .Title = "Programmer"} _
                   }
        Dim p3() = { _
                   New With {.Name = "code6421", .Hand = "Manager", .Address = "Taipai"}, _
                   New With {.Name = "tom", .Hand = "Director", .Address = "Taipai"}, _
                   New With {.Name = "jeffray", .Hand = "Programmer", .Address = "Taipai"} _
                   }
 
        Dim result = From s In p2  _
         Where (From s1 In p1 Where s1.Name = s.Name Select s1.Address).ToArray()(0) = _
         (From s2 In p3 Where s2.Name = s.Name Select s2.Address).ToArray()(0) Select s
        For Each item In result
            Console.WriteLine(item.Name)
        Next
        Console.ReadLine()
    End Sub
ToArray函式是LINQ To Object Framework所提供的函式,用途是將query-expression的結果轉成陣列,一旦轉成陣列後就可以透過()陣列元素存取值去取得單值。
當然,基於LINQ Expression的轉譯規則,你也可以使用傳回Boolean值的函式來協助比對。
Sub TestWhereWithFunction()
    Dim list() As String = {"code6421", "tom", "cathy"}
    Dim result = From s1 In list Where MyExpressionFunc(s1) Select s1
End Sub
 
Function MyExpressionFunc(ByVal s As String) As Boolean
     Return IIf(s = "code6421", True, False)
End Function
這個函式可以是成員函式(需要有物件、靜態函式,或是位於Module中的函式皆可。
 
Group-expression
 
 Query-expression中允許含有Group-expression,用於將資料分類用,如程式2。
[程式2]
Sub TestGroupByLinq()
        Dim persons() = { _
                         New With {.Name = "code6421", .Age = 18, .Address = "Taipai"}, _
                         New With {.Name = "jeffray", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "catch", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "joe", .Age = 18, .Address = "NY"} _
                         }
        Dim result = From o In persons Group o By o.Address Into g = Group _
              Select New With {.Address = Address, .Persons = g}
        For Each item In result
            Console.WriteLine("Group : {0}", item.Address)
            For Each detailItem In item.Persons
                Console.WriteLine("{0}", detailItem.Name)
            Next
        Next
        Console.ReadLine()
    End Sub
此例執行結果如圖1。
[圖1]
Group : Taipai
code6421
Group : USA
jeffray
catch
Group : NY
joe
此處有兩個尚未提及的指令,Into是將Group後的結果放到g變數中,於VB.NET中,你必須指定g的值為Group關鍵字,此處Group關鍵字的內涵值為Group後的結果,是一個IEnumerable(Of Object)的物件。Select New With則是應用了VB.NET 2008的Anonymous Type技巧,建立一個簡單的物件,其中含有Address及Persons兩個屬性,Address的值來自於Address,也就是Group o By o.Address的鍵值,那就是Address的值。使用Select New With時,記得屬性名稱需以【.】作為前導字,如下:
Select New With {.Name = "code6421", .Age = 18, .Address = "Taipai"},
New With除了可放在Select之後,也可直接用來建立Anonymous Type,如下所示:
New With {.Name = "code6421", .Age = 18, .Address = "Taipai"}
而g本身是一個分類後的IEnumerable(Of Object)型別物件,其內所含的就是以Address值所分類後的Persons物件。與where-expression一樣,group-expression中也可以包含query-expression,如程式4。
[程式4]
Sub TestComplexGroupBy2()
 
        Dim p2() = { _
                   New With {.Name = "code6421", .Hand = "Manager", .Address = "Taipai"}, _
                   New With {.Name = "tom", .Hand = "Director", .Address = "Taipai"}, _
                   New With {.Name = "jeffray", .Hand = "Programmer", .Address = "NT"} _
                   }
 
        Dim result = From s In p2 Group s By Address = (From s1 In p2 Where _
            s.Name = s1.Name Select s1.Address).ToArray()(0) Into g = Group _
                     Select New With {.Address = Address, .Persons = g}
        For Each item In result
            Console.WriteLine("Group : {0}", item.Address)
            For Each detailItem In item.Persons
                Console.WriteLine("{0}", detailItem.Name)
            Next
        Next
        Console.ReadLine()
    End Sub
執行結果如圖2。
[圖2]
Group : Taipai
code6421
tom
Group : NT
jeffray
 
Join-expression
 
 LINQ Expression也支援SQL中常用的Join指令,如程式5。
[程式5]
Sub TestJoin()
        Dim p1() = { _
                     New With {.Name = "code6421", .Address = "Taipai"}, _
                     New With {.Name = "tom", .Address = "Taipai"}, _
                     New With {.Name = "jeffray", .Address = "NY"} _
                   }
        Dim p2() = { _
                     New With {.Name = "code6421", .Title = "Manager"}, _
                     New With {.Name = "tom", .Title = "Director"}, _
                     New With {.Name = "jeffray", .Title = "Programmer"} _
                   }
        Dim result = From s In p1 _
                     Join s1 In p2 On s.Name Equals s1.Name _
                     Select New With {.Name = s.Name, .Address = s.Address, .Title = s1.Title}
        For Each item In result
            Console.WriteLine("Name : {0}, Address : {1}, Title : {2}", _
                   item.Name, item.Address, item.Title)
        Next
        Console.ReadLine()
    End Sub
圖3是執行結果。
[圖3]
Name : code6421, Address : Taipai, Title : Manager
Name : tom, Address : Taipai, Title : Director
Name : jeffray, Address : NY, Title : Programmer
程式6是運用一個以上join的例子。
[程式6]
Sub TestJoin2()
        Dim p1() = { _
                     New With {.Name = "code6421", .Address = "Taipai"}, _
                     New With {.Name = "tom", .Address = "Taipai"}, _
                     New With {.Name = "jeffray", .Address = "NY"} _
                   }
        Dim p2() = { _
                     New With {.Name = "code6421", .Title = "Manager"}, _
                     New With {.Name = "tom", .Title = "Director"}, _
                     New With {.Name = "jeffray", .Title = "Programmer"} _
                   }
        Dim p3() = { _
                     New With {.Name = "code6421", .Hand = "Left"}, _
                     New With {.Name = "tom", .Hand = "Left"}, _
                     New With {.Name = "jeffray", .Hand = "Right"} _
                   }
        Dim result = From s In p1 _
                     Join s1 In p2 On s.Name Equals s1.Name _
                     Join s2 In p3 On s.Name Equals s2.Name _
                     Select New With {.Name = s.Name, .Address = s.Address, _
                          .Title = s1.Title, .Hand = s2.Hand}
        For Each item In result
            Console.WriteLine("Name : {0}, Address : {1}, Title : {2}, Hand : {3}", _
               item.Name, item.Address, item.Title, item.Hand)
        Next
        Console.ReadLine()
    End Sub
執行結果如圖4。
[圖4]
Name : code6421, Address : Taipai, Title : Manager, Hand : Left
Name : tom, Address : Taipai, Title : Director, Hand : Left
Name : jeffray, Address : NY, Title : Programmer, Hand : Right
 
Order-by-expression
 
 Order-by-expression用於將query-expression的結果集排序,如程式7。
[程式7]
Sub TestOrderLinq()
        Dim p() = {"1111", "3333", "2222"}
        Dim result = From s1 In p Order By s1 Descending Select s1
        For Each item In result
            Console.WriteLine(item)
        Next
        Console.ReadLine()
    End Sub
圖5是執行結果。
[圖5]
3333
2222
1111
Descending指的是倒序,Ascending則是正序,未加上Descending也未加上Ascending,則預設為Ascending,Order By可以有多個,彼此以 , 分接,每個都能有正倒序之修飾字,如下所示。
Sub TestOrderLinq2()
        Dim p1() = { _
                     New With {.Name = "code6421", .Address = "Taipai"}, _
                     New With {.Name = "tom", .Address = "Taipai"}, _
                     New With {.Name = "jeffray", .Address = "NY"} _
                     }
        Dim result = From s1 In p1 Order By s1.Name, s1.Address Descending Select s1
        For Each item In result
            Console.WriteLine(item.Name)
        Next
        Console.ReadLine()
    End Sub
 
 
Select-expresssion
 
 Select-expression出現於query-expression的尾端,涵意是由結果集中取出資料,其中較特別的是,有時我們會使用Select New With語句由結果集中取出需要的欄位,建構出另一個型態的回傳物件,如同於join範例中的片段。
Dim result = From s In p1 _
                     Join s1 In p2 On s.Name Equals s1.Name _
                     Join s2 In p3 On s.Name Equals s2.Name _
                     Select New With _
                   {.Name = s.Name, .Address = s.Address, .Title = s1.Title, .Hand = s2.Hand}
這是運用了VB.NET 2008的Anonymouse Type功能所達到的。
 
Distinct
 
 如同SQL中的Distinct一樣,LINQ To Object Framework中的Distinct函式允許設計師將一陣列中特定欄位值相同的部份,僅擷取出一代表值,見程式8。
[程式8]
Sub TestSelectDistinct()
        Dim persons() = { _
                         New With {.Name = "code6421", .Age = 18, .Address = "Taipai"}, _
                         New With {.Name = "jeffray", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "catch", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "joe", .Age = 18, .Address = "NY"} _
                         }
        Dim result = (From s1 In persons Select s1.Address).Distinct()
        For Each item In result
            Console.WriteLine(item)
        Next
        Console.ReadLine()
    End Sub
此例的執行結果如圖6。
[圖6]
Taipai
USA
NY
此例僅列出一個Taipai,這就是Distinct函式的能力,相同值者僅取一代表值。
 
Select Many 功能
 
 截至目前為止的例子皆以一個query-expression作為開始,事實上LINQ Expression支援query-expression的串接,名為Select Many功能,見程式9。
[程式9]
Sub TestSelectMany()
        Dim persons() = { _
                         New With {.Name = "code6421", .Age = 18, .Address = "Taipai"}, _
                         New With {.Name = "jeffray", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "catch", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "joe", .Age = 18, .Address = "NY"} _
                         }
        Dim p2() = { _
                     New With {.Title = "Director"}, _
                     New With {.Title = "Programmer"}, _
                     New With {.Title = "Manager"} _
                    }
 
        Dim result = From s1 In persons _
                     From s2 In p2 _
                     Select New With {.Name = s1.Name, .Title = s2.Title}
 
        For Each item In result
            Console.WriteLine("Name : {0}, Title : {1}", item.Name, item.Title)
        Next
        Console.ReadLine()
    End Sub
此例的執行結果如圖7。
[圖7]
Name : code6421, Title : Director
Name : code6421, Title : Programmer
Name : code6421, Title : Manager
Name : jeffray, Title : Director
Name : jeffray, Title : Programmer
Name : jeffray, Title : Manager
Name : catch, Title : Director
Name : catch, Title : Programmer
Name : catch, Title : Manager
Name : joe, Title : Director
Name : joe, Title : Programmer
Name : joe, Title : Manager
 
Index
 
 LINQ To Object Framework的LINQ Expression允許指定Index,什麼是Index呢 ?請看程式10。
[程式10]
Sub TestIndex()
        Dim persons() = { _
                        New With {.Name = "code6421", .Age = 18, .Address = "Taipai"}, _
                         New With {.Name = "jeffray", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "catch", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "joe", .Age = 18, .Address = "NY"} _
                         }
        Dim result = From s1 In persons Select s1.Name(0)
        For Each item In result
            Console.WriteLine(item)
        Next
        Console.ReadLine()
    End Sub
此例會列出陣列中Name屬性值的第一個字元,事實上這是當然的結果,因為where後面的語句,會被VB.NET編譯器當成Lambda Expression處理,而Name屬性是string型態,自然也能對其使用()陣列存取子了,同時也因為是string型別,而且會被轉為Lambda Expression,自然能呼叫屬於string型別的Contains函式了。
 
Take、TakeWhile
 
 Take函式可以讓設計師由一個IEnumerable<T>物件中取出指定的元素數量,請見程式11。
[程式11]
Sub TestTake()
        Dim persons() = { _
                         New With {.Name = "code6421", .Age = 18, .Address = "Taipai"}, _
                         New With {.Name = "jeffray", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "catch", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "joe", .Age = 18, .Address = "NY"} _
                         }
        Dim result = persons.Take(2)
        For Each item In result
            Console.WriteLine(item.Name)
        Next
        Console.ReadLine()
    End Sub
此例會從p1中取出前兩個元素,也就是code6421及jeffray,Take函式的功能有點像是SQL Server的Select TOP。另一個TakeWhile函式則可以讓設計師以while的方式來取出IEnumerable<T>中的元素,請見程式12。
[程式12]
Sub TestTakeWhile()
        Dim persons() = { _
                         New With {.Name = "code6421", .Age = 18, .Address = "Taipai"}, _
                         New With {.Name = "jeffray", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "catch", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "joe", .Age = 18, .Address = "NY"} _
                         }
        Dim result = persons.TakeWhile(Function(p) p.Address = "Taipai")
        For Each item In result
            Console.WriteLine(item.Name)
        Next
        Console.ReadLine()
    End Sub
此例的執行結果與Take相同,不同之處在於TakeWhile會一直取出元素,直到某個元素不符合所指定的Lambda Expression為止,這意味著假如指定Address == “USA”時,將不會有任何元素列出,因為陣列中的第一個元素就已經不符合條件了,所以While動作就結束了。
 
Skip、SkipWhile
 
 相對於Take,Skip允許設計師略過指定的元素數,如程式13。
[程式13]
Sub TestSkip()
        Dim persons() = { _
                         New With {.Name = "code6421", .Age = 18, .Address = "Taipai"}, _
                         New With {.Name = "jeffray", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "catch", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "joe", .Age = 18, .Address = "NY"} _
                         }
        Dim result = (From s1 In persons Select s1).Skip(2)
        For Each item In result
            Console.WriteLine(item.Name)
        Next
        Console.ReadLine()
    End Sub
此例會由catch的元素開始列出。另一個SkipWhile函式與TakeWhile的概念相同,SkipWhile也是以While的觀念執行Skip動作,如程式14。
[程式14]
Sub TestSkipWhile()
        Dim persons() = { _
                         New With {.Name = "code6421", .Age = 18, .Address = "Taipai"}, _
                         New With {.Name = "jeffray", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "catch", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "joe", .Age = 18, .Address = "NY"} _
                         }
        Dim result = (From s1 In persons Select s1).SkipWhile(Function(p) p.Address = "Taipai")
        For Each item In result
            Console.WriteLine(item.Name)
        Next
        Console.ReadLine()
    End Sub
與TakeWhile一樣,若指定Address == “USA”時,那麼將會列出所有元素,因為在第一個元素時,While就已經結束了。
 
First、FirstOrDefault
 
   First函式允許設計師指定一個Lambda Expression條件式, 她將以此對IEnumerable(Of T)中的元素查詢,並傳回第一個符合條件的元素,當不指定條件式時,First會傳回第一個元素。與Where不同,First只會傳回符合資料的第一個元素,而非內含所有符合條件元素的IEnumerable(Of T)結果集,見程式15。
[程式15]
Sub TestFirst()
        Dim persons() = { _
                         New With {.Name = "code6421", .Age = 18, .Address = "Taipai"}, _
                         New With {.Name = "jeffray", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "catch", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "joe", .Age = 18, .Address = "NY"} _
                         }
        Dim result = (From s1 In persons Select s1).First()
        Console.WriteLine(result.Name)
        Console.ReadLine()
    End Sub
下例則是使用條件式的寫法。
Sub TestFirst2()
        Dim persons() = { _
                         New With {.Name = "code6421", .Age = 18, .Address = "Taipai"}, _
                         New With {.Name = "jeffray", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "catch", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "joe", .Age = 18, .Address = "NY"} _
                         }
        Dim result = (From s1 In persons Select s1).First(Function(p) p.Address = "USA")
        Console.WriteLine(result.Name)
        Console.ReadLine()
    End Sub
當IEnumerable(Of T)中未含有符合條件的元素時,First將會引發例外。另一個FirstOrDefault函式與First函式類似,唯一不同之處是當IEnumerable(Of T)中未含有符合條件之元素時,她將回傳元素之型別的預設值(object為null、數值為0)。與First相對的是Last函式,可取得最後一個元素,她也有另一個同型函式為LastOrDefault,設計理念與FirstOrDefault相同。
 
ElementAt、ElementAtOrDefault
 
 ElementAt函式可以取得一個物件集中特定位置的元素,如下所示:
Sub TestElementAt()
        Dim p1() As String = {"code6421", "tom", "cathy"}
        Dim result = p1.ElementAt(1)
        Console.WriteLine(result)
        Console.ReadLine()
End Sub
此例會列出tom這個元素,當指定位置超出物件集大小時,ElementAt會丟擲例外。如同FirstOrDefault一般,ElementAt有另一個同型函式:ElementAtOrDefault,當使用此函式時指定位置超出物件集大小時,不會觸發例外,而是直接回傳預設值(object為null,數值為0)。
 
ToArray、ToList、ToDictionary
 
 這三個函式會將IEnuermable(Of T)轉成Array、List或是Dictionary型別,預設情況下,當我們對某個IEnumerable(Of T)下達Where等條件式時,所取得的結果會是一個IEnumerable(Of T)物件,此時所有條件都尚未執行比對的動作,當對這個IEnumerable(Of T)物件下達MoveNext(For Each會觸發此函式)時,該物件才會進行條件比對。ToArray等函式可以改變此行為模式,當我們對IEnumerable(Of T)物件呼叫這些函式時,其將會以For Each一一巡覽IEnumerable(Of T)物件中的元素並進行條件比對,然後放到另一個結果值後傳回,這也就是說,呼叫此函式所傳回的結果值,將會是已經完成比對後的結果值,操作這個結果集自然比直接操控具條件的結果集來得有效率,程式16是一個使用ToDictionary函式的例子。
[程式16]
Sub TestToDictionary()
        Dim persons() = { _
                        New With {.Name = "code6421", .Age = 18, .Address = "Taipai"}, _
                        New With {.Name = "jeffray", .Age = 18, .Address = "USA"}, _
                        New With {.Name = "catch", .Age = 18, .Address = "USA"}, _
                        New With {.Name = "joe", .Age = 18, .Address = "NY"} _
                        }
        Dim result = persons.ToDictionary(Function(p) p.Name)
        Console.WriteLine(result("code6421").Address)
        Console.ReadLine()
    End Sub
ToDictionary函式為以指定的鍵值為主鍵,將元素轉為Dictionary物件,此時你可透過預設存取子,以主鍵為引數來取得對應之元素。
 
Union
 
   Union函式可以將兩個IEnumerable(Of T)物件整合成一個, 如程式17。
[程式17]
Sub TestUnion()
        Dim p1() As Integer = {1, 3, 5, 7, 9, 11}
        Dim p2() As Integer = {2, 4, 6, 8, 10, 11}
        Dim result = p1.Union(p2)
        For Each item In result
            Console.WriteLine(item)
        Next
        Console.ReadLine()
    End Sub
執行結果圖10。
[圖10]
1
3
5
7
9
11
2
4
6
8
10
如結果所示,兩個IEnumerable(Of T)物件被整合成一個了,在呼叫Union函式時,我們也可以指定一個實作了IEqualComparaer介面的物件來改變結果,如程式18所示。
[程式18]
Sub TestUnion2()
        Dim p1() As String = {"code6421", "tom", "cathy"}
        Dim p2() As String = {"code6421", "Tom", "cathy"}
        Dim result = p1.Union(p2, _
            StringComparer.Create(System.Globalization.CultureInfo.CurrentCulture, True))
        For Each item In result
            Console.WriteLine(item)
        Next
        Console.ReadLine()
    End Sub
此例的意思是,以無關大小寫的字串比對方式,比對兩個IEnumerable(Of T)物件中的元素,避免列出重複的元素,圖11是此例的執行結果。
[圖11]
code6421
tom
cathy
 
Intersect
 
 Intersect函式可以找出兩個IEnumerable(Of T)物件中相同的元素,如程式19。
[程式19]
Sub TestIntersect()
        Dim p1() As Integer = {1, 3, 5, 7, 9, 11}
        Dim p2() As Integer = {2, 4, 6, 7, 10, 11}
        Dim result = p1.Intersect(p2)
        For Each item In result
            Console.WriteLine(item)
        Next
        Console.ReadLine()
    End Sub
此例的執行結果僅會列出相同的數字,也就是7與11。在呼叫Intersect函式時,也可以像Union函式般,指定一個 實作了IEqualComparer介面的物件做為比對基準,如程式20。
[程式20]
Sub TestIntersect2()
        Dim p1() As String = {"code6421", "tom", "cathy"}
        Dim p2() As String = {"code6421", "Tom", "cathy"}
        Dim result = p1.Intersect(p2, _
          StringComparer.Create(System.Globalization.CultureInfo.CurrentCulture, True))
        For Each item In result
            Console.WriteLine(item)
        Next
        Console.ReadLine()
    End Sub
此例是以無關大小寫字串的方式比對,執行結果圖12。
[圖12]
code6421
tom
cathy
Tom、tom因字串是以不分大小寫比對的方式,所以僅列出一個。
 
Except
 
 Except函式允許傳入一個IEnumerable(Of T)物件,以其來比對原始的IEnumerable(Of T)物件中的元素,僅列出未出現在所傳入IEnumerable(Of T)物件中的元素,見程式21。
[程式21]
Sub TestExcept()
        Dim p1() As Integer = {1, 3, 5, 7, 9, 11}
        Dim p2() As Integer = {2, 4, 6, 8, 10, 11}
        Dim result = p1.Except(p2)
        For Each item In result
            Console.WriteLine(item)
        Next
        Console.ReadLine()
End Sub
執行結果如圖13。
[圖13]
1
3
5
7
9
如結果所示,11已經被排除了。與Union函式相同,Except函式也允許傳入一個實作了IEqualComparer介面的物件,用來當做比對的基準,如程式22。
[程式22]
Sub TestExcept2()
        Dim p1() As String = {"code6421", "tom", "cathy2"}
        Dim p2() As String = {"code6421", "Tom", "cathy"}
        Dim result = p1.Except(p2, _
         StringComparer.Create(System.Globalization.CultureInfo.CurrentCulture, True))
        For Each item In result
            Console.WriteLine(item)
        Next
        Console.ReadLine()
    End Sub
此例僅會列出cathy2這個元素。
 
OfType、Cast
 
 OfType、Cast是LINQ To Object Framework所提供的轉型函式,可以將一個IEnumerable(Of T)物件中的元素轉型為所要求的型別,兩者不同之處在於OfType函式於轉型失敗時並不會丟出例外,而Cast則會丟出例外,見程式23。
[程式23]
Sub TestOfType()
        Dim ar As New ArrayList()
        ar.Add("Test1")
        ar.Add("Test2")
        ar.Add("Test3")
        ar.Add("Test4")
        ar.Add(16)
        Dim result = ar.OfType(Of String)()
        'Of Type will not raise invalid type-casting exception.
        'if you need receive the exception at type-casting,use Cast.
        For Each item In result
            Console.WriteLine("Type is {0} and value is {1}", item.GetType(), item)
        Next
        Console.ReadLine()
    End Sub
此例執行結果如圖14。
[圖14]
Type is System.String and value is Test1
Type is System.String and value is Test2
Type is System.String and value is Test3
Type is System.String and value is Test4
 
Sum、Average、Min、Max、Count、
 
 LINQ To Object Framework也提供了Sum、Min、Max、Average、Count等之類常見於SQL指令中的功能函式,Sum用於加總、Min用於取出最小值、Max則用於取出最大值、Count則用於計算符合條件的元素數目,基本上這四個函式的用法大致相同,因此此處我只以Sum函式做為代表例,見程式24。
[程式24]
Sub TestSum()
        Dim list() As Integer = {18, 20, 25}
        Dim result = list.Sum()
        Console.WriteLine(result)
        Console.ReadLine()
End Sub
無意外,此例的執行結果為63,無引數的Sum函式僅作用於數值型別,如Int32、Int64、Decimal、Double、Single等等,當需加總的是一個物件時,就必需傳入一個Lambda Expression來協助Sum函式運算,見程式25。
[程式25]
Sub TestSum2()
        Dim persons() = { _
                         New With {.Name = "code6421", .Age = 18, .Address = "Taipai"}, _
                         New With {.Name = "jeffray", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "catch", .Age = 18, .Address = "USA"}, _
                         New With {.Name = "joe", .Age = 18, .Address = "NY"} _
                         }
        Dim result = persons.Sum(Function(p) p.Age)
        Console.WriteLine(result)
        Console.ReadLine()
End Sub
 
Aggregate
 
 Sum函式用於計算加總值,但若需要的不僅僅是加法呢?例如乘法、減法、除法等等,此時另一個Aggregate函式就能派上用場了,見程式26。
[程式26]
Sub TestAggreate()
       Dim list() As Integer = {18, 20, 25}
        Dim result = list.Aggregate(Function(x, y) x * y)
        Console.WriteLine(result)
        Console.ReadLine()
End Sub
此例的結果是9000,也就是18*20*25後的結果,另外於呼叫Aggregate函式時,也可以指定一個起始值,如程式27。
[程式27]
Sub TestAggreate2()
        Dim list() As Integer = {18, 20, 25}
        Dim result = list.Aggregate(9000, Function(x, y) x * y)
        Console.WriteLine(result)
        Console.ReadLine()
End Sub
此例的結果是,9000*18*20*25 = 81000000。
 
語法之外,明確使用LINQ TO Object Framework的理由
 
 在本文中的範例,偶而會使用Where函式而不是使用LINQ Expression所提供的where
,讀者或許對此舉會有所疑惑,既然已經有了對應的LINQ Expression,又何必使用函式呼叫呢?簡單的說,LINQ Expression的轉換支援並無法完全取代函式呼叫,以Where例子來說,當使用函式呼叫時,我們可以寫下很複雜的處理函式,如程式28。
[程式28]
Sub TestWhereFunction()
        Dim list() As String = {"code6421", "tom", "cathy"}
        Dim result = list.Where(AddressOf CompareData)
        For Each item In result
            Console.WriteLine(item)
        Next
        Console.ReadLine()
End Sub
 
Function CompareData(ByVal val As String) As Boolean
        Dim conn As New SqlClient.SqlConnection()
        'do something of you want..............
        Return True
End Function
這是無法以單純的LINQ Expression來辦到的。
註:C#的Lambda Expression可以允許寫得很複雜,如下:
var p5 = p1.Where(x =>
                              {
                                  bool result = false;
                                  SqlConnection conn = new SqlConnection("....");
                                  conn.Open();
                                  return result;
                              });
不過VB.NET 2008並不支援此類寫法,所以還是得走Function模式。
 
 
Dim、Dim xxx() As Type的差異
 
 一般來說,你可以以Dim來宣告一個陣列,而不事先指定其型別,如下所示:
Dim list() = {1, 2, 3}
此時當你透過list來呼叫某些LINQ函式時,會發生要求傳入一個Lambda Expression的情況,例如Sum就得寫成下面這樣:
Dim list() = {1, 2, 3}
Dim result = list.Sum(Function(p) p)
這是因為list陣列被當成Object陣列的緣故,直接於Dim時給予型別,那麼就可以用下面的方式呼叫Sum函式。
Sub Main()
        Dim list() As Integer = {1, 2, 3}
        Dim result = list.Sum()
        Console.WriteLine(result)
        Console.ReadLine()
End Sub
所以,如非必要,使用Dim As Type的方式,可以避免因編譯器的猜測所產生的不必要問題。
 
 
還是效能的課題
 
 在LINQ To Object Framework中,效能並非是首要考量的課題,因此許多功能都隱涵效能的問題,例如where,預設是遍覽IEnumerable(Of T)物件中的元素,並一一以傳入的Lambda Expression來驗證,這在某些情況下是不適用的,因為 有時我們只是想尋找某一個符合條件的元素,而且該IEnumerable(Of T)物件是擁有既定排序的,此時採取Binary Search的演演算法來搜尋所要的資料是較有效率的。因此!設計師應該將LINQ To Object Framework視為是一個查詢演演算法的Framework,但其不保證以最快的方式來完成所要求的動作。

相關文章