LINQ 概述

语言集成查询 (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 文档以执行此任务将更具挑战性。

LINQ 提供程序的作用不仅仅是与 XML 交互。 Linq to SQL 是适用于 MSSQL Server 数据库的极其简练的对象关系映射器 (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 的一些基本部分。 这绝不是全面的,因为 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 图表

可通过 LINQ 轻松表示的可并行 CPU 绑定作业(换句话说,是纯函数,没有副作用)是 PLINQ 的绝佳候选项。 对于确实有副作用的作业,请考虑使用任务并行库

更多资源

  • Linqpad,适用于 C#/F#/Visual Basic 的演练环境和数据库查询引擎
  • EduLinq,帮助用户了解如何实现 LINQ 到对象的电子书