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

2

EnumerableEx.OfThese:

public static class EnumerableEx
{
    public static IEnumerable<T> OfThese<T>(params T[] objects)
    {
        return objects;
    }
}

为了轻松构建已知值的序列:
var nums=EnumerableEx.OfThese(1,2,3);
var nums2 = nums.Concat(EnumerableEx.OfThese(4));

这是一个好主意,但这个扩展方法比简单写法更长,也不够清晰 -> var nums = new int[] { 1, 2, 3}; - Bear Monkey
我认为当你只需要“一个序列”,而不关心它是数组、列表还是其他什么时,实际上更清晰。 - Nevermind
1
我见过的最好的identity函数使用! - Gabe

1

OneOrDefault

类似于 SingleOrDefault,但是当列表中有多个元素时,它返回 null 而不是抛出异常。

public static T OneOrDefault<T>(this IEnumerable<T> list)
{
    using (var e = list.GetEnumerator())
    {
        if (!e.MoveNext())
            return default(T);
        T val = e.Current;
        if (e.MoveNext())
            return default(T);
        return val;
    }
}

这不就是和FirstOrDefault()一样的吗?http://msdn.microsoft.com/en-us/library/bb358452.aspx - M4N
M4N:不,FirstOrDefault 不会再调用 MoveNext 一次。 - Gabe
@Gabe,第二次调用或不调用“MoveNext()”有什么意义?你能告诉我们“OneOrDefault”的实际用例吗?谢谢。 - Stéphane Gourichon
1
如果你没有第二个 MoveNext,你会得到 FirstOrDefault。如果你有第二个 MoveNext 并且在返回 true 时抛出异常,你会得到 SingleOrDefault。如果你有第二个并且返回默认值,你会得到 OneOrDefault - Gabe

1

GetBreadthFirstEnumerable

这是我最喜欢的方法 - 可以轻松地对任何集合进行广度优先搜索。在每次迭代中,该方法会返回当前节点、其父节点、所处图中的层级以及其在广度优先顺序中的索引。在某些场景中,这非常有帮助,但如果你不需要这些信息,该方法也可以大大简化,你可以选择只返回节点本身。

public class IteratedNode<T>
{
    public T Node;
    public T ParentNode;
    public int Level;
    public int Index;
}

/// <summary>
/// Iterates over a tree/graph via In Order Breadth First search.
/// </summary>
/// <param name="root">The root item.</param>
/// <param name="childSelector">A func that receives a node in the tree and returns its children.</param>
public static IEnumerable<IteratedNode<T>> GetBreadthFirstEnumerable<T>(this T root, Func<T, IEnumerable<T>> childSelector)
{
    var rootNode = new IteratedNode<T> { Node = root, ParentNode = default(T), Level = 1, Index = 1};
    var nodesToProcess = new Queue<IteratedNode<T>>( new[] {rootNode});

    int itemsIterated = 0;
    while (nodesToProcess.Count > 0)
    {
        IteratedNode<T> currentItem = nodesToProcess.Dequeue();

        yield return currentItem; itemsIterated++;

        // Iterate over the children of this node, and add it to queue, to process later.
        foreach (T child in childSelector(currentItem.Node))
        {
            nodesToProcess.Enqueue( 
                new IteratedNode<T> {
                    Node = child,
                    ParentNode = currentItem.Node,
                    Level = currentItem.Level + 1,
                    Index = itemsIterated
                });                      
        }
    }
}

1

带参数列表的例外

这是一个非常简单的包装器,可以使某些查询变得更加容易:

public static IEnumerable<T> Except<T>(this IEnumerable<T> elements, params T[] exceptions)
{
    return elements.Except(exceptions);
}

使用方法:

//returns a list of "work week" DayOfWeek values.
Enum.GetValues(typeof(DayOfWeek)).Except(DayOfWeek.Saturday, DayOfWeek.Sunday);

这样可以避免创建自己的集合来传递到Except中,或者使用Where和大型布尔“不等于A且不等于B”的构造,使代码更加简洁。

带有谓词的Except(反向Where)

我还看到过给Except一个谓词的情况,基本上是将其变成Where的反向操作,最简单的实现方式就是:

public static IEnumerable<T> Except<T>(this IEnumerable<T> elements, Predicate<T> predicate)
{
   return elements.Where(x=>!predicate(x));
}

使用方法:

//best use is as a "natural language" statement already containing Where()
ListOfRecords.Where(r=>r.SomeValue = 123).Except(r=>r.Id = 2536);

如果C#同时具有do-while和do-until结构,那么它们各自都有其用处。

1

ObjectWithMin/ObjectWithMax

类似于Timwi的MinElement,但是有一种更简洁的算法,使用Aggregate扩展方法来遍历源Enumerable:

public static T ObjectWithMax<T, TResult>(this IEnumerable<T> elements, Func<T, TResult> projection)
    where TResult : IComparable<TResult>
{
    if (elements == null) throw new ArgumentNullException("elements", "Sequence is null.");
    if (!elements.Any()) throw new ArgumentException("Sequence contains no elements.");

    //Set up the "seed" (current known maximum) to the first element
    var seed = elements.Select(t => new {Object = t, Projection = projection(t)}).First();

    //run through all other elements of the source, comparing the projection of each
    //to the current "seed" and replacing the seed as necessary. Last element wins ties.
    return elements.Skip(1).Aggregate(seed,
                              (s, x) =>
                              projection(x).CompareTo(s.Projection) >= 0
                                  ? new {Object = x, Projection = projection(x)}
                                  : s
        ).Object;
}

public static T ObjectWithMin<T, TResult>(this IEnumerable<T> elements, Func<T, TResult> projection)
    where TResult : IComparable<TResult>
{
    if (elements == null) throw new ArgumentNullException("elements", "Sequence is null.");
    if (!elements.Any()) throw new ArgumentException("Sequence contains no elements.");

    var seed = elements.Select(t => new {Object = t, Projection = projection(t)}).First();

    //ties won by the FIRST element in the Enumerable
    return elements.Aggregate(seed,
                              (s, x) =>
                              projection(x).CompareTo(s.Projection) < 0
                                  ? new {Object = x, Projection = projection(x)}
                                  : s
        ).Object;
}

相较于 OrderBy().First() 或者 Where(x=>x.SomeField == source.Min(someField)).First(),我发现这些方法是非常宝贵的。它是线性的,只需要遍历一次 Enumerable,并且与列表当前顺序保持稳定。


为什么领带之间存在不对称性? - Stéphane Gourichon
@StéphaneGourichon - 很好的问题。我这样做是为了使方法的行为类似于稳定排序。在这种排序中,相对顺序是决定因素;当两个元素比较为“相等”时,先出现的被认为是“小于”后出现的。这种稳定性通常被认为是排序算法的优点。我的上述方法以类似的方式尊重元素的相对顺序;最小值的第一次出现被视为最小,最大值的最后一次出现被视为最大。 - KeithS

0

分区

根据条件将IEnumerable拆分为两个IList

public static void Partition<T>(this IEnumerable<T> source, Func<T, bool> predicate, ref IList<T> matches, ref IList<T> nonMatches)
{
    if (source == null)
        throw new ArgumentNullException(nameof(source));
    if (predicate == null)
        throw new ArgumentNullException(nameof(source));

    var _matches = new List<T>();
    var _nonMatches = new List<T>();
    foreach (var itm in source)
    {
        if (predicate(itm))
            _matches.Add(itm);
        else
            _nonMatches.Add(itm);
    }

    if (matches == null)
        matches = new List<T>();
    else
        matches.Clear();
    if (nonMatches == null)
        nonMatches = new List<T>();
    else
        nonMatches.Clear();

    foreach (var m in _matches)
        matches.Add(m);
    nonMatches.Clear();
    foreach (var m in _nonMatches)
        nonMatches.Add(m);
    nonMatches = _nonMatches;
}

0

WhereLike

Where 与 SQL 的 LIKE 模式匹配相结合。

public static IEnumerable<TSource> WhereLike<TSource>(this IEnumerable<TSource> source, Func<TSource, string> selector, string match)
    {
        /* Turn "off" all regular expression related syntax in
            * the pattern string. */
        string pattern = Regex.Escape(match);

        /* Replace the SQL LIKE wildcard metacharacters with the
        * equivalent regular expression metacharacters. */
        pattern = pattern.Replace("%", ".*?").Replace("_", ".");

        /* The previous call to Regex.Escape actually turned off
        * too many metacharacters, i.e. those which are recognized by
        * both the regular expression engine and the SQL LIKE
        * statement ([...] and [^...]). Those metacharacters have
        * to be manually unescaped here. */
        pattern = pattern.Replace(@"\[", "[").Replace(@"\]", "]").Replace(@"\^", "^");
        Regex reg = new Regex(pattern, RegexOptions.IgnoreCase);

        return source.Where(t => reg.IsMatch(selector(t)));
    }

0

Between

C#中类似于广为人知的SQL "between"结构的等效语句

/// <summary>
/// Determines if the current value is included in the range of specified values. Bounds are included.
/// </summary>
/// <typeparam name="T">The type of the values</typeparam>
/// <param name="val">The value.</param>
/// <param name="firstValue">The lower bound.</param>
/// <param name="secondValue">The upper bound.</param>
/// <returns>
/// Return <c>true</c> if the <paramref name="val">value</paramref> is between the <paramref name="firstValue"/> and the <paramref name="secondValue"/>; otherwise, <c>false</c>
/// </returns>
public static bool Between<T>(this T val, T firstValue, T secondValue) where T : IComparable<T>
{
  if (val == null)
    throw new ArgumentNullException();

  if (firstValue == null ||
      secondValue == null)
    return false;

  return firstValue.CompareTo(val) <= 0 && secondValue.CompareTo(val) >= 0;
}

与问题无关,该问题涉及 IEnumerable<T> - Timwi
1
虽然它不是严格的主题,但由于LINQ to Object旨在缩小SQL语法和C#代码之间的差距,我认为这个Between运算符在大局中非常合适。 - Johann Blais

0
public static void AddRangeWhere<T>(this IEnumerable<T> list, IEnumerable<T> sourceList, Func<T, bool> predicate)
  {
   if (list != null)
   {
    List<T> newRange = new List<T>();

    foreach (var item in sourceList)
    {
     if (predicate(item))
      newRange.Add(item);
    }

    list.Concat(newRange);
   }
  }

0
我的是RemoveConcurrent<T>():

public static IEnumerable<T> RemoveConcurrent<T>(this IEnumerable<T> ts) where T : IEquatable<T>
{
    bool first = true;
    T lval = default(T);
    foreach (T t in ts)
    {
        if (first || !t.Equals(lval))
        {
            first = false;
            yield return t;
        }
        lval = t;
    }
}

也许可以使用标准的LINQ查询来完成这个任务,但如果有的话,我不知道怎么做。


在 System.Interactive (Ix) / System.Reactive (Rx) 中,这个操作符被称为“DistinctUntilChanged”,这可能是一个更直观的名称,但它确实是一个非常有用的操作符。 - Nappy

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