LINQ 概觀

Language-Integrated Query (LINQ) 提供語言層級查詢功能,以及 C# 和 Visual Basic 的較高順序函式 API,可讓您撰寫運算式宣告式程式碼。

語言層級查詢語法

這是語言層級查詢語法:

var linqExperts = from p in programmers
                  where p.IsNewToLINQ
                  select new LINQExpert(p);
Dim linqExperts = From p in programmers
                  Where p.IsNewToLINQ
                  Select New LINQExpert(p)

這是使用 IEnumerable<T> API 的相同範例:

var linqExperts = programmers.Where(p => p.IsNewToLINQ)
                             .Select(p => new LINQExpert(p));
Dim linqExperts = programmers.Where(Function(p) p.IsNewToLINQ).
                             Select(Function(p) New LINQExpert(p))

LINQ 簡單易懂

假設您有一份寵物清單,但想要將它轉換成字典,讓您可以藉由其 RFID 值直接存取寵物。

這是傳統的命令式程式碼:

var petLookup = new Dictionary<int, Pet>();

foreach (var pet in pets)
{
    petLookup.Add(pet.RFID, pet);
}
Dim petLookup = New Dictionary(Of Integer, Pet)()

For Each pet in pets
    petLookup.Add(pet.RFID, pet)
Next

程式碼背後的目的不是要建立新的 Dictionary<int, Pet> 並透過迴圈將其加入其中,而是要將現有的清單轉換為字典! LINQ 會保留這項目的,而命令式程式碼不會。

這是同等的 LINQ 運算式:

var petLookup = pets.ToDictionary(pet => pet.RFID);
Dim petLookup = pets.ToDictionary(Function(pet) pet.RFID)

使用 LINQ 的程式碼之所以重要,是因為它在程式設計師思考時為目的和程式碼之間提供了平衡。 另一個好處是讓程式碼保持簡潔。 想像一下減少 1/3 程式碼基底的情況,就如同以上所做一樣。 很不錯,對吧?

LINQ 提供者可簡化資料存取

在現實世界的軟體中,大部分的程序都圍繞在從某個來源處理資料 (資料庫、JSON、XML 等)。 通常這牽涉到針對每個資料來源學習新的 API,這可能很煩人。 LINQ 藉由將常見元素提取至查詢語法內,而該語法在哪一個資料來源中看起來都相同來簡化此工作。

這會尋找具有特定屬性值的所有 XML 元素:

public static IEnumerable<XElement> FindAllElementsWithAttribute(XElement documentRoot, string elementName,
                                           string attributeName, string value)
{
    return from el in documentRoot.Elements(elementName)
           where (string)el.Element(attributeName) == value
           select el;
}
Public Shared Function FindAllElementsWithAttribute(documentRoot As XElement, elementName As String,
                                           attributeName As String, value As String) As IEnumerable(Of XElement)
    Return From el In documentRoot.Elements(elementName)
           Where el.Element(attributeName).ToString() = value
           Select el
End Function

撰寫程式碼以手動周遊 XML 文件來執行此工作會是更大的挑戰。

與 XML 互動不是您可以使用 LINQ 提供者來執行的唯一工作。 Linq to SQL 是相當基本的 MSSQL 伺服器資料庫物件關聯式對應程式 (ORM)。 Json.NET 程式庫提供透過 LINQ 的具效率 JSON 文件周遊。 此外,如果沒有您所需的程式庫,您也可以撰寫你自己的 LINQ 提供者

使用查詢語法的原因

為什麼要使用查詢語法? 這是常常被人提起的問題。 畢竟,下列程式碼:

var filteredItems = myItems.Where(item => item.Foo);
Dim filteredItems = myItems.Where(Function(item) item.Foo)

是比下列問題更簡潔︰

var filteredItems = from item in myItems
                    where item.Foo
                    select item;
Dim filteredItems = From item In myItems
                    Where item.Foo
                    Select item

API 語法只是一個使用查詢語法的更簡潔方法嗎?

不會。 查詢語法可讓您使用 let 子句,允許您在運算式的範圍內導入及繫結變數,並在運算式的後續片段中使用它。 可以僅使用 API 語法來重新產生相同的程式碼,但很可能會產生難以閱讀的程式碼。

而這就帶出了一個問題,您應該只使用查詢語法嗎?

如果符合下列條件,則此問題的答案為

  • 現有的程式碼基底已經使用查詢語法。
  • 為了避免過於複雜,您需要在查詢內指定變數的範圍。
  • 您偏好查詢語法,且它不會偏離程式碼基底。

如果你符合下列條件,則此問題的答案為...

  • 現有的程式碼基底已經使用 API 語法
  • 您不需要在查詢內指定變數的範圍
  • 您偏好 API 語法,且它不會影響程式碼基底

Essential LINQ

如需 LINQ 範例的真正完整清單,請瀏覽 101 LINQ Samples (LINQ 範例入門)。

以下範例是一些 LINQ 基本部分的簡單示範。 這些示範並不完整,因為 LINQ 可提供比此處說明的更多功能。

基本要素 - WhereSelectAggregate

// Filtering a list.
var germanShepherds = dogs.Where(dog => dog.Breed == DogBreed.GermanShepherd);

// Using the query syntax.
var queryGermanShepherds = from dog in dogs
                          where dog.Breed == DogBreed.GermanShepherd
                          select dog;

// Mapping a list from type A to type B.
var cats = dogs.Select(dog => dog.TurnIntoACat());

// Using the query syntax.
var queryCats = from dog in dogs
                select dog.TurnIntoACat();

// Summing the lengths of a set of strings.
int seed = 0;
int sumOfStrings = strings.Aggregate(seed, (partialSum, nextString) => partialSum + nextString.Length);
' Filtering a list.
Dim germanShepherds = dogs.Where(Function(dog) dog.Breed = DogBreed.GermanShepherd)

' Using the query syntax.
Dim queryGermanShepherds = From dog In dogs
                          Where dog.Breed = DogBreed.GermanShepherd
                          Select dog

' Mapping a list from type A to type B.
Dim cats = dogs.Select(Function(dog) dog.TurnIntoACat())

' Using the query syntax.
Dim queryCats = From dog In dogs
                Select dog.TurnIntoACat()

' Summing the lengths of a set of strings.
Dim seed As Integer = 0
Dim sumOfStrings As Integer = strings.Aggregate(seed, Function(partialSum, nextString) partialSum + nextString.Length)

簡維清單的清單

// Transforms the list of kennels into a list of all their dogs.
var allDogsFromKennels = kennels.SelectMany(kennel => kennel.Dogs);
' Transforms the list of kennels into a list of all their dogs.
Dim allDogsFromKennels = kennels.SelectMany(Function(kennel) kennel.Dogs)

兩組集合之間的聯集 (使用自訂比較子)

public class DogHairLengthComparer : IEqualityComparer<Dog>
{
    public bool Equals(Dog a, Dog b)
    {
        if (a == null && b == null)
        {
            return true;
        }
        else if ((a == null && b != null) ||
                 (a != null && b == null))
        {
            return false;
        }
        else
        {
            return a.HairLengthType == b.HairLengthType;
        }
    }

    public int GetHashCode(Dog d)
    {
        // Default hashcode is enough here, as these are simple objects.
        return d.GetHashCode();
    }
}
...

// Gets all the short-haired dogs between two different kennels.
var allShortHairedDogs = kennel1.Dogs.Union(kennel2.Dogs, new DogHairLengthComparer());
Public Class DogHairLengthComparer
  Inherits IEqualityComparer(Of Dog)

  Public Function Equals(a As Dog,b As Dog) As Boolean
      If a Is Nothing AndAlso b Is Nothing Then
          Return True
      ElseIf (a Is Nothing AndAlso b IsNot Nothing) OrElse (a IsNot Nothing AndAlso b Is Nothing) Then
          Return False
      Else
          Return a.HairLengthType = b.HairLengthType
      End If
  End Function

  Public Function GetHashCode(d As Dog) As Integer
      ' Default hashcode is enough here, as these are simple objects.
      Return d.GetHashCode()
  End Function
End Class

...

' Gets all the short-haired dogs between two different kennels.
Dim allShortHairedDogs = kennel1.Dogs.Union(kennel2.Dogs, New DogHairLengthComparer())

兩組集合之間的交集

// Gets the volunteers who spend share time with two humane societies.
var volunteers = humaneSociety1.Volunteers.Intersect(humaneSociety2.Volunteers,
                                                     new VolunteerTimeComparer());
' Gets the volunteers who spend share time with two humane societies.
Dim volunteers = humaneSociety1.Volunteers.Intersect(humaneSociety2.Volunteers,
                                                     New VolunteerTimeComparer())

排序

// Get driving directions, ordering by if it's toll-free before estimated driving time.
var results = DirectionsProcessor.GetDirections(start, end)
              .OrderBy(direction => direction.HasNoTolls)
              .ThenBy(direction => direction.EstimatedTime);
' Get driving directions, ordering by if it's toll-free before estimated driving time.
Dim results = DirectionsProcessor.GetDirections(start, end).
                OrderBy(Function(direction) direction.HasNoTolls).
                ThenBy(Function(direction) direction.EstimatedTime)

執行個體屬性的同等性

最後是一項更進階的範例︰判斷兩個相同類型執行個體的屬性值是否相等 (從 此 StackOverflow 文章中借用並經過修改):

public static bool PublicInstancePropertiesEqual<T>(this T self, T to, params string[] ignore) where T : class
{
    if (self == null || to == null)
    {
        return self == to;
    }

    // Selects the properties which have unequal values into a sequence of those properties.
    var unequalProperties = from property in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
                            where !ignore.Contains(property.Name)
                            let selfValue = property.GetValue(self, null)
                            let toValue = property.GetValue(to, null)
                            where !Equals(selfValue, toValue)
                            select property;
    return !unequalProperties.Any();
}
<System.Runtime.CompilerServices.Extension()>
Public Function PublicInstancePropertiesEqual(Of T As Class)(self As T, [to] As T, ParamArray ignore As String()) As Boolean
    If self Is Nothing OrElse [to] Is Nothing Then
        Return self Is [to]
    End If

    ' Selects the properties which have unequal values into a sequence of those properties.
    Dim unequalProperties = From [property] In GetType(T).GetProperties(BindingFlags.Public Or BindingFlags.Instance)
                            Where Not ignore.Contains([property].Name)
                            Let selfValue = [property].GetValue(self, Nothing)
                            Let toValue = [property].GetValue([to], Nothing)
                            Where Not Equals(selfValue, toValue) Select [property]
    Return Not unequalProperties.Any()
End Function

PLINQ

PLINQ 或 Parallel LINQ,都是 LINQ 運算式的平行執行引擎。 換句話說,標準 LINQ 運算式可以在任意數目的執行緒上進行完整的平行處理。 這是透過在執行運算式之前呼叫 AsParallel() 所完成的。

請考慮下列事項:

public static string GetAllFacebookUserLikesMessage(IEnumerable<FacebookUser> facebookUsers)
{
    var seed = default(UInt64);

    Func<UInt64, UInt64, UInt64> threadAccumulator = (t1, t2) => t1 + t2;
    Func<UInt64, UInt64, UInt64> threadResultAccumulator = (t1, t2) => t1 + t2;
    Func<Uint64, string> resultSelector = total => $"Facebook has {total} likes!";

    return facebookUsers.AsParallel()
                        .Aggregate(seed, threadAccumulator, threadResultAccumulator, resultSelector);
}
Public Shared GetAllFacebookUserLikesMessage(facebookUsers As IEnumerable(Of FacebookUser)) As String
{
    Dim seed As UInt64 = 0

    Dim threadAccumulator As Func(Of UInt64, UInt64, UInt64) = Function(t1, t2) t1 + t2
    Dim threadResultAccumulator As Func(Of UInt64, UInt64, UInt64) = Function(t1, t2) t1 + t2
    Dim resultSelector As Func(Of Uint64, string) = Function(total) $"Facebook has {total} likes!"

    Return facebookUsers.AsParallel().
                        Aggregate(seed, threadAccumulator, threadResultAccumulator, resultSelector)
}

此程式碼會視需要來跨系統執行緒進行 facebookUsers 分割、平行加總每個執行緒上的總相似項目、加總每個執行緒所計算的結果,並將結果投射至良好的字串。

以圖表形式︰

PLINQ diagram

可以輕鬆地透過 LINQ 表示、可平行的 CPU 繫結工作 (亦即,純虛擬函式,而且沒有任何副作用),是 PLINQ 的絕佳候選項目。 針對確實會產生副作用的工作,請考量使用工作平行程式庫

其他資源

  • 101 LINQ 範例
  • Linqpad 為 C#/F#/Visual Basic 的遊樂場環境和資料庫查詢引擎
  • EduLinq,為學習 LINQ-to-object 如何實作的電子書