LINQ 概述LINQ overview

语言集成查询 (LINQ) 为 C# 和 Visual Basic 提供语言级查询功能和高阶函数 API,让你能够编写具有很高表达力度的声明性代码。Language-Integrated Query (LINQ) provides language-level querying capabilities, and a higher-order function API to C# and Visual Basic, that enable you to write expressive declarative code.

语言级查询语法Language-level query syntax

语言级查询语法如下:This is the language-level query syntax:

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 的情况如下:This is the same example using the 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 具有很高的表达力度LINQ is expressive

假设你有一份宠物列表,但想要将它转换为字典,以便可以使用宠物的 RFID 值直接访问宠物信息。Imagine you have a list of pets, but want to convert it into a dictionary where you can access a pet directly by its RFID value.

传统的命令性代码如下:This is traditional imperative code:

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> 并通过循环在其中添加条目,而是将现有列表转换为字典!The intention behind the code isn't to create a new Dictionary<int, Pet> and add to it via a loop, it's to convert an existing list into a dictionary! LINQ 维持这种意图,而命令性代码则不会。LINQ preserves the intention whereas the imperative code doesn't.

等效的 LINQ 表达式如下:This is the equivalent LINQ expression:

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

使用 LINQ 的代码非常有效,因为在程序员的推理过程中,LINQ 能够在意图与代码之间找到合理的平衡。The code using LINQ is valuable because it evens the playing field between intent and code when reasoning as a programmer. 另一个好处就是精简代码。Another bonus is code brevity. 想像一下,如果能够像上面一样将大部分的基本代码减掉 1/3,情况会怎样?Imagine reducing large portions of a codebase by 1/3 as done above. 真好,对吧?Sweet deal, right?

LINQ 提供程序简化数据访问LINQ providers simplify data access

对于生产环境中的软件,其重要功能块的任务不外乎就是来自某些源(数据库、JSON、XML 等)的数据。For a significant chunk of software out in the wild, everything revolves around dealing with data from some source (Databases, JSON, XML, and so on). 通常,这就需要用户学习每个数据源的新 API,而这是一个枯燥的过程。Often this involves learning a new API for each data source, which can be annoying. LINQ 可将用于数据访问的常用元素抽象化成查询语法,不过你选择哪种数据源,这种语法看上去都是相同的,因而简化了此任务。LINQ simplifies this by abstracting common elements of data access into a query syntax that looks the same no matter which data source you pick.

这将查找具有特定属性值的所有 XML 元素:This finds all XML elements with a specific attribute value:

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 文档会带来重重困难。Writing code to manually traverse the XML document to do this task would be far more challenging.

LINQ 提供程序的作用不仅仅是与 XML 交互。Interacting with XML isn't the only thing you can do with LINQ Providers. Linq to SQL 是适用于 MSSQL Server 数据库的极其简练的对象关系映射器 (ORM)。Linq to SQL is a fairly bare-bones Object-Relational Mapper (ORM) for an MSSQL Server Database. 使用 JSON.NET 库可以通过 LINQ 有效遍历 JSON 文档。The JSON.NET library provides efficient JSON Document traversal via LINQ. 此外,如果没有哪个库可以解决你的需要,你还可以编写自己的 LINQ 提供程序Furthermore, if there isn't a library that does what you need, you can also write your own LINQ Provider!

使用查询语法的理由Reasons to use the query syntax

为什么要使用查询语法?Why use query syntax? 这是用户经常提出的一个问题。This is a question that often comes up. 无论如何,对于下面的代码:After all, the following code:

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

要比下面的代码简洁得多:is a lot more concise than this:

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

难道 API 语法不比查询语法更简洁吗?Isn't the API syntax just a more concise way to do the query syntax?

不是。No. 查询语法允许使用 let 子句,这样,便可以在表达式的作用域内引入和绑定变量,然后在表达式的后续片段中使用该变量。The query syntax allows for the use of the let clause, which allows you to introduce and bind a variable within the scope of the expression, using it in subsequent pieces of the expression. 只使用 API 语法重现相同的代码也是可行的,不过,这很可能会导致代码难以阅读。Reproducing the same code with only the API syntax can be done, but will most likely lead to code that's hard to read.

那么,问题来了,只使用查询语法可以吗?So this begs the question, should you just use the query syntax?

在以下情况下,此问题的答案是可以:The answer to this question is yes if:

  • 现有的基本代码已使用查询语法。Your existing codebase already uses the query syntax.
  • 由于复杂性的问题,需要在查询中限定变量的作用域。You need to scope variables within your queries because of complexity.
  • 你偏好使用查询语法,并且它不会使基本代码变得混乱。You prefer the query syntax and it won't distract from your codebase.

在以下情况下,此问题的答案是 不可以...The answer to this question is no if...

  • 现有的基本代码已使用 API 语法Your existing codebase already uses the API syntax
  • 不需要在查询中限定变量的作用域You have no need to scope variables within your queries
  • 你偏好使用 API 语法,并且它不会使基本代码变得混乱You prefer the API syntax and it won't distract from your codebase

基本 LINQEssential LINQ

有关 LINQ 示例的完整列表,请访问 101 个 LINQ 示例For a truly comprehensive list of LINQ samples, visit 101 LINQ Samples.

以下示例简单演示了 LINQ 的一些重要片段。The following examples are a quick demonstration of some of the essential pieces of LINQ. 没有办法演示完整的代码,因为 LINQ 提供的功能比此处演示的要多。This is in no way comprehensive, as LINQ provides more functionality than what is showcased here.

语句构成 - WhereSelectAggregateThe bread and butter - Where, Select, and Aggregate

// 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, (s1, s2) => s1.Length + s2.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(s1, s2) s1.Length + s2.Length)

平展列表的列表Flattening a list of lists

// 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)

两个集之间的联合(使用自定义比较运算符)Union between two sets (with custom comparator)

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())

两个集之间的交集Intersection between two sets

// 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())

中间件排序Ordering

// 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)

实例属性的相等性Equality of instance properties

最后,我们演示一个更高级的示例:确定相同类型的两个实例的属性值是否相等(该示例摘自此 StackOverflow 文章,不过已做修改):Finally, a more advanced sample: determining if the values of the properties of two instances of the same type are equal (Borrowed and modified from this StackOverflow post):

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

PLINQPLINQ

PLINQ(又称并行 LINQ)是 LINQ 表达式的并行执行引擎。PLINQ, or Parallel LINQ, is a parallel execution engine for LINQ expressions. 换言之,LINQ 正则表达式可能会没有意义地在任意数量的线程之间并行化。In other words, a regular LINQ expression can be trivially parallelized across any number of threads. 为此,可以调用表达式前面的 AsParallel()This is accomplished via a call to AsParallel() preceding the expression.

考虑以下情况:Consider the following:

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 分区,累加每个并行线程上的类似项总计,累加每个线程计算的结果,然后将该结果投影为一个合理的字符串。This code will partition facebookUsers across system threads as necessary, sum up the total likes on each thread in parallel, sum the results computed by each thread, and project that result into a nice string.

图示:In diagram form:

PLINQ 图示

可通过 LINQ 能够轻松表达的可并行化 CPU 密集型作业(即,没有副作用的纯函数)非常适合使用 PLINQ 来处理。Parallelizable CPU-bound jobs that can be easily expressed via LINQ (in other words, are pure functions and have no side effects) are a great candidate for PLINQ. 对于 确实 有副作用的作业,请考虑使用 任务并行库For jobs that do have a side effect, consider using the Task Parallel Library.

更多资源More resources

  • 101 LINQ 示例101 LINQ Samples
  • Linqpad,适用于 C#/F#/Visual Basic 的演练环境和数据库查询引擎Linqpad, a playground environment and Database querying engine for C#/F#/Visual Basic
  • EduLinq,帮助用户了解如何实现 LINQ 到对象的电子书EduLinq, an e-book for learning how LINQ-to-objects is implemented