Usare Linq per ottenere gli ultimi N elementi di una collezione?

Data una collezione, c’è un modo per ottenere gli ultimi N elementi di quella collezione? Se non esiste un metodo nel framework, quale sarebbe il modo migliore per scrivere un metodo di estensione per farlo?

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

Questo approccio preserva l’ordine degli articoli senza alcuna dipendenza da alcun ordinamento e ha un’ampia compatibilità tra diversi provider LINQ.

È importante fare attenzione a non chiamare Skip con un numero negativo. Alcuni provider, come Entity Framework, generano una ArgumentException quando vengono presentati con un argomento negativo. La chiamata a Math.Max evita accuratamente.

La class seguente ha tutti gli elementi essenziali per i metodi di estensione, che sono: una class statica, un metodo statico e l’uso della parola chiave this .

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

Una breve nota sulla performance:

Poiché la chiamata a Count() può causare l’enumerazione di alcune strutture di dati, questo approccio ha il rischio di causare due passaggi sui dati. Questo non è davvero un problema con la maggior parte degli enumerabili; infatti, esistono già ottimizzazioni per Liste, Array e anche query EF per valutare l’operazione Count() in O (1).

Se, tuttavia, è necessario utilizzare una enumerazione forward-only e si desidera evitare di effettuare due passaggi, si consideri un algoritmo one-pass come Lasse V. Karlsen o Mark Byers descrivono. Entrambi questi approcci utilizzano un buffer temporaneo per contenere gli elementi durante l’enumerazione, che vengono restituiti una volta trovata la fine della raccolta.

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

AGGIORNAMENTO: Per risolvere il problema di clintp: a) L’uso del metodo TakeLast () che ho definito sopra risolve il problema, ma se si vuole veramente farlo senza il metodo extra, allora si deve riconoscere che mentre Enumerable.Reverse () può essere utilizzato come metodo di estensione, non è necessario utilizzarlo in questo modo:

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

Nota : mi mancava il titolo della domanda che diceva Usando Linq , quindi la mia risposta in realtà non usa Linq.

Se si desidera evitare di memorizzare nella cache una copia non pigra dell’intera raccolta, è ansible scrivere un metodo semplice che lo faccia utilizzando un elenco collegato.

Il seguente metodo aggiungerà ogni valore che trova nella raccolta originale in un elenco collegato e ridurrà l’elenco collegato al numero di elementi richiesti. Poiché mantiene l’elenco collegato tagliato a questo numero di elementi per tutto il tempo attraverso l’iterazione attraverso la raccolta, manterrà solo una copia di al massimo N elementi dalla collezione originale.

Non richiede che tu conosca il numero di elementi nella collezione originale, né esegua iterazioni su di essa più di una volta.

Uso:

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

Metodo di estensione:

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

Ecco un metodo che funziona su qualsiasi enumerabile ma utilizza solo la memoria temporanea O (N):

 public static class TakeLastExtension { public static IEnumerable TakeLast(this IEnumerable 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 l = new List {4, 6, 3, 6, 2, 5, 7}; List lastElements = l.TakeLast(3).ToList(); 

Funziona utilizzando un buffer circolare di dimensione N per memorizzare gli elementi come li vede, sovrascrivendo i vecchi elementi con quelli nuovi. Quando viene raggiunta la fine dell'enumerabile, il buffer dell'anello contiene gli ultimi N elementi.

Sono sorpreso che nessuno lo abbia menzionato, ma SkipWhile ha un metodo che usa l’indice dell’elemento .

 public static IEnumerable TakeLastN(this IEnumerable 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) 

L'unico vantaggio percepibile che questa soluzione presenta rispetto ad altri è che si può avere l'opzione di aggiungere un predicato per creare una query LINQ più potente ed efficiente, invece di avere due operazioni separate che attraversano l'IEnumerable due volte.

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

Utilizzare EnumerableEx.TakeLast nell’assembly System.Interactive di RX. È un’implementazione O (N) come @ Mark, ma utilizza una coda anziché un costrutto ring-buffer (e rimuove le voci quando raggiunge la capacità del buffer).

(NB: Questa è la versione IEnumerable – non la versione IObservable, anche se l’implementazione dei due è praticamente identica)

.NET Core 2.0 fornisce il metodo LINQ TakeLast() :

https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.takelast

esempio :

 Enumerable .Range(1, 10) .TakeLast(3) // <--- takes last 3 items .ToList() .ForEach(i => System.Console.WriteLine(i)) // outputs: // 8 // 9 // 10 

Se non ti dispiace immergerti in Rx come parte della monade, puoi usare TakeLast :

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

Se si ha a che fare con una collezione con una chiave (ad esempio, voci da un database), una soluzione rapida (cioè più veloce della risposta selezionata) sarebbe

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

Se l’utilizzo di una libreria di terze parti è un’opzione, MoreLinq definisce TakeLast() che fa esattamente questo.

Ho cercato di combinare efficienza e semplicità e alla fine ho:

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

Informazioni sulle prestazioni: in C #, la Queue viene implementata utilizzando un buffer circolare, pertanto non viene eseguita alcuna istanza dell’object per ogni ciclo (solo quando la coda cresce). Non ho impostato la capacità della coda (utilizzando il costruttore dedicato) perché qualcuno potrebbe chiamare questa estensione con count = int.MaxValue . Per prestazioni extra è ansible controllare se l’origine implementa IList e in caso affermativo, estrarre direttamente gli ultimi valori utilizzando gli indici di array.

È un po ‘inefficiente prendere l’ultimo N di una raccolta usando LINQ poiché tutte le soluzioni sopra citate richiedono una iterazione attraverso la collezione. TakeLast(int n) in System.Interactive ha anche questo problema.

Se hai una lista, una cosa più efficiente da fare è tagliarla usando il seguente metodo

 /// Select from start to end exclusive of end using the same semantics /// as python slice. ///  the list to slice /// The starting index /// The ending index. The result does not include this index public static List Slice (this IReadOnlyList 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(); } throw new IndexOutOfRangeException( "count = " + list.Count() + " start = " + start + " end = " + end); } 

con

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

e alcuni casi di test

 [Fact] public void GetRange() { IReadOnlyList l = new List() { 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() { 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}); } 

So che è troppo tardi per rispondere a questa domanda. Ma se stai lavorando con la raccolta di tipo IList <> e non ti interessa un ordine della collezione restituita, allora questo metodo funziona più velocemente. Ho usato la risposta di Mark Byers e ho apportato alcune modifiche. Quindi ora il metodo TakeLast è:

 public static IEnumerable TakeLast(IList 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]; } } } 

Per il test ho usato il metodo Mark Byers e il comando di kbrimington . Questo è un test:

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

E qui ci sono risultati per prendere 10 elementi:

inserisci la descrizione dell'immagine qui

e per aver preso 1000001 elementi i risultati sono: inserisci la descrizione dell'immagine qui

Ecco la mia soluzione:

 public static class EnumerationExtensions { public static IEnumerable TakeLast(this IEnumerable input, int count) { if (count <= 0) yield break; var inputList = input as IList; 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 SkipLast(this IEnumerable input, int count) { if (count <= 0) return input; else return input.SkipLastIter(count); } private static IEnumerable SkipLastIter(this IEnumerable input, int count) { var inputList = input as IList; 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 elements = new Queue(); foreach (T item in input) { elements.Enqueue(item); if (elements.Count > count) yield return elements.Dequeue(); } } } } 

Il codice è un po ‘grosso, ma come componente riutilizzabile, dovrebbe funzionare come meglio può nella maggior parte degli scenari, e manterrà il codice che lo sta usando bello e conciso. 🙂

Il mio TakeLast per non- IList`1 si basa sullo stesso algoritmo del buffer circolare di quello nelle risposte di @Mark Byers e @MackieChan più avanti. È interessante quanto siano simili – ho scritto il mio in modo completamente indipendente. Indovina c’è davvero solo un modo per fare correttamente un ring buffer. 🙂

Guardando la risposta di @ kbrimington, a IQuerable potrebbe essere aggiunto un ulteriore controllo per IQuerable all’approccio che funziona bene con Entity Framework – assumendo che ciò che ho a questo punto non lo è.

Sotto l’esempio reale come prendere gli ultimi 3 elementi da una collezione (array):

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

Utilizzo di questo metodo per ottenere tutti gli intervalli senza errori

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