tutorial query español ejemplos c# linq

query - linq c# tutorial español



¿Usando Linq para obtener los últimos N elementos de una colección? (16)

Dada una colección, ¿hay una manera de obtener los últimos N elementos de esa colección? Si no hay un método en el marco, ¿cuál sería la mejor manera de escribir un método de extensión para hacer esto?



Aquí está mi solución:

public static class EnumerationExtensions { public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> input, int count) { if (count <= 0) yield break; var inputList = input as IList<T>; if (inputList != null) { int last = inputList.Count; int first = last - count; if (first < 0) first = 0; for (int i = first; i < last; i++) yield return inputList[i]; } else { // Use a ring buffer. We have to enumerate the input, and we don''t know in advance how many elements it will contain. T[] buffer = new T[count]; int index = 0; count = 0; foreach (T item in input) { buffer[index] = item; index = (index + 1) % buffer.Length; count++; } // The index variable now points at the next buffer entry that would be filled. If the buffer isn''t completely // full, then there are ''count'' elements preceding index. If the buffer *is* full, then index is pointing at // the oldest entry, which is the first one to return. // // If the buffer isn''t full, which means that the enumeration has fewer than ''count'' elements, we''ll fix up // ''index'' to point at the first entry to return. That''s easy to do; if the buffer isn''t full, then the oldest // entry is the first one. :-) // // We''ll also set ''count'' to the number of elements to be returned. It only needs adjustment if we''ve wrapped // past the end of the buffer and have enumerated more than the original count value. if (count < buffer.Length) index = 0; else count = buffer.Length; // Return the values in the correct order. while (count > 0) { yield return buffer[index]; index = (index + 1) % buffer.Length; count--; } } } public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> input, int count) { if (count <= 0) return input; else return input.SkipLastIter(count); } private static IEnumerable<T> SkipLastIter<T>(this IEnumerable<T> input, int count) { var inputList = input as IList<T>; if (inputList != null) { int first = 0; int last = inputList.Count - count; if (last < 0) last = 0; for (int i = first; i < last; i++) yield return inputList[i]; } else { // Aim to leave ''count'' items in the queue. If the input has fewer than ''count'' // items, then the queue won''t ever fill and we return nothing. Queue<T> elements = new Queue<T>(); foreach (T item in input) { elements.Enqueue(item); if (elements.Count > count) yield return elements.Dequeue(); } } } }

El código es un poco grueso, pero como componente reutilizable, debe funcionar tan bien como puede en la mayoría de los escenarios, y mantendrá el código que lo usa de forma agradable y concisa. :-)

My TakeLast for TakeLast - IList`1 se basa en el mismo algoritmo de búfer de anillo que en las respuestas de @Mark Byers y @MackieChan más adelante. Es interesante lo similares que son, escribí el mío de forma completamente independiente. Supongo que realmente hay una sola manera de hacer un búfer de anillo correctamente. :-)

Mirando la respuesta de @kbrimington, se podría agregar una verificación adicional para que IQuerable<T> al enfoque que funciona bien con Entity Framework, asumiendo que lo que tengo en este punto no lo hace.


Aquí hay un método que funciona en cualquier enumerable pero que usa solo almacenamiento temporal O (N):

public static class TakeLastExtension { public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int takeCount) { if (source == null) { throw new ArgumentNullException("source"); } if (takeCount < 0) { throw new ArgumentOutOfRangeException("takeCount", "must not be negative"); } if (takeCount == 0) { yield break; } T[] result = new T[takeCount]; int i = 0; int sourceCount = 0; foreach (T element in source) { result[i] = element; i = (i + 1) % takeCount; sourceCount++; } if (sourceCount < takeCount) { takeCount = sourceCount; i = 0; } for (int j = 0; j < takeCount; ++j) { yield return result[(i + j) % takeCount]; } } }

Uso:

List<int> l = new List<int> {4, 6, 3, 6, 2, 5, 7}; List<int> lastElements = l.TakeLast(3).ToList();

Funciona utilizando un búfer de anillo de tamaño N para almacenar los elementos tal como los ve, sobrescribiendo los elementos antiguos por otros nuevos. Cuando se llega al final de lo enumerable, el búfer de anillo contiene los últimos N elementos.


Debajo del ejemplo real de cómo tomar los últimos 3 elementos de una colección (matriz):

// split address by spaces into array string[] adrParts = adr.Split(new string[] { " " },StringSplitOptions.RemoveEmptyEntries); // take only 3 last items in array adrParts = adrParts.SkipWhile((value, index) => { return adrParts.Length - index > 3; }).ToArray();


Es un poco ineficiente tomar la última N de una colección utilizando LINQ, ya que todas las soluciones anteriores requieren una iteración en toda la colección. TakeLast(int n) en System.Interactive también tiene este problema.

Si tiene una lista, una cosa más eficiente que hacer es cortarla usando el siguiente método

/// Select from start to end exclusive of end using the same semantics /// as python slice. /// <param name="list"> the list to slice</param> /// <param name="start">The starting index</param> /// <param name="end">The ending index. The result does not include this index</param> public static List<T> Slice<T> (this IReadOnlyList<T> list, int start, int? end = null) { if (end == null) { end = list.Count(); } if (start < 0) { start = list.Count + start; } if (start >= 0 && end.Value > 0 && end.Value > start) { return list.GetRange(start, end.Value - start); } if (end < 0) { return list.GetRange(start, (list.Count() + end.Value) - start); } if (end == start) { return new List<T>(); } throw new IndexOutOfRangeException( "count = " + list.Count() + " start = " + start + " end = " + end); }

con

public static List<T> GetRange<T>( this IReadOnlyList<T> list, int index, int count ) { List<T> r = new List<T>(count); for ( int i = 0; i < count; i++ ) { int j=i + index; if ( j >= list.Count ) { break; } r.Add(list[j]); } return r; }

y algunos casos de prueba

[Fact] public void GetRange() { IReadOnlyList<int> l = new List<int>() { 0, 10, 20, 30, 40, 50, 60 }; l .GetRange(2, 3) .ShouldAllBeEquivalentTo(new[] { 20, 30, 40 }); l .GetRange(5, 10) .ShouldAllBeEquivalentTo(new[] { 50, 60 }); } [Fact] void SliceMethodShouldWork() { var list = new List<int>() { 1, 3, 5, 7, 9, 11 }; list.Slice(1, 4).ShouldBeEquivalentTo(new[] { 3, 5, 7 }); list.Slice(1, -2).ShouldBeEquivalentTo(new[] { 3, 5, 7 }); list.Slice(1, null).ShouldBeEquivalentTo(new[] { 3, 5, 7, 9, 11 }); list.Slice(-2) .Should() .BeEquivalentTo(new[] {9, 11}); list.Slice(-2,-1 ) .Should() .BeEquivalentTo(new[] {9}); }


Intenté combinar eficiencia y simplicidad y terminar con esto:

public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int count) { if (source == null) { throw new ArgumentNullException("source"); } Queue<T> lastElements = new Queue<T>(); foreach (T element in source) { lastElements.Enqueue(element); if (lastElements.Count > count) { lastElements.Dequeue(); } } return lastElements; }

Acerca del rendimiento: en C #, Queue<T> se implementa utilizando un búfer circular, por lo que no se realiza una creación de instancias de objetos en cada bucle (solo cuando la cola está creciendo). No configuré la capacidad de la cola (usando un constructor dedicado) porque alguien podría llamar a esta extensión con count = int.MaxValue . Para obtener un rendimiento adicional, puede verificar si la fuente implementa IList<T> y, en caso afirmativo, extraer directamente los últimos valores utilizando índices de matriz.


Me sorprende que nadie lo haya mencionado, pero SkipWhile tiene un método que utiliza el índice del elemento .

public static IEnumerable<T> TakeLastN<T>(this IEnumerable<T> source, int n) { if (source == null) throw new ArgumentNullException("Source cannot be null"); int goldenIndex = source.Count() - n; return source.SkipWhile((val, index) => index < goldenIndex); } //Or if you like them one-liners (in the spirit of the current accepted answer); //However, this is most likely impractical due to the repeated calculations collection.SkipWhile((val, index) => index < collection.Count() - N)

El único beneficio perceptible que esta solución presenta sobre otras es que puede tener la opción de agregar un predicado para hacer una consulta LINQ más poderosa y eficiente, en lugar de tener dos operaciones separadas que atraviesan IEnumerable dos veces.

public static IEnumerable<T> FilterLastN<T>(this IEnumerable<T> source, int n, Predicate<T> pred) { int goldenIndex = source.Count() - n; return source.SkipWhile((val, index) => index < goldenIndex && pred(val)); }


Sé que es demasiado tarde para responder a esta pregunta. Pero si está trabajando con una colección de tipo IList <> y no le importa un pedido de la colección devuelta, entonces este método funciona más rápido. He utilizado la respuesta de Mark Byers y he hecho algunos pequeños cambios. Así que ahora el método TakeLast es:

public static IEnumerable<T> TakeLast<T>(IList<T> source, int takeCount) { if (source == null) { throw new ArgumentNullException("source"); } if (takeCount < 0) { throw new ArgumentOutOfRangeException("takeCount", "must not be negative"); } if (takeCount == 0) { yield break; } if (source.Count > takeCount) { for (int z = source.Count - 1; takeCount > 0; z--) { takeCount--; yield return source[z]; } } else { for(int i = 0; i < source.Count; i++) { yield return source[i]; } } }

Para la prueba, he usado el método de Mark Byers y kbrimington''s andswer . Esto es prueba:

IList<int> test = new List<int>(); for(int i = 0; i<1000000; i++) { test.Add(i); } Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); IList<int> result = TakeLast(test, 10).ToList(); stopwatch.Stop(); Stopwatch stopwatch1 = new Stopwatch(); stopwatch1.Start(); IList<int> result1 = TakeLast2(test, 10).ToList(); stopwatch1.Stop(); Stopwatch stopwatch2 = new Stopwatch(); stopwatch2.Start(); IList<int> result2 = test.Skip(Math.Max(0, test.Count - 10)).Take(10).ToList(); stopwatch2.Stop();

Y aquí están los resultados para tomar 10 elementos:

y por tomar elementos de 1000001 los resultados son:


Si está tratando con una colección con una clave (por ejemplo, entradas de una base de datos), una solución rápida (es decir, más rápida que la respuesta seleccionada) sería

collection.OrderByDescending(c => c.Key).Take(3).OrderBy(c => c.Key);


Si no te importa sumergirte en Rx como parte de la mónada, puedes usar TakeLast :

IEnumerable<int> source = Enumerable.Range(1, 10000); IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();


Si usar una biblioteca de terceros es una opción, MoreLinq define TakeLast() que hace exactamente esto.


Usando este método para obtener todo el rango sin error

public List<T> GetTsRate( List<T> AllT,int Index,int Count) { List<T> Ts = null; try { Ts = AllT.ToList().GetRange(Index, Count); } catch (Exception ex) { Ts = AllT.Skip(Index).ToList(); } return Ts ; }


Use EnumerableEx.TakeLast en el ensamblado System.Interactive de RX. Es una implementación O (N) como la de @ Mark, pero usa una cola en lugar de una construcción de buffs de anillo (y elimina los elementos en cola cuando alcanza la capacidad del búfer).

(Nota: esta es la versión IEnumerable, no la versión IObservable, aunque la implementación de los dos es prácticamente idéntica)


Nota : me perdí el título de la pregunta que decía Uso de Linq , por lo que mi respuesta en realidad no usa Linq.

Si desea evitar el almacenamiento en caché de una copia no perezosa de toda la colección, puede escribir un método simple que lo haga usando una lista vinculada.

El siguiente método agregará cada valor que encuentre en la colección original en una lista vinculada y recortará la lista vinculada al número de elementos requeridos. Dado que mantiene la lista vinculada recortada a este número de elementos todo el tiempo a través de la iteración a través de la colección, solo conservará una copia de como máximo N elementos de la colección original.

No requiere que usted sepa la cantidad de elementos en la colección original, ni que repita más de una vez.

Uso:

IEnumerable<int> sequence = Enumerable.Range(1, 10000); IEnumerable<int> last10 = sequence.TakeLast(10); ...

Método de extensión:

public static class Extensions { public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> collection, int n) { if (collection == null) throw new ArgumentNullException("collection"); if (n < 0) throw new ArgumentOutOfRangeException("n", "n must be 0 or greater"); LinkedList<T> temp = new LinkedList<T>(); foreach (var value in collection) { temp.AddLast(value); if (temp.Count > n) temp.RemoveFirst(); } return temp; } }


coll.Reverse().Take(N).Reverse().ToList(); public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> coll, int N) { return coll.Reverse().Take(N).Reverse(); }

ACTUALIZACIÓN: Para solucionar el problema de clintp: a) El uso del método TakeLast () que he definido anteriormente resuelve el problema, pero si realmente desea hacerlo sin el método adicional, solo tiene que reconocer que mientras Enumerable.Reverse () puede ser utilizado como método de extensión, no es necesario que lo uses de esa manera:

List<string> mystring = new List<string>() { "one", "two", "three" }; mystring = Enumerable.Reverse(mystring).Take(2).Reverse().ToList();


collection.Skip(Math.Max(0, collection.Count() - N));

Este enfoque preserva el orden de los artículos sin depender de ninguna clasificación, y tiene una amplia compatibilidad con varios proveedores de LINQ.

Es importante tener cuidado de no llamar a Skip con un número negativo. Algunos proveedores, como Entity Framework, producirán una ArgumentException cuando se les presente un argumento negativo. La llamada a Math.Max evita esto claramente.

La clase a continuación tiene todos los elementos esenciales para los métodos de extensión, que son: una clase estática, un método estático y el uso de this palabra clave.

public static class MiscExtensions { // Ex: collection.TakeLast(5); public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int N) { return source.Skip(Math.Max(0, source.Count() - N)); } }

Una breve nota sobre el rendimiento:

Debido a que la llamada a Count() puede causar la enumeración de ciertas estructuras de datos, este enfoque tiene el riesgo de causar dos pasadas sobre los datos. Esto no es realmente un problema con la mayoría de los enumerables; de hecho, ya existen optimizaciones para Listas, Arrays e incluso consultas EF para evaluar la operación Count() en O (1).

Sin embargo, si debe usar un enumerable de solo avance y desea evitar hacer dos pasadas, considere un algoritmo de una pasada como Lasse V. Karlsen o Mark Byers describen. Ambos enfoques utilizan un búfer temporal para contener elementos mientras se enumeran, que se obtienen una vez que se encuentra el final de la colección.