C#或滑动窗口枚举器中的成对迭代

f3l*_*lix 48 .net c# ienumerable iterator

如果我有一个IEnumerable像:

string[] items = new string[] { "a", "b", "c", "d" };
Run Code Online (Sandbox Code Playgroud)

我想循环通过所有连续项目(大小为2的滑动窗口).这将是

("a","b"), ("b", "c"), ("c", "d")
Run Code Online (Sandbox Code Playgroud)

我的解决方案就是这样

    public static IEnumerable<Pair<T, T>> Pairs(IEnumerable<T> enumerable) {
        IEnumerator<T> e = enumerable.GetEnumerator(); e.MoveNext();
        T current = e.Current;
        while ( e.MoveNext() ) {
            T next = e.Current;
            yield return new Pair<T, T>(current, next);
            current = next;
        }
    }

 // used like this :
 foreach (Pair<String,String> pair in IterTools<String>.Pairs(items)) {
    System.Out.PrintLine("{0}, {1}", pair.First, pair.Second)
 }
Run Code Online (Sandbox Code Playgroud)

当我编写这段代码时,我想知道.NET框架中是否已经存在执行相同操作的函数,并且它不仅适用于对,而且适用于任何大小的元组.恕我直言应该有一个很好的方法来做这种滑动窗口操作.

我使用C#2.0,我可以想象使用C#3.0(w/LINQ)有更多(更好)的方法来做到这一点,但我主要对C#2.0解决方案感兴趣.不过,我也很欣赏C#3.0解决方案.

Ian*_*cer 50

In .NET 4 this becomes even easier:-

var input = new[] { "a", "b", "c", "d", "e", "f" };
var result = input.Zip(input.Skip(1), (a, b) => Tuple.Create(a, b));
Run Code Online (Sandbox Code Playgroud)

  • 值得一提的是,这会对"输入"两次进行评估 - 对于数组来说不是问题,但如果它被懒惰地评估可能会很昂贵. (19认同)
  • 另外,`Zip`的第二个参数可以作为方法组传递:`... input.Zip(input.Skip(1),Tuple.Create);` (6认同)
  • 我只是在单元测试中做到这一点,只是为了看到差异.使用`Enumerable.Range(0,count)`作为迭代器,我必须在延迟明显之前将计数增加到~100万,并且在它足够慢以至于打扰我之前需要大约1000万.尽管如此,@ dahlbyk的解决方案仍然优雅地避免了这一点,所以我会在任何一天使用它.(扩展方法的全部*点*是能够隐藏不可读的代码,因此这里的优先级应该是直截了当的......). (3认同)
  • @Jay,自 .NET Core 3.0(从 2019 年起)以来,有一个重载完全跳过该参数,因此“var result = input.Zip(input.Skip(1));”。 (2认同)

dah*_*byk 37

而不是需要一个元组(对)类型,为什么不接受一个选择器:

public static IEnumerable<TResult> Pairwise<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TSource, TResult> resultSelector)
{
    TSource previous = default(TSource);

    using (var it = source.GetEnumerator())
    {
        if (it.MoveNext())
            previous = it.Current;

        while (it.MoveNext())
            yield return resultSelector(previous, previous = it.Current);
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您愿意,可以跳过中间对象:

string[] items = new string[] { "a", "b", "c", "d" };
var pairs = items.Pairwise((x, y) => string.Format("{0},{1}", x, y));

foreach(var pair in pairs)
    Console.WriteLine(pair);
Run Code Online (Sandbox Code Playgroud)

或者您可以使用匿名类型:

var pairs = items.Pairwise((x, y) => new { First = x, Second = y });
Run Code Online (Sandbox Code Playgroud)

  • 是的,请参阅C#规范的[第7.4.1节](http://msdn.microsoft.com/en-us/library/aa691335%28v=vs.71%29.aspx).*"在函数成员调用的运行时处理期间,参数列表的表达式或变量引用按从左到右的顺序进行评估,如下所示:..."* (4认同)
  • 我想知道`yield return ...(previous,previous = ...)`中的执行顺序.C#语言是否保证在计算第二个参数之前准备第一个参数? (2认同)

bra*_*ing 12

最简单的方法是使用ReactiveExtensions

using System.Reactive;
using System.Reactive.Linq;
Run Code Online (Sandbox Code Playgroud)

并使自己成为一个扩展方法,以便将它们放在一起

public static IEnumerable<IList<T>> Buffer<T>(this IEnumerable<T> seq, int bufferSize, int stepSize)
{
    return seq.ToObservable().Buffer(bufferSize, stepSize).ToEnumerable();
}
Run Code Online (Sandbox Code Playgroud)

  • Rx的互动扩展伴侣(https://www.nuget.org/packages/ix-main)附带一个`IEnumerable <T>`版本的`Buffer()`:https:而不是强制进/出可观察性. //github.com/Reactive-Extensions/Rx.NET/blob/2252cb4edbb25aca12005b9a912311edd2f095f3/Ix.NET/Source/System.Interactive/EnumerableEx.Single.cs#L211-L229 (7认同)

Jam*_*ell 7

为方便起见,这里是@dahlbyk 答案的无选择器版本。

public static IEnumerable<Tuple<T, T>> Pairwise<T>(this IEnumerable<T> enumerable)
{
    var previous = default(T);

    using (var e = enumerable.GetEnumerator())
    {
        if (e.MoveNext())
            previous = e.Current;

        while (e.MoveNext())
            yield return Tuple.Create(previous, previous = e.Current);
    }
}
Run Code Online (Sandbox Code Playgroud)


Sph*_*xxx 5

派对有点晚了,但作为所有这些扩展方法的替代方法,可以使用实际的"滑动" Collection来保存(和丢弃)数据.

这是我今天最终制作的一个:

public class SlidingWindowCollection<T> : ICollection<T>
{
    private int _windowSize;
    private Queue<T> _source;

    public SlidingWindowCollection(int windowSize)
    {
        _windowSize = windowSize;
        _source = new Queue<T>(windowSize);
    }

    public void Add(T item)
    {
        if (_source.Count == _windowSize)
        {
            _source.Dequeue();
        }
        _source.Enqueue(item);
    }

    public void Clear()
    {
        _source.Clear();
    }

    ...and just keep forwarding all other ICollection<T> methods to _source.
}
Run Code Online (Sandbox Code Playgroud)

用法:

int pairSize = 2;
var slider = new SlidingWindowCollection<string>(pairSize);
foreach(var item in items)
{
    slider.Add(item);
    Console.WriteLine(string.Join(", ", slider));
}
Run Code Online (Sandbox Code Playgroud)


mqp*_*mqp 1

C# 3.0 解决方案(抱歉:)

public static IEnumerable<IEnumerable<T>> Tuples<T>(this IEnumerable<T> sequence, int nTuple)
{
    if(nTuple <= 0) throw new ArgumentOutOfRangeException("nTuple");

    for(int i = 0; i <= sequence.Count() - nTuple; i++)
        yield return sequence.Skip(i).Take(nTuple);
}
Run Code Online (Sandbox Code Playgroud)

这不是世界上性能最高的,但看起来确实令人愉悦。

实际上,使此解决方案成为 C# 3.0 解决方案的唯一因素是 .Skip.Take 构造,因此如果您只是将其更改为将该范围内的元素添加到列表中,那么它对于 2.0 来说应该是黄金。也就是说,它仍然没有性能。

  • 不是性能最好的?这是一个 O(n*n) 的实现!对于一个包含 10 项的小型列表,整个列表被遍历了 20 次 (7认同)
  • 确实如此,但它也是两行(真正的)代码,并且是一个明显简单的实现。由 OP 决定他是否需要快速解决方案 - 也许他只需要对几十个项目的列表进行此操作。 (2认同)