你最喜欢的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个回答

33

添加和前置

(自本回答撰写以来,这些已被添加到.NET中。)

/// <summary>Adds a single element to the end of an IEnumerable.</summary>
/// <typeparam name="T">Type of enumerable to return.</typeparam>
/// <returns>IEnumerable containing all the input elements, followed by the
/// specified additional element.</returns>
public static IEnumerable<T> Append<T>(this IEnumerable<T> source, T element)
{
    if (source == null)
        throw new ArgumentNullException("source");
    return concatIterator(element, source, false);
}

/// <summary>Adds a single element to the start of an IEnumerable.</summary>
/// <typeparam name="T">Type of enumerable to return.</typeparam>
/// <returns>IEnumerable containing the specified additional element, followed by
/// all the input elements.</returns>
public static IEnumerable<T> Prepend<T>(this IEnumerable<T> tail, T head)
{
    if (tail == null)
        throw new ArgumentNullException("tail");
    return concatIterator(head, tail, true);
}

private static IEnumerable<T> concatIterator<T>(T extraElement,
    IEnumerable<T> source, bool insertAtStart)
{
    if (insertAtStart)
        yield return extraElement;
    foreach (var e in source)
        yield return e;
    if (!insertAtStart)
        yield return extraElement;
}

@Lasse V. Karlsen:我已经将一个InsertBetween方法作为单独的答案发布 - Timwi
7
你可以将Append<T>的实现缩短为一行:return source.Concat(Enumerable.Repeat(element, 1)); - Steven
16
Append和Prepend也可以使用AsEnumerable实现:head.AsEnumerable().Concat(source) / source.Concat(element.AsEnumerable()) - Nappy
这意味着确切的含义。例如,在ASP.NET中,如果您想将IEnumerable存储在Session对象中,并且您正在使用除InProc之外的其他机制(如StateServer或SQLServer),则您的IEnumerable必须是可序列化的。有些人没有意识到这一点,如果他们不知道这一点,可能会出现问题。 - Carlos Muñoz
2
不错的代码 +1,但我会将它从 T 更改为 params T[],这样你就可以在末尾添加一个或多个项目。 - user1228
显示剩余7条评论

21
我很惊讶还没有人提到过MoreLINQ项目。它是由Jon Skeet发起的,并且已经吸引了一些开发者。根据该项目页面上的介绍:

LINQ to Objects缺少一些理想的特性。

此项目将以符合LINQ精神的方式增强LINQ to Objects并添加额外的方法。

请查看运算符概述维基页面,了解实现的操作符列表。

这肯定是从一些清晰优雅的源代码中学习的好方式。


16

每个

对于纯粹主义者来说,可能没有什么作用,但它非常有用!

 public static void Each<T>(this IEnumerable<T> items, Action<T> action)
 {
   foreach (var i in items)
      action(i);
 }

3
Parallel.ForEach可以做到相同的并行执行,对吧? - abhishek
1
一个以函数而不是动作为参数重载,并通过yield return返回结果的方法会更加明显。 - Nappy
25
那个被称为 Select,它是内置的。 - Timwi
1
它是Reactive Extensions for .NET (Rx)的System.Interactive.dll的一部分,称为Do:“在序列中对每个值调用动作以产生其副作用。” - Nappy
2
@Nappy:Do方法不等同于示例中的方法;它必须跟随Run(),后者还有一个重载,接受一个Action<T>。后者将是该示例的等效方法。 - Markus Johnsson
显示剩余7条评论

14

队列和栈

/// <summary>Creates a <see cref="Queue&lt;T&gt;"/> from an enumerable
/// collection.</summary>
public static Queue<T> ToQueue<T>(this IEnumerable<T> source)
{
    if (source == null)
        throw new ArgumentNullException("source");
    return new Queue<T>(source);
}

/// <summary>Creates a <see cref="Stack&lt;T&gt;"/> from an enumerable
/// collection.</summary>
public static Stack<T> ToStack<T>(this IEnumerable<T> source)
{
    if (source == null)
        throw new ArgumentNullException("source");
    return new Stack<T>(source);
}

仅使用 var myQueue = new Queue<ObjectType>(myObj); 有什么问题吗?对于仅一行的代码,这并不是一个值得的扩展... - cjk
2
@ck:你可以将相同的逻辑应用于内置扩展ToList(),这些扩展也很好地补充了ToArray()扩展。我更喜欢流畅的var myQueue = a.SelectMany(...).Where(...).OrderBy(...).ToQueue()而不是更传统的语法。 - Martin Liversage
1
@Martin(&TimwI)- 我可以理解当链式连接大量运算符时,那样做会更加整洁。+1。 - cjk
2
@cjk 我看到的最大优势是不需要指定类型参数。如果编译器能够推断出来,我就不想在那里写 <ObjectType> - nawfal

14

IsEmpty

public static bool IsEmpty<T>(this IEnumerable<T> source)
{
    return !source.Any();
}

7
我不确定为什么这个帖子会被踩。对我来说,source.IsEmpty() 比 !source.Any() 更容易阅读。我总是尽可能避免使用 ! 运算符,因为在快速浏览代码时很容易忽略它。 - Bear Monkey
3
"None"更类似于"Any",而不是"IsEmpty"。 - nawfal

13

In和NotIn

这是两个与SQL相关的C#语言结构

/// <summary>
/// Determines if the source value is contained in the list of possible values.
/// </summary>
/// <typeparam name="T">The type of the objects</typeparam>
/// <param name="value">The source value</param>
/// <param name="values">The list of possible values</param>
/// <returns>
///     <c>true</c> if the source value matches at least one of the possible values; otherwise, <c>false</c>.
/// </returns>
public static bool In<T>(this T value, params T[] values)
{
    if (values == null)
        return false;

    if (values.Contains<T>(value))
        return true;

    return false;
}

/// <summary>
/// Determines if the source value is contained in the list of possible values.
/// </summary>
/// <typeparam name="T">The type of the objects</typeparam>
/// <param name="value">The source value</param>
/// <param name="values">The list of possible values</param>
/// <returns>
///     <c>true</c> if the source value matches at least one of the possible values; otherwise, <c>false</c>.
/// </returns>
public static bool In<T>(this T value, IEnumerable<T> values)
{
    if (values == null)
        return false;

    if (values.Contains<T>(value))
        return true;

    return false;
}

/// <summary>
/// Determines if the source value is not contained in the list of possible values.
/// </summary>
/// <typeparam name="T">The type of the objects</typeparam>
/// <param name="value">The source value</param>
/// <param name="values">The list of possible values</param>
/// <returns>
///     <c>false</c> if the source value matches at least one of the possible values; otherwise, <c>true</c>.
/// </returns>
public static bool NotIn<T>(this T value, params T[] values)
{
    return In(value, values) == false;
}

/// <summary>
/// Determines if the source value is not contained in the list of possible values.
/// </summary>
/// <typeparam name="T">The type of the objects</typeparam>
/// <param name="value">The source value</param>
/// <param name="values">The list of possible values</param>
/// <returns>
///     <c>false</c> if the source value matches at least one of the possible values; otherwise, <c>true</c>.
/// </returns>
public static bool NotIn<T>(this T value, IEnumerable<T> values)
{
    return In(value, values) == false;
}

我认为应该抛出异常,而不是使用 if (values == null) return false;。默默地忽略错误条件从来都不是好的做法。 - Timwi
这取决于你的看法。事实是,一个元素永远不会包含在一个空值列表中。 - Johann Blais
一个东西,有助于DRY但会增加调用栈;params数组作为数组是IEnumerable,因此你的params重载可以简单地调用IEnumerable重载。 - KeithS
1
只需要 return values.Contains(value); 即可。 - nawfal

9

JoinString

这个方法与string.Join基本相同,但有以下不同:

  • 可以在任何集合上使用,而不仅仅是字符串集合(对每个元素调用ToString

  • 可以为每个字符串添加前缀和后缀。

  • 作为扩展方法。我发现string.Join很烦人,因为它是静态的,在一系列操作中它在词法上不是正确的顺序。


/// <summary>
/// Turns all elements in the enumerable to strings and joins them using the
/// specified string as the separator and the specified prefix and suffix for
/// each string.
/// <example>
///   <code>
///     var a = (new[] { "Paris", "London", "Tokyo" }).JoinString(", ", "[", "]");
///     // a contains "[Paris], [London], [Tokyo]"
///   </code>
/// </example>
/// </summary>
public static string JoinString<T>(this IEnumerable<T> values,
    string separator = null, string prefix = null, string suffix = null)
{
    if (values == null)
        throw new ArgumentNullException("values");

    using (var enumerator = values.GetEnumerator())
    {
        if (!enumerator.MoveNext())
            return "";
        StringBuilder sb = new StringBuilder();
        sb.Append(prefix).Append(enumerator.Current.ToString()).Append(suffix);
        while (enumerator.MoveNext())
            sb.Append(separator).Append(prefix)
              .Append(enumerator.Current.ToString()).Append(suffix);
        return sb.ToString();
    }
}

1
非常有用,尽管个人认为应该删除所有格式化代码,只保留使用分隔符连接IEnumerable<string>的能力。在调用此方法之前,您始终可以将数据投影到IEnumerable<string>中。 - Christian Hayter
@Timwi:几个小点需要注意:您在追加到 StringBuilder 之前不需要进行 null 检查。您需要处理枚举器的释放。您可以摆脱循环之上的 Append 调用,并将循环替换为 do-while。最后,除非您想避免创建 StringBuilder 的成本,否则您不需要将第一个元素视为特殊情况:new StringBuilder().ToString() 将返回 string.Empty - Ani
@Ani:谢谢,我已经进行了大部分修复。我更喜欢库代码在紧密循环中尽可能快,所以我会保留特殊情况和while之前的附加操作。 - Timwi
4
在.NET 4中,String.Join接受一个IEnumerable<T>。 - Ian Mercer
1
这个做的事情是否有Aggregate操作符所没有的功能?这是一个不常用的方法,但绝对可以用于连接对象列表。 - Kirk Broadhurst
1
@Kirk Broadhurst:首先,如果您在每个阶段连接字符串而不是使用StringBuilder,则Aggregate会变慢。但是,即使我想在这种情况下使用Aggregate,我仍然会将其包装到具有此签名的JoinString方法中,因为它可以使使用它的代码更清晰。一旦我拥有了它,我也可以通过使用StringBuilder编写更快的方法。 - Timwi

9

AsIEnumerable

/// <summary>
/// Returns a sequence containing one element.
/// </summary>
public static IEnumerable<T> AsIEnumerable<T>(this T obj)
{
    yield return obj;
}  

使用方法:

var nums = new[] {12, 20, 6};
var numsWith5Prepended = 5.AsIEnumerable().Concat(nums);   

2
我更喜欢编写 EnumerableEx.Return(5).Concat(nums) 而不是让 IntelliSense 膨胀任何对象。 - Nappy
2
我更喜欢使用AppendPrepend来实现这个功能。 - Timwi
14
出于性能考虑,我建议使用return new T[] { obj };。这样,编译器不必构造一个完整的状态机类来产生一个值。 - Christian Hayter
4
我觉得这个实现很危险。你对 new[]{1, 2, 3, 4}.AsIEnumerable() 的期望是什么?我期望的结果是 1,2,3,4,而不是 [1,2,3,4]。 - larsmoa
1
@larsm:这就是为什么大多数库使用 EnumerableEx.Return(new[]{1, 2, 3, 4})。你是正确的,“As” 意味着有一些强制转换正在进行,因为数组已经实现了 IEnumerable,所以你期望什么都不会改变。 - Nappy
显示剩余5条评论

8

订单

/// <summary>Sorts the elements of a sequence in ascending order.</summary>
public static IEnumerable<T> Order<T>(this IEnumerable<T> source)
{
    return source.OrderBy(x => x);
}

8
我更愿意把这个方法称为“排序”。 - Steven
4
@Steven说:“Sort”会与List<T>.Sort()造成歧义。 - Ani
1
现在不会了,因为C#编译器总是优先选择实例方法而非扩展方法。 - Steven
4
@Steven: 是的,但是无论如何,对于阅读代码的人来说,这仍然是不明确的。区别很重要,因为 List<T>.Sort 是原地排序。 - Timwi
1
这不需要一个泛型类型约束吗?要求 T 实现 IComparable 接口吗? - KeithS
显示剩余3条评论

8

洗牌

public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> items)
{
    var random = new Random();
    return items.OrderBy(x => random.Next());
}

编辑:似乎上述实现存在几个问题。这是一个基于@LukeH的代码和@ck和@Strilanc的评论改进版本。

private static Random _rand = new Random();
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source)
{
    var items = source == null ? new T[] { } : source.ToArray();
    var count = items.Length;
    while(count > 0)
    {
        int toReturn = _rand.Next(0, count);
        yield return items[toReturn];
        items[toReturn] = items[count - 1];
        count--;
    }
}

我建议将其更名为Randomize或Shuffle。 - Nappy
如果你要在你的库中包含一个Shuffle方法,那么最好让它尽可能高效。使用OrderBy是O(n log n),而Fisher-Yates-Durstenfeld shuffle将是O(n)。这里有一个例子:https://dev59.com/z3I-5IYBdhLWcg3w1sPi#1653204 - LukeH
2
你的实现是错误的。更糟糕的是,它是难以察觉的错误。首先,它有一个隐藏的全局依赖:随机源(更糟糕的是,如果快速多次调用,你选择的源将给出相同的洗牌!)。其次,使用的算法很差。它不仅在渐近意义下比费舍尔-耶茨慢,而且不均匀(分配相同键的元素保持相同的相对顺序)。 - Craig Gidney
2
我可能会将随机源作为参数添加。这样做有两个好处:一方面可以避免在启动时创建多个随机源,或者至少可以提高开发人员正确初始化它们的反应速度;另一方面,如果方法每次调用都返回不同/随机的结果,那么这也是一个良好的指标。 - Nappy
1
@Nappy 很不幸,.NET 中大多数随机源的外部接口并不相同;与 System.Security.Cryptography.RandomNumberGenerator 的实现相比较,可以看出 Math.Random 的区别。你可以编写适配器和/或接受一个简单的 Func<int>,但必须有人来简化方法获取其伪随机数的方式。 - KeithS
显示剩余4条评论

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