2009-02-23 4 views
37

Wenn ich ein IEnumerable habe wie:Pair-wise Iteration in C# oder Schiebefenstern Enumerator

string[] items = new string[] { "a", "b", "c", "d" }; 

Ich möchte die alle Paare in einer Schleife durch aufeinander folgenden Artikel (Schiebefenster der Größe 2). Welche

("a","b"), ("b", "c"), ("c", "d") 

Meine Lösung wurde ist diese

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) 
} 

sein würde, wenn ich diesen Code schrieb, fragte ich mich, ob es bereits Funktionen im .NET Framework, die das gleiche tun und tun es nicht nur für Paare, aber für Tupel jeder Größe. IMHO sollte es eine gute Möglichkeit, diese Art von Schiebefenster Operationen zu tun.

Ich benutze C# 2.0 und ich kann mir vorstellen, dass es mit C# 3.0 (mit LINQ) mehr (und schönere) Möglichkeiten gibt, aber ich bin hauptsächlich an C# 2.0-Lösungen interessiert. Allerdings werde ich auch C# 3.0-Lösungen zu schätzen wissen.

+0

Dies scheint, als könnte es eine Menge Implementierung mit Jon Skeet "SmartEnumerator" teilen, die Ihnen sagt, ob ein Element das letzte in der Liste ist. http://msmvps.com/blogs/jon_skeet/archive/2007/07/27/smart-enumerations.aspx –

+0

Als Referenz wird diese Funktion in F # 'Windowed' genannt: http://stackoverflow.com/questions/8874901/ist-es-gleich-zu-dem-f-seq-windowed-in-c – Benjol

Antwort

1

C# 3.0-Lösung (sorry :)

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); 
} 

Dies ist nicht die performant in der Welt, aber es ist sicher, angenehm zu sehen.

Wirklich, das einzige, was dies zu einer C# 3.0-Lösung macht, ist das .Skip.Take-Konstrukt. Wenn Sie also nur die Elemente in diesem Bereich zu einer Liste hinzufügen, sollte es für 2.0 golden sein. Das heißt, es ist immer noch nicht performant.

+5

Nicht die performantesten? Dies ist eine O (n * n) Implementierung! Für eine kleine 10 Artikel Liste wurde die gesamte Liste 20 mal durchlaufen – configurator

+0

Das stimmt, aber es ist auch zwei Zeilen (real) Code und ist eine offensichtlich einfache Implementierung. Es liegt am OP zu entscheiden, ob er eine schnelle Lösung braucht - vielleicht braucht er diese Operation nur auf Listen von ein paar Dutzend Items. – mquander

2

auf den previous answer Aufweiten von O zu vermeiden (n) Ansatz ausdrücklich bestanden Iterator:

public static IEnumerable<IEnumerable<T>> Tuples<T>(this IEnumerable<T> input, int groupCount) { 
    if (null == input) throw new ArgumentException("input"); 
    if (groupCount < 1) throw new ArgumentException("groupCount"); 

    var e = input.GetEnumerator(); 

    bool done = false; 
    while (!done) { 
    var l = new List<T>(); 
    for (var n = 0; n < groupCount; ++n) { 
     if (!e.MoveNext()) { 
     if (n != 0) { 
      yield return l; 
     } 
     yield break; 
     } 
     l.Add(e.Current); 
    } 
    yield return l; 
    } 
} 

für C# 2, vor dem Erweiterungsmethode fallen die "this" von dem Eingabeparameter und als statische Methode aufrufen.

+0

Das Ergebnis der Anfrage wird nicht zurückgegeben. 'Enumerable.Range (1, 5) .Tuples (2)' returns '{{1, 2}, {3, 4}, {5}}' anstelle des gewünschten '{{1, 2}, {2, 3}, {3, 4}, {4, 5}} 'das ist ein gleitendes Fenster. –

0

Alternative Pairs Implementierung, indem letztes Paar vorherigen Wert speichern:

static IEnumerable<Pair<T, T>> Pairs(IEnumerable<T> collection) { 
    Pair<T, T> pair = null; 
    foreach(T item in collection) { 
    if(pair == null) 
     pair = Pair.Create(default(T), item); 
    else 
     yield return pair = Pair.Create(pair.Second, item); 
    } 
} 

Einfache Window Implementierung (nur sicher für die privaten Gebrauch, wenn der Anrufer nicht speichern zurückgegeben Arrays; siehe Hinweis):

static IEnumerable<T[]> Window(IEnumerable<T> collection, int windowSize) { 
    if(windowSize < 1) 
    yield break; 

    int index = 0; 
    T[] window = new T[windowSize]; 
    foreach(var item in collection) { 
    bool initializing = index < windowSize; 

    // Shift initialized window to accomodate new item. 
    if(!initializing) 
     Array.Copy(window, 1, window, 0, windowSize - 1); 

    // Add current item to window. 
    int itemIndex = initializing ? index : windowSize - 1; 
    window[itemIndex] = item; 

    index++; 
    bool initialized = index >= windowSize; 
    if(initialized) 
     //NOTE: For public API, should return array copy to prevent 
     // modifcation by user, or use a different type for the window. 
     yield return window; 
    } 
} 

Beispiel verwenden:

for(int i = 0; i <= items.Length; ++i) { 
    Console.WriteLine("Window size {0}:", i); 
    foreach(string[] window in IterTools<string>.Window(items, i)) 
    Console.WriteLine(string.Join(", ", window)); 
    Console.WriteLine(); 
} 
31

Rath er als erfordern ein Tupel (Paar) Typ, warum nehmen nicht nur einen Selektor:

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); 
    } 
} 

dem Sie das Zwischen Objekt überspringen können, wenn Sie wollen:

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); 

Oder Sie können einen anonymen Typ verwenden :

var pairs = items.Pairwise((x, y) => new { First = x, Second = y }); 
+1

Ich wundere mich über die Ausführungsreihenfolge in 'yield return ... (vorherige, vorherige = ...)'. Gewährleistet die C# -Sprache, dass das erste Argument vorbereitet wird, bevor das zweite Argument ausgewertet wird? – stakx

+0

Ich bin mir ziemlich sicher, dass es tut, aber ich muss die Spezifikation überprüfen, um sicher zu gehen. – dahlbyk

+2

Ja, es tut, siehe [Abschnitt 7.4.1] (http://msdn.microsoft.com/en-us/library/aa691335%28v=vs.71%29.aspx) der C# -Spezifikation. * "Während der Laufzeitverarbeitung eines Funktionsmemberaufrufs werden die Ausdrücke oder Variablenverweise einer Argumentliste in der Reihenfolge von links nach rechts wie folgt ausgewertet: ..."* –

0

die F # Seq Modul definiert die paarweise Funktion über IEnumerable<T>, aber diese Funktion ist nicht in der NET Framework.

Wenn es bereits im .NET-Framework wäre, würde es wahrscheinlich anstelle von Paaren eine Selektorfunktion akzeptieren, da Tupel in Sprachen wie C# und VB nicht unterstützt werden.

var pairs = ns.Pairwise((a, b) => new { First = a, Second = b }; 

Ich glaube nicht, eine der Antworten hier wirklich auf dem einfachen Iterator Umsetzung zu verbessern, die die meisten natural schien mir (und das Plakat dahlbyk durch die Blicke der Dinge!) Zu.

0

Etwas wie folgt aus:

public static IEnumerable<TResult> Pairwise<T, TResult>(this IEnumerable<T> enumerable, Func<T, T, TResult> selector) 
{ 
    var previous = enumerable.First(); 
    foreach (var item in enumerable.Skip(1)) 
    { 
     yield return selector(previous, item); 
     previous = item; 
    } 
} 
40

In .NET 4 wird dies noch einfacher: -

 var input = new[] { "a", "b", "c", "d", "e", "f" }; 
     var result = input.Zip(input.Skip(1), (a, b) => Tuple.Create(a, b)); 
+16

Es ist erwähnenswert, dass dies "Input" zweimal evaluiert - kein Problem für ein Array, aber wenn es lazily-evaluiert wird, könnte das teuer sein. – dahlbyk

+5

Das zweite Argument zu 'Zip' kann als Methodengruppe übergeben werden:' ... input.Zip (input.Skip (1), Tup le.Create); ' – Jay

+2

Ich habe das gerade in einem Komponententest gemacht, nur um den Unterschied zu sehen. Mit 'Enumerable.Range (0, count)' als Iterator musste ich die Anzahl auf ~ 1 Million erhöhen, bevor die Verzögerung bemerkbar war, und ~ 10 Millionen, bevor sie langsam genug war, um mich zu stören. Dennoch, @dahlbyks Lösung vermeidet dies elegant, also würde ich das jeden Tag verwenden. (Der ganze * Punkt * der Erweiterungsmethoden besteht darin, den nicht extrem lesbaren Code aus dem Blickfeld zu verbergen, so dass die Prioritäten hier einfach sein sollten ...). –

9

Der einfachste Weg ist ReactiveExtensions zu verwenden

using System.Reactive; 
using System.Reactive.Linq; 

und machen Sie sich ein Erweiterungsmethode zu Kit bash dies zusammen

public static IEnumerable<IList<T>> Buffer<T>(this IEnumerable<T> seq, int bufferSize, int stepSize) 
{ 
    return seq.ToObservable().Buffer(bufferSize, stepSize).ToEnumerable(); 
} 
+4

Anstatt zu/von Observable zu zwingen, wird der Complex von Interactive Extensions zu Rx (https://www.nuget.org/packages/ix-main) mit einer 'IEnumerable ' Version von 'Buffer()' ausgeliefert: https://github.com/Reactive-Extensions/Rx.NET/blob/2252cb4edbb25aca12005b9a912311edd2f095f3/Ix.NET/Source/System.Interactive/EnumerableEx.Single.cs#L211-L229 – dahlbyk

0

Nur für die Bequemlichkeit, hier ist eine Auswahlversion von @ dahlbyk's Antwort.

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); 
    } 
} 
3

ein wenig zu spät an die Partei, aber als Alternative zu all diesen Erweiterungsmethoden, könnte man eine tatsächliche „Sliding“ verwenden Collection (und verworfen), um die Daten zu halten.

ist hier, den ich heute machen endete:

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. 
} 

Verbrauch:

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

Hier ist meine Lösung, die einen Stapel verwenden. Es ist kurz und prägnant.

string[] items = new string[] { "a", "b", "c", "d" }; 

Stack<string> stack = new Stack<string>(items.Reverse()); 

while(stack.Count > 1) 
{ 
    Console.WriteLine("{0},{1}", stack.Pop(), stack.Peek()); 
}