2013-07-05 16 views
6

raubend Was ich tun möchte, kurze Version:eine IOrderedEnumerable Graf ohne es

var source = new[]{2,4,6,1,9}.OrderBy(x=>x); 
int count = source.Count; // <-- get the number of elements without performing the sort 

Lange Version:

Um die Anzahl der Elemente in einem IEnumerable, es festzustellen, ist notwendig, um über alle Elemente zu iterieren. Dies könnte möglicherweise eine sehr teure Operation sein.

Wenn die IEnumerable kann ICollection gegossen werden, dann kann die Zählung schnell ohne Iteration bestimmt werden. Die LINQ Count() -Methode führt dies automatisch aus.

Die Funktion myEnumerable.OrderBy() eine IOrderedEnumerable zurückgibt. Ein IOrderedEnumerable kann natürlich nicht auf ICollection umgewandelt werden, so ruft Count() wird die ganze Sache verbrauchen.

Die Sortierung ändert jedoch nicht die Anzahl der Elemente, und ein IOrderedEnumerable muss einen Verweis auf seine Quelle behalten. Wenn also diese Quelle eine ICollection ist, sollte es möglich sein, die Zählung von IOrderedEnumerable zu ermitteln, ohne sie zu verbrauchen.

Mein Ziel ist es, eine Bibliotheksmethode zu haben, die eine IEnumerable mit n Elementen nimmt und dann zum Beispiel das Element an der Position n/2 abruft;

Ich möchte vermeiden, zweimal über die IEnumerable zweimal nur um seine Anzahl zu bekommen, aber ich möchte auch vermeiden, eine unnötige Kopie zu erstellen, wenn überhaupt möglich.


ist hier ein Skelett der Funktion I

public void DoSomething(IEnumerable<T> source) 
{ 
    int count; // What we do with the source depends on its length 

    if (source is ICollection) 
    { 
     count = source.Count(); // Great, we can use ICollection.Count 
    } 
    else if (source is IOrderedEnumerable) 
    { 
     // TODO: Find out whether this is based on an ICollection, 
     // TODO: then determine the count of that ICollection 
    } 
    else 
    { 
     // Iterating over the source may be expensive, 
     // to avoid iterating twice, make a copy of the source 
     source = source.ToList(); 
     count = source.Count(); 
    } 

    // do some stuff 

} 
+0

LINQ ist leider so konstruiert, dass dies völlig unmöglich ist. – SLaks

+0

@SLaks: Oft erweist sich "komplett unmöglich" als möglich. Ich denke, du könntest das mit Expression Tree machen, aber es ist jenseits meiner Fähigkeiten. –

+0

Können Sie Ihre eigene Wrapper-Klasse erstellen, die die ursprüngliche Quelle enthält, und bei Bedarf auch alle Bestellungen übernehmen? – Rob

Antwort

7

Lassen Sie uns überlegen, was dieser Code tatsächlich aussieht:

var source = new[]{ 2, 4, 6, 1, 9 }.OrderBy(x => x); 
int count = source.Count(); 

Es gleiche wie

int count = Enumerable.Count(Enumerable.OrderBy(new[]{ 2, 4, 6, 1, 9 }, x => x)); 

Ergebnis Enumerable.OrderBy(new[]{ 2, 4, 6, 1, 9 }, x => x) in Count Erweiterung übergeben wird. Sie können nicht vermeiden, OrderBy Ausführung. Und so ist es ein Nicht-Streaming-Operator, der alle Quellen verbraucht, bevor er etwas zurückgibt, das an Count übergeben wird.

Die einzige Möglichkeit, die Iteration über die gesamte Sammlung zu vermeiden, ist die Vermeidung von OrderBy - Elemente vor dem Sortieren zählen.


UPDATE: Sie können auf jedem OrderedEnumerable diese Erweiterung Methode aufrufen - es Reflexion verwenden source Bereich OrderedEnumerable<T> zu erhalten, die Quellsequenz hält. Dann prüfen Sie, ob diese Sequenz Sammlung ist, und verwenden Sie Count ohne Ausführung Bestellung:

public static class Extensions 
{ 
    public static int Count<T>(this IOrderedEnumerable<T> ordered) 
    { 
     // you can check if ordered is of type OrderedEnumerable<T> 
     Type type = ordered.GetType(); 
     var flags = BindingFlags.NonPublic | BindingFlags.Instance; 
     var field = type.GetField("source", flags); 
     var source = field.GetValue(ordered); 
     if (source is ICollection<T>) 
      return ((ICollection<T>)source).Count; 

     return ordered.Count(); 
    } 
} 

Verbrauch:

var source = new[]{ 2, 4, 6, 1, 9 }.OrderBy(x => x); 
int count = source.Count(); 
+0

Ich weiß, dass der Code, den ich gepostet habe, die OrderBy-Ausführung nicht vermeidet. Deshalb frage ich nach einem anderen Weg. Aber das IOrderedEnumerable, das von OrderBy erstellt wird, muss einen Verweis auf der ursprünglichen Liste behalten, um seine ausgelagerte Executivgeschäft zu tun, so dass es theoretisch möglich sein sollte, die Anzahl von dieser Referenz zu erhalten. – HugoRune

+0

@HugoRune OK, habe dein Problem verstanden. Die Antwort wurde aktualisiert –

+0

Ich experimentierte nur mit der gleichen Reflektionsidee - es funktioniert, obwohl man sich auf private Felder verlassen sollte, wenn es möglich ist. Es gibt vermutlich keine Garantie, dass das gleiche Feld in Aktualisierungen des Frameworks existiert. – Rob

0

Wenn Sie schauen, erstellen eine performante Lösung erstellen möchte ich die Schaffung Überlastungen betrachten würde, die entweder eine Sammlung oder ein IOrderedEnumerable nehmen usw. .. alles, was "ist" und "wie" Typchecking und Casting kann nicht gut für die Art von Dingen, die Sie erstellen.

Sie erfinden das Rad neu. linq's "Count()" Funktion macht so ziemlich wie Sie wollen.

Fügen Sie auch das Schlüsselwort this hinzu und machen Sie dies zu einer raffinierten Erweiterungsmethode, um sich selbst und andere mit dem Code zufrieden zu stellen.

DoSomething(this Collection source); 
DoSomething<T>(this List<T> source); 
DoSomething<T>(this IOrderedEnumerable<T> source); 

etc ...

+0

Das Ganze ist eine Erweiterungsmethode mit mehreren Überladungen, die ich aus meinem vereinfachten Codebeispiel herausgelassen habe, da es das grundlegende Problem nicht ändert. Die native Count() - Funktion * kann * nicht tun, was ich will: Zählen Sie ein IOrderedEnumerable anhand einer Liste, ohne die Liste zu sortieren. – HugoRune

+0

Okay, dann denke ich, dass es in erster Linie kein IOrderedEnumerable sein sollte ... –

0

Ein weiterer Ansatz ist es, eine Klasse zu implementieren, die IOrderedEnumerable<T> implementiert. Sie können dann Klassenelemente implementieren, die die üblichen Linq-Erweiterungsmethoden kurzschließen, und eine Zählmethode bereitstellen, die die ursprüngliche Enumeration berücksichtigt.

public class MyOrderedEnumerable<T> : IOrderedEnumerable<T> 
{ 
    private IEnumerable<T> Original; 
    private IOrderedEnumerable<T> Sorted; 

    public MyOrderedEnumerable(IEnumerable<T> orig) 
    { 
      Original = orig; 
      Sorted = null; 
    } 

    private void ApplyOrder<TKey>(Func<T, TKey> keySelector, IComparer<TKey> comparer, bool descending) 
    { 
      var before = Sorted != null ? Sorted : Original; 
      if (descending) 
        Sorted = before.OrderByDescending(keySelector, comparer); 
      else 
        Sorted = before.OrderBy(keySelector, comparer); 
    } 

    #region Interface Implementations 

    public IEnumerator<T> GetEnumerator() 
    { 
      return Sorted != null ? Sorted.GetEnumerator() : Original.GetEnumerator(); 
    } 

    IEnumerator IEnumerable.GetEnumerator() 
    { 
      return GetEnumerator(); 
    } 

    public IOrderedEnumerable<T> CreateOrderedEnumerable<TKey>(
      Func<T, TKey> keySelector, 
      IComparer<TKey> comparer, 
      bool descending) 
    { 
      var newSorted = new MyOrderedEnumerable<T>(Original); 
      newSorted.ApplyOrder(keySelector, comparer, descending); 
      return newSorted; 
    } 

    #endregion Interface Implementations 


    //Ensure that OrderBy returns the right type. 
    //There are other variants of OrderBy extension methods you'll have to short-circuit 
    public MyOrderedEnumerable<T> OrderBy<TKey>(Func<T, TKey> keySelector) 
    { 
      Console.WriteLine("Ordering"); 
      var newSorted = new MyOrderedEnumerable<T>(Original); 
      newSorted.Sorted = (Sorted != null ? Sorted : Original).OrderBy(keySelector); 
      return newSorted; 
    } 

    public int Count() 
    { 
      Console.WriteLine("Fast counting.."); 
      var collection = Original as ICollection; 
      return collection == null ? Original.Count() : collection.Count; 
    } 

    public static void Test() 
    { 
      var nums = new MyOrderedEnumerable<int>(Enumerable.Range(0,10).ToList()); 
      var nums2 = nums.OrderBy(x => -x); 
      var z = nums.Count() + nums2.Count(); 
    } 
}