LINQ-to-Lists
使用此库,“from x in list select x.Property”返回另一个列表。
引言
著名的“LINQ to Objects”(Enumerable 类)允许我们查询和转换任何实现了 IEnumerable<T>
的对象,并返回另一个 IEnumerable<T>
。但如果我们的输入对象是一个集合呢?难道我们不能用 LINQ 查询一个列表并返回另一个列表吗?
我的库 Loyc.Essentials 为 System.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.Last
对IReadOnlyList<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
。对于Take
和Skip
,我是否也应该这样做?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 中的这些内容编写单元测试。