65.9K
CodeProject 正在变化。 阅读更多。
Home

LINQ-to-Lists

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.46/5 (7投票s)

2017 年 1 月 10 日

CPOL

6分钟阅读

viewsIcon

17702

使用此库,“from x in list select x.Property”返回另一个列表。

引言

著名的“LINQ to Objects”(Enumerable 类)允许我们查询和转换任何实现了 IEnumerable<T> 的对象,并返回另一个 IEnumerable<T>。但如果我们的输入对象是一个集合呢?难道我们不能用 LINQ 查询一个列表并返回另一个列表吗?

我的库 Loyc.EssentialsSystem.Linq.Enumerable 提供了一个补充,称为“LINQ-to-lists”,以前称为 LINQ-to-Collections(我更改了名称,因为它不专注于非列表集合,例如字典)。LinqToLists 有两个目的:

  • 提高性能,利用对象是列表的知识来优化 Enumerable 的一些方法。例如,Enumerable.Skip(IEnumerable<T>, int) 必须扫描指定数量的项才能跳过它们,而 LinqToLists.Skip(IList<T> list, int) 通过返回一个切片(子列表)来跳过它们,而无需循环。
  • 保持接口:如果输入是列表,那么输出也可以是列表。但是,LinqToLists 仅在列表大小能立即知道(无需扫描列表)的情况下才提供保持接口的方法。因此,有一个返回相同大小列表的 LinqToLists 版本的 Select,但没有 LinqToLists 版本的 Where,因为有必要扫描整个列表来确定输出大小。相反,使用常规的 .Where() 扩展方法,然后调用 .ToList() 扩展方法立即(贪婪地)从它构造一个列表,或调用 .Buffered()LinqToLists 的一个扩展方法)按需(惰性地)从它构造一个列表。

用法:添加对 Loyc.Essentials(可用作 NuGet 包)的引用,然后在源文件的顶部添加 using Loyc.Collections;

LINQ-to-Lists 支持传统的 IList<T> 和 .NET 4.5 中的新 IReadOnlyList<T>。如果您使用的是 Loyc.Essentials 的 .NET 3.5 版本,那么 IReadOnlyList<T> 定义在兼容性库 Theraot.Core.dll 中;如果您使用的是 .NET 4 版本,那么 IReadOnlyList<T> 由 Loyc.Essentials.dll 本身定义。

此外,LINQ-to-Lists 支持“neg-lists”:其最小有效索引不一定为零的列表。

返回列表的扩展方法依赖于 Loyc.Essentials 中定义的适配器结构和类,例如 ListSlice<T>SelectList<T,TResult>ReversedList<T>。为了优化性能,这些类型会直接返回,而不是返回 IList<T>IReadOnlyList<T>

LINQ-to-Lists 具有以下性能增强方法(这里只列出 IList<T> 重载,但通常也有 IReadOnlyList<T>IListSource<T> 的重载,并且通常还有 INegListSource<T>

  • FirstOrDefault<T>(this IList<T> list, T defaultValue = default(T)):返回第一个项。这比 Enumerable.FirstOrDefault 性能更好,因为它不需要创建和查询枚举器对象。另外,您可以选择默认值。
  • Last<T>(this IList<T> list):返回 list[list.Count - 1],如果列表为空,则抛出 EmptySequenceException。(注意:Enumerable.Last 会检查“此对象是否实现 IList<T>?如果是,则返回 list[list.Count - 1],但此方法避免了类型转换,并且 Enumerable.LastIReadOnlyList<T> 没有类似的优化,它会扫描整个列表来获取最后一项。)
  • LastOrDefault<T>(this IList<T> list, T defaultValue = default(T)):如果列表为空,则返回 list[list.Count - 1]defaultValue
  • Skip<T>(this IList<T> list, int start):返回一个不包含前 start 项的较小列表。这更快,因为它不扫描跳过的项。注意:此行为与 Enumerable.Skip 略有不同,因为后者在您开始枚举之前实际上什么都不做,而此方法会立即创建切片并计算其大小。此方法是另一个 Loyc.Essentials 扩展方法 Slice(list, start) 的同义词。
  • Count<T>(this IList<T> list):返回 list.Count

LINQ-to-Lists 具有以下不提高性能但保持列表接口的方法:

  • Take<T>(this IList<T> list, int count):返回一个大小限制为 count 项的列表。如果 count 大于或等于原始列表的大小,此方法会返回原始列表,该列表被包装在切片结构(ListSlice<T>)中。注意:此行为与 Enumerable.Take 略有不同,因为后者在您开始枚举之前实际上什么都不做,而此方法会立即创建切片并计算其大小。此方法是 Slice(list, 0, count) 的同义词。
  • TakeNowWhile<T>(this IList<T> list, Func<T, bool> predicate):从列表开头获取元素,直到指定的谓词返回 true,然后返回一个提供对这些元素的访问的切片。由于整个(可能很长的)操作是预先完成的,我决定将其命名为 TakeNowWhile 而不是 TakeWhile
  • SkipNowWhile<T>(this IList<T> list, Func<T, bool> predicate):从列表开头跳过元素,直到指定的谓词返回 true,然后返回一个提供对列表剩余部分的访问的切片。由于整个(可能很长的)操作是预先完成的,我决定将其命名为 SkipNowWhile 而不是 SkipWhile对于 TakeSkip,我是否也应该这样做?
  • Select<T, TResult>(this IListSource<T> source, Func<T, TResult> selector):返回列表的“投影”版本,因此每当您读取索引 i 处的项时,都会调用 selector(source[i])。与其他 LinqToLists 方法不同,此方法返回一个只读列表,但它确实实现了 IList<T>IReadOnlyList<T>
  • Reverse<T>(this IList<T> c):返回列表的反向视图(因此 Reverse(list)[i] 实际上表示 list[list.Count - 1 - i])。

有时,保持接口可以带来更高的性能;例如,使用 Enumerable 时,list.Take(N).Last() 会扫描 N 个项,而使用 Loyc.Essentials 时,它会立即返回 list[Math.Max(list.Count, N) - 1]

内部一窥

您可以在此处找到增强 C# 源代码。诚然,它并不那么简单,因为它是围绕“应该内置到 .NET 框架中但尚未内置”的更大库的一部分。它使用增强 C# 来生成类似代码的变体,并依赖于定义此处的几个适配器类型以及此处的辅助类。这些反过来可能依赖于此处的一些基类和此处的一些接口。

代码量很大,所以我们只看一个例子。TakeNowWhile 的实现如下:

public static ListSlice<T> TakeNowWhile<T>(this IList<T> list,
                                           Func<T, bool> predicate)
{
    Maybe<T> value;
    for (int i = 0;; i++) {
        if (!(value = list.TryGet(i)).HasValue)
            return new ListSlice<T>(list); // entire list
        else if (!predicate(value.Value))
            return new ListSlice<T>(list, 0, i);
    }
}

Loyc.Essentials 有许多 LinqToLists 之外的扩展方法。其中之一是 TryGet,它在这里用于在项存在时获取它。

public static Maybe<T> TryGet<T>(this IList<T> list, int index)
{
    if ((uint)index < (uint)list.Count)
        return list[index];
    return Maybe<T>.NoValue;
}

这会返回 Maybe<T>,这是 Loyc.Essentials 中的一个结构,它与标准的 Nullable<T> 几乎相同,只是 T 可以是类。因此,如果索引超出范围,TryGet(i) 会返回 NoValue(又名 default(Maybe<T>)),这使得 TakeNowWhile 决定应该返回整个列表(包装在 ListSlice 中)。

另一方面,如果 predicate(value.Value) 返回 false,则只返回列表的一部分或“切片”。ListSlice<T> 是一个结构,因此如果您使用 var 来保存结果,或者立即使用 foreach 进行迭代,则不会发生内存分配(与 System.Linq.Enumerable 相比)。

var list = new List<int> { 1, 2, 3, -4, 5 };
var slice = list.TakeNowWhile(x => x > 0); // first three items

ListSlice 看起来像这样:

public struct ListSlice<T> : IRange<T>, ICloneable<ListSlice<T>>,
     IListAndListSource<T>, ICollectionEx<T>, IArray<T>, IIsEmpty
{
    public static readonly ListSlice<T> Empty = new ListSlice<T>();

    IList<T> _list;
    int _start, _count;

    public ListSlice(IList<T> list, int start, int count = int.MaxValue)
    {
        _list = list;
        _start = start;
        _count = count;
        if (start < 0) 
            throw new ArgumentException("The start index was below zero.");
        if (count < 0)
            throw new ArgumentException("The count was below zero.");
        if (count > _list.Count - start)
            _count = System.Math.Max(_list.Count - start, 0);
    }

    public ListSlice(IList<T> list)
    {
        _list = list;
        _start = 0;
        _count = list.Count;
    }

    public int Count
    {
        get { return _count; }
    }
    public bool IsEmpty
    {
        get { return _count == 0; }
    }
    public T First
    {
        get { return this[0]; }
    }
    public T Last
    {
        get { return this[_count - 1]; }
    }

    public T this[int index]
    {
        get {
            if ((uint)index < (uint)_count)
                return _list[_start + index];
            throw new ArgumentOutOfRangeException("index");
        }
        set {
            if ((uint)index < (uint)_count)
                _list[_start + index] = value;
            throw new ArgumentOutOfRangeException("index");
        }
    }
    ...
}

如此类推,还有大约 150 行代码;Loyc 集合类和包装器不仅实现了标准的 IList<T> 接口,还实现了几个其他有用的接口,我暂时不会讨论它们,因为它们与“LINQ to lists”的概念无关。

一个有趣的注意事项是,ListSlice<T> 是可写的,所以例如,如果您编写

var list = new List<int> { 1, 2, 3, -4, 5 };
(list.Skip(2))[1] = 4;

您正在修改原始列表的索引 3。

更多扩展方法!

除了 LINQ to Lists 之外,还值得注意的是,Loyc.Essentials 在 EnumerableExt 中为普通的 IEnumerable<T> 提供了额外的扩展方法(EnumerableExt)。

招募(2017 年 1 月):我还没有时间为 LoycCore.Tests 中的这些内容编写单元测试。

© . All rights reserved.