你最喜欢的LINQ to Objects运算符是什么,它不是内置的?

71

使用扩展方法,我们可以编写方便的LINQ操作符来解决通用问题。

我想听听您在System.Linq命名空间中缺少哪些方法或重载以及您如何实现它们。

干净而优雅的实现,最好使用现有的方法。


看起来你目前得到的大多数实现都选择了减少开销而不是清洁和优雅,但对我个人来说,这使它们更有用。 - Roman Starkov
在这个页面上,能够折叠所有代码块将非常有用 ☺ - Timwi
在 http://extensionmethod.net/ 上有很多 VB 和 C# 的示例。 - p.campbell
3
这个问题很可能应该被锁定,就像这个链接中的问题一样:https://dev59.com/oHVD5IYBdhLWcg3wE3No - Wayne Werner
43个回答

4

ZipMerge

这是我版本的Zip,它像真正的拉链一样工作。它不会将两个值合并为一个,而是返回一个组合的IEnumerable。可以进行重载,跳过右侧和/或左侧的尾部。

public static IEnumerable<TSource> ZipMerge<TSource>(
        this IEnumerable<TSource> first,
        IEnumerable<TSource> second)
{
    using (var secondEnumerator = second.GetEnumerator())
    {
        foreach (var item in first)
        {
            yield return item;

            if (secondEnumerator.MoveNext())
                yield return secondEnumerator.Current;
        }

        while (secondEnumerator.MoveNext())
            yield return secondEnumerator.Current;
    }
}

有用,但也许应该被称为内置的Zip以外的其他名称?(我知道参数足以区分,但为了代码的可读性...) - Timwi
@Timwi 有没有其他的建议名称?也许是ZipMerge? - Nappy
这样会不会跳过 second 的第一个元素?还是我对代码理解有误? - Aistina
@Aistina:我认为你对 MoveNext 存在误解。第一次调用告诉你枚举器是否为空。Current 然后包含第一个元素。 - Nappy
1
@realbart 这不是真的:“如果 MoveNext 超出了集合的末尾,则枚举器定位在集合中的最后一个元素之后,并且 MoveNext 返回 false。当枚举器处于此位置时,对 MoveNext 的后续调用也将返回 false,直到调用 Reset。”请参阅 https://msdn.microsoft.com/library/system.collections.ienumerator.movenext.aspx - Nappy
显示剩余2条评论

4

随机样本

这是一个简单的函数,如果您有一个中等到大型的数据集(比如100个以上的项),并且想要随机抽取一部分进行观察,那么这个函数非常有用。

public static IEnumerable<T> RandomSample<T>(this IEnumerable<T> source,
                                             double percentage)
{
    source.ThrowIfNull("source");

    var r = new Random();
    return source.Where(x => (r.NextDouble() * 100.0) < percentage);
}

使用方法:

List<DataPoint> data = GetData();

// Sample roughly 3% of the data
var sample = data.RandomSample(3.0);

// Verify results were correct for this sample
foreach (DataPoint point in sample)
{
    Console.WriteLine("{0} => {1}", point, DoCalculation(point));
}

注意事项:

  1. 对于小型集合来说并不是很适用,因为返回的项目数量是随机的(在小序列上可能会轻易返回零)。
  2. 对于大型集合或数据库查询来说并不是很适用,因为它需要枚举序列中的每个项目。

有趣的是,通常更有用的是请求X个随机元素,而不是说“随机给我大约X%的元素”。为此,您应该执行以下操作:source.OrderBy(r.NextDouble()).Take(x); - mattmc3

4

窗口

枚举长度为size的数组(“窗口”),其中包含最新的值。
{ 0, 1, 2, 3 } 变成了 { [0, 1], [1, 2], [2, 3] }

例如,我使用此方法连接两个点以绘制线图。

public static IEnumerable<TSource[]> Window<TSource>(
    this IEnumerable<TSource> source)
{
    return source.Window(2);
}

public static IEnumerable<TSource[]> Window<TSource>(
    this IEnumerable<TSource> source, int size)
{
    if (size <= 0)
        throw new ArgumentOutOfRangeException("size");

    return source.Skip(size).WindowHelper(size, source.Take(size));
}

private static IEnumerable<TSource[]> WindowHelper<TSource>(
    this IEnumerable<TSource> source, int size, IEnumerable<TSource> init)
{
    Queue<TSource> q = new Queue<TSource>(init);

    yield return q.ToArray();

    foreach (var value in source)
    {
        q.Dequeue();
        q.Enqueue(value);
        yield return q.ToArray();
    }
}

4

一个,两个,多个,至少,全部

public static bool One<T>(this IEnumerable<T> enumerable)
{
    using (var enumerator = enumerable.GetEnumerator())
        return enumerator.MoveNext() && !enumerator.MoveNext();
}

public static bool Two<T>(this IEnumerable<T> enumerable)
{
    using (var enumerator = enumerable.GetEnumerator())
        return enumerator.MoveNext() && enumerator.MoveNext() && !enumerator.MoveNext();
}

public static bool MoreThanOne<T>(this IEnumerable<T> enumerable)
{
    return enumerable.Skip(1).Any();
}

public static bool AtLeast<T>(this IEnumerable<T> enumerable, int count)
{
    using (var enumerator = enumerable.GetEnumerator())
        for (var i = 0; i < count; i++)
            if (!enumerator.MoveNext())
                return false;
    return true;
}

public static bool AnyAtAll<T>(this IEnumerable<T> enumerable)
{
    return enumerable != null && enumerable.Any();
}

不要忘记在 using 语句中使用枚举器。 - Bear Monkey
我已经擅自删除了 ToEnumerable,因为它似乎与其它内容无关,并且已经在另一个答案中发布过了。 - Timwi
嗯,Timwi,如果你仔细检查了ToEnumerable的实现,你会发现它们并不相同。我的版本将任意集合转换为可枚举对象。这与其他人发布的函数不同。但是没关系...你可以随便改。 - noopman
1
+1 我喜欢这些方法。我在自己的 utils 库中定义了类似的方法。在我看来,如果你将 AnyAtAll 反转并称之为 IsNullOrEmpty,它会更容易理解。 - Bear Monkey
我有点同意,但是...AnyAtAll()是针对会在null上抛出异常的.Any()方法的。 - noopman
1
我在自己的扩展方法库中有一个非常类似的“One”函数。重载这些函数以接受投影可能也很有用,就像Any、First等函数一样。 - KeithS

3

SkipLast & TakeLast

/// <summary>
/// Enumerates the items of this collection, skipping the last
/// <paramref name="count"/> items. Note that the memory usage of this method
/// is proportional to <paramref name="count"/>, but the source collection is
/// only enumerated once, and in a lazy fashion. Also, enumerating the first
/// item will take longer than enumerating subsequent items.
/// </summary>
public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int count)
{
    if (source == null)
        throw new ArgumentNullException("source");
    if (count < 0)
        throw new ArgumentOutOfRangeException("count",
            "count cannot be negative.");
    if (count == 0)
        return source;
    return skipLastIterator(source, count);
}
private static IEnumerable<T> skipLastIterator<T>(IEnumerable<T> source,
    int count)
{
    var queue = new T[count];
    int headtail = 0; // tail while we're still collecting, both head & tail
                      // afterwards because the queue becomes completely full
    int collected = 0;

    foreach (var item in source)
    {
        if (collected < count)
        {
            queue[headtail] = item;
            headtail++;
            collected++;
        }
        else
        {
            if (headtail == count) headtail = 0;
            yield return queue[headtail];
            queue[headtail] = item;
            headtail++;
        }
    }
}

/// <summary>
/// Returns a collection containing only the last <paramref name="count"/>
/// items of the input collection. This method enumerates the entire
/// collection to the end once before returning. Note also that the memory
/// usage of this method is proportional to <paramref name="count"/>.
/// </summary>
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int count)
{
    if (source == null)
        throw new ArgumentNullException("source");
    if (count < 0)
        throw new ArgumentOutOfRangeException("count",
            "count cannot be negative.");
    if (count == 0)
        return new T[0];

    var queue = new Queue<T>(count + 1);
    foreach (var item in source)
    {
        if (queue.Count == count)
            queue.Dequeue();
        queue.Enqueue(item);
    }
    return queue.AsEnumerable();
}

我相信你实现的 TakeLast 可以用于 SkipLast,只需使用 yield return queue.Dequeue(); - Mark Hurd

3

WhereIf

WhereIf是一个可选的IEnumerableIQueryable中的筛选条件。它能够避免在构建查询谓词和lambda表达式时使用if语句。当你不知道在编译时是否应该应用过滤器时,它非常有用。

public static IEnumerable<TSource> WhereIf<TSource>(
            this IEnumerable<TSource> source, bool condition,
            Func<TSource, bool> predicate)
{
    return condition ? source.Where(predicate) : source;
}

用法:

var custs = Customers.WhereIf(someBool, x=>x.EyeColor=="Green");

ExtensionMethod.NET的LINQ WhereIf是从Andrew的博客借鉴而来。


有趣。您可以使用此功能将复选框链接到 AJAX 式搜索结果页面中的过滤器;mySearchResults.WhereIf(chkShowOnlyUnapproved.Checked, x=>!x.IsApproved) - KeithS

3

重复项

与 Ani 的 AssertCount 方法(我使用一个称为 CountAtLeast 的方法)一起使用,可以很容易地找到序列中出现多次的元素:

public static IEnumerable<T> Duplicates<T, TKey>(this IEnumerable<T> source,
    Func<T, TKey> keySelector = null, IEqualityComparer<TKey> comparer = null)
{
    source.ThrowIfNull("source");
    keySelector = keySelector ?? new Func<T, TKey>(x => x);
    comparer = comparer ?? EqualityComparer<TKey>.Default;

    return source.GroupBy(keySelector, comparer)
        .Where(g => g.CountAtLeast(2))
        .SelectMany(g => g);
}

我认为你可以在“内置”LINQ中将g.CountAtLeast(2)写成g.Skip(1).Any() - Timwi
@Timwi:这正是我写的方式;)我使用的几个扩展方法实际上只是非常薄的包装器,用于已经可以简洁编写的功能(另一个例子:SkipNulls(),它只是 Where(x => x != null))。我使用它们并不是因为它们做了很多额外的工作,而是因为我发现它们使代码更易读(在适当的情况下将几个方法调用包装成一个以便于代码重用也不错)。 - Dan Tao
Dan: SkipNulls<T>() 实际上就是 OfType<T>() - Gabe
@Gabe:你可以使用 OfType<T>Where<T>,两种方式都只是一个微不足道的包装器。我的意思只是 SkipNulls 这个名称让它更有目的性。 - Dan Tao

3

使用初始容量的ToList和ToDictionary

ToList和ToDictionary可以暴露基础集合类的初始容量重载。当源长度已知或有限时,偶尔会有用。

public static List<TSource> ToList<TSource>(
    this IEnumerable<TSource> source, 
    int capacity)
{
    if (source == null)
    {
        throw new ArgumentNullException("source");
    }
    var list = new List<TSource>(capacity);
    list.AddRange(source);
    return list;
}     

public static Dictionary<TKey, TSource> ToDictionary<TSource, TKey>(
    this IEnumerable<TSource> source, 
    Func<TSource, TKey> keySelector, 
    int capacity,
    IEqualityComparer<TKey> comparer = null)
{
    return source.ToDictionary<TSource, TKey, TSource>(
                  keySelector, x => x, capacity, comparer);
}

public static Dictionary<TKey, TElement> ToDictionary<TSource, TKey, TElement>(
    this IEnumerable<TSource> source, 
    Func<TSource, TKey> keySelector, 
    Func<TSource, TElement> elementSelector,
    int capacity,
    IEqualityComparer<TKey> comparer = null)
{
    if (source == null)
    {
        throw new ArgumentNullException("source");
    }
    if (keySelector == null)
    {
        throw new ArgumentNullException("keySelector");
    }
    if (elementSelector == null)
    {
        throw new ArgumentNullException("elementSelector");
    }
    var dictionary = new Dictionary<TKey, TElement>(capacity, comparer);
    foreach (TSource local in source)
    {
        dictionary.Add(keySelector(local), elementSelector(local));
    }
    return dictionary;
}

2

CountUpTo

static int CountUpTo<T>(this IEnumerable<T> source, int maxCount)
{
    if (maxCount == 0)
        return 0;

    var genericCollection = source as ICollection<T>; 
    if (genericCollection != null) 
        return Math.Min(maxCount, genericCollection.Count);

    var collection = source as ICollection; 
    if (collection != null)
        return Math.Min(maxCount, collection.Count);

    int count = 0;
    foreach (T item in source)
        if (++count >= maxCount)
            break;
    return count;
}

1
这在语义上与 collection.Take(maxCount).Count() 是一样的,对吗? - Timwi

2

Coalesce

public static T Coalesce<T>(this IEnumerable<T> items) {
   return items.Where(x => x != null && !x.Equals(default(T))).FirstOrDefault();
   // return items.OfType<T>().FirstOrDefault(); // Gabe's take
}

既然你真的在使用它,让我为你简化一下。 - Gabe
不,谢谢。因为你的版本只适用于对象类型(而非DateTime等),而且你使用OfType<T>的方式并不典型,因为大多数人都将其视为一种强制转换方法而非非空过滤器,并且它在可读性或性能方面没有任何增益,所以我会坚持原来的版本。 - mattmc3
但是 OfType<T> 能够处理 DateTime,不是吗?此外,我认为 OfType<T> 是一种非空过滤方法,否则它怎么能工作呢?也许只有我和 Gabe 这样想... - Bear Monkey
OfType 可以与值类型一起使用,当然。但是 Gabe 的用法改变了 Coalesce 方法,因此对于值类型来说,它不能按照我想要的方式工作。至于 OfType,我认为它更多地用在异构或多态集合中,其中你希望按类型进行过滤 (http://msdn.microsoft.com/en-us/library/bb360913.aspx)。MSDN 文章甚至没有提到过滤空值。 - mattmc3
在从VB转换到C#时犯了一个经典错误... Nothing不等同于null... 必须修改C#实现。 - mattmc3
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接