Compter un IOrderedEnumerable sans le consumr

Ce que je veux faire, version courte:

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

Version longue:

Pour déterminer le nombre d’éléments dans un IEnumerable , il est nécessaire de parcourir tous les éléments. Cela pourrait potentiellement être une opération très coûteuse.

Si IEnumerable peut être converti en ICollection , le nombre peut être déterminé rapidement sans itération. La méthode LINQ Count () le fait automatiquement.

La fonction myEnumerable.OrderBy () renvoie un IOrderedEnumerable . Un IOrderedEnumerable ne peut évidemment pas être converti en ICollection , donc l’appel de Count () consum tout le contenu.

Mais le sorting ne change pas le nombre d’éléments et un IOrderedEnumerable doit conserver une référence à sa source. Ainsi, si cette source est une collection ICollection , il devrait être possible de déterminer le nombre à partir de IOrderedEnumerable sans le consumr.

Mon objective est d’avoir une méthode de bibliothèque, qui prend un IEnumerable avec n éléments, puis récupère par exemple l’élément à la position n / 2;

Je veux éviter d’itérer deux fois l’ IEnumerable pour obtenir son décompte, mais je veux aussi éviter de créer une copie non nécessaire dans la mesure du possible.


Voici un squelette de la fonction que je veux créer

 public void DoSomething(IEnumerable 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 } 

Pensons à quoi ressemble ce code:

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

C’est pareil que

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

Résultat de Enumerable.OrderBy(new[]{ 2, 4, 6, 1, 9 }, x => x) est passé dans l’extension Count . Vous ne pouvez pas éviter l’exécution de OrderBy . Et donc c’est un opérateur non-streaming, il consum toutes les sources avant de retourner quelque chose, qui sera passé à Count .

Ainsi, le seul moyen d’éviter de parcourir l’ensemble de la collection consiste à éviter les éléments OrderBy -count avant le sorting.


UPDATE: Vous pouvez appeler cette méthode d’extension sur n’importe quelle commande OrderedEnumerable . Elle utilisera la reflection pour obtenir source champ source de OrderedEnumerable qui contient la séquence source. Ensuite, vérifiez si cette séquence est une collection et utilisez Count sans exécuter l’ordre:

 public static class Extensions { public static int Count(this IOrderedEnumerable ordered) { // you can check if ordered is of type OrderedEnumerable 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) return ((ICollection)source).Count; return ordered.Count(); } } 

Usage:

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

Si vous cherchez à créer une solution performante, je vous conseillerais de créer des surcharges prenant soit une collection, soit un IOrderedEnumerable, etc. Tout ce qui est “et” comme sont en train de créer.

Vous réinventez la roue. La fonction “Count ()” de linq fait à peu près tout ce que vous voulez.

Ajoutez également le mot-clé this et transformez-le en une méthode d’extension astucieuse, pour vous faire plaisir et pour ceux qui utilisent le code.

 DoSomething(this Collection source); DoSomething(this List source); DoSomething(this IOrderedEnumerable source); 

etc…

Une autre approche consiste à implémenter une classe qui implémente IOrderedEnumerable . Vous pouvez ensuite implémenter des membres de classe qui court-circuiteront les méthodes d’extension Linq habituelles et fournir une méthode de comptage qui examine l’énumération d’origine.

 public class MyOrderedEnumerable : IOrderedEnumerable { private IEnumerable Original; private IOrderedEnumerable Sorted; public MyOrderedEnumerable(IEnumerable orig) { Original = orig; Sorted = null; } private void ApplyOrder(Func keySelector, IComparer 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 GetEnumerator() { return Sorted != null ? Sorted.GetEnumerator() : Original.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public IOrderedEnumerable CreateOrderedEnumerable( Func keySelector, IComparer comparer, bool descending) { var newSorted = new MyOrderedEnumerable(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 OrderBy(Func keySelector) { Console.WriteLine("Ordering"); var newSorted = new MyOrderedEnumerable(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(Enumerable.Range(0,10).ToList()); var nums2 = nums.OrderBy(x => -x); var z = nums.Count() + nums2.Count(); } }