Étant donné une collection, existe-t-il un moyen d'obtenir les N derniers éléments de cette collection? S'il n'y a pas de méthode dans le framework, quelle serait la meilleure façon d'écrire une méthode d'extension pour ce faire?
Étant donné une collection, existe-t-il un moyen d'obtenir les N derniers éléments de cette collection? S'il n'y a pas de méthode dans le framework, quelle serait la meilleure façon d'écrire une méthode d'extension pour ce faire?
Réponses:
collection.Skip(Math.Max(0, collection.Count() - N));
Cette approche préserve l'ordre des éléments sans dépendre d'aucun tri et a une large compatibilité entre plusieurs fournisseurs LINQ.
Il est important de ne pas appeler Skip
avec un numéro négatif. Certains fournisseurs, tels que Entity Framework, produisent une ArgumentException lorsqu'ils sont présentés avec un argument négatif. L'appel à Math.Max
évite cela proprement.
La classe ci-dessous contient tous les éléments essentiels des méthodes d'extension, qui sont: une classe statique, une méthode statique et l'utilisation du this
mot clé.
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));
}
}
Une brève note sur les performances:
Étant donné que l'appel à Count()
peut provoquer l'énumération de certaines structures de données, cette approche présente le risque de provoquer deux passages sur les données. Ce n'est pas vraiment un problème avec la plupart des énumérables; en fait, des optimisations existent déjà pour les requêtes Lists, Arrays et même EF pour évaluer l' Count()
opération en temps O (1).
Si, toutefois, vous devez utiliser un énumérable uniquement vers l'avant et que vous souhaitez éviter d'effectuer deux passes, envisagez un algorithme à une passe comme Lasse V. Karlsen ou Mark Byers le décrivent. Ces deux approches utilisent un tampon temporaire pour conserver les éléments lors de l'énumération, qui sont générés une fois la fin de la collection trouvée.
List
s et LinkedList
s, la solution de James a tendance à être plus rapide, mais pas d'un ordre de grandeur. Si l'IEnumerable est calculé (via Enumerable.Range, par exemple), la solution de James prend plus de temps. Je ne peux penser à aucun moyen de garantir un seul passage sans savoir quelque chose sur l'implémentation ou copier des valeurs dans une structure de données différente.
coll.Reverse().Take(N).Reverse().ToList();
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> coll, int N)
{
return coll.Reverse().Take(N).Reverse();
}
MISE À JOUR: Pour résoudre le problème de clintp: a) L'utilisation de la méthode TakeLast () que j'ai définie ci-dessus résout le problème, mais si vous voulez vraiment le faire sans la méthode supplémentaire, alors il vous suffit de reconnaître que si Enumerable.Reverse () peut être utilisé comme méthode d'extension, vous n'êtes pas obligé de l'utiliser de cette façon:
List<string> mystring = new List<string>() { "one", "two", "three" };
mystring = Enumerable.Reverse(mystring).Take(2).Reverse().ToList();
List<string> mystring = new List<string>() { "one", "two", "three" }; mystring = mystring.Reverse().Take(2).Reverse();
j'obtiens une erreur de compilation car .Reverse () retourne void et le compilateur choisit cette méthode à la place de Linq qui retourne un IEnumerable. Suggestions?
N
enregistrements, vous pouvez ignorer le second Reverse
.
Remarque : J'ai raté le titre de votre question qui disait Utilisation de Linq , donc ma réponse n'utilise pas en fait Linq.
Si vous voulez éviter de mettre en cache une copie non paresseuse de la collection entière, vous pouvez écrire une méthode simple qui le fait en utilisant une liste liée.
La méthode suivante ajoute chaque valeur trouvée dans la collection d'origine dans une liste liée et réduit la liste liée au nombre d'éléments requis. Puisqu'il conserve la liste chaînée ajustée à ce nombre d'éléments tout au long de l'itération de la collection, il ne conserve qu'une copie d'au plus N éléments de la collection d'origine.
Il ne vous oblige pas à connaître le nombre d'articles dans la collection d'origine, ni à le parcourir plus d'une fois.
Usage:
IEnumerable<int> sequence = Enumerable.Range(1, 10000);
IEnumerable<int> last10 = sequence.TakeLast(10);
...
Méthode d'extension:
public static class Extensions
{
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> collection,
int n)
{
if (collection == null)
throw new ArgumentNullException(nameof(collection));
if (n < 0)
throw new ArgumentOutOfRangeException(nameof(n), $"{nameof(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;
}
}
Voici une méthode qui fonctionne sur n'importe quel énumérable mais utilise uniquement le stockage temporaire 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];
}
}
}
Usage:
List<int> l = new List<int> {4, 6, 3, 6, 2, 5, 7};
List<int> lastElements = l.TakeLast(3).ToList();
Il fonctionne en utilisant un tampon circulaire de taille N pour stocker les éléments tels qu'ils les voient, en remplaçant les anciens éléments par de nouveaux. Lorsque la fin de l'énumérable est atteinte, le tampon en anneau contient les N derniers éléments.
n
.
.NET Core 2.0+ fournit la méthode LINQ TakeLast()
:
https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.takelast
exemple :
Enumerable
.Range(1, 10)
.TakeLast(3) // <--- takes last 3 items
.ToList()
.ForEach(i => System.Console.WriteLine(i))
// outputs:
// 8
// 9
// 10
netcoreapp1.x
) mais uniquement pour les versions 2.0 et 2.1 de dotnetcore ( netcoreapp2.x
). Il est possible que vous cibliez le framework complet (par exemple net472
) qui n'est également pas pris en charge. (Les bibliothèques standard .net peuvent être utilisées par n'importe lequel des éléments ci-dessus mais ne peuvent exposer que certaines API spécifiques à un framework cible. voir docs.microsoft.com/en-us/dotnet/standard/frameworks )
Je suis surpris que personne ne l'ait mentionné, mais SkipWhile a une méthode qui utilise l'index de l'élément .
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)
Le seul avantage perceptible que cette solution présente par rapport aux autres est que vous pouvez avoir la possibilité d'ajouter un prédicat pour créer une requête LINQ plus puissante et efficace, au lieu d'avoir deux opérations distinctes qui traversent deux fois l'IEnumerable.
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));
}
Utilisez EnumerableEx.TakeLast dans l'assembly System.Interactive de RX. C'est une implémentation O (N) comme @ Mark's, mais elle utilise une file d'attente plutôt qu'une construction de tampon en anneau (et supprime les éléments lorsqu'elle atteint la capacité de la mémoire tampon).
(NB: il s'agit de la version IEnumerable - pas de la version IObservable, bien que l'implémentation des deux soit à peu près identique)
Queue<T>
implémenté à l'aide d'un tampon circulaire ?
Si vous traitez une collection avec une clé (par exemple des entrées d'une base de données), une solution rapide (c'est-à-dire plus rapide que la réponse sélectionnée) serait
collection.OrderByDescending(c => c.Key).Take(3).OrderBy(c => c.Key);
Si cela ne vous dérange pas de plonger dans Rx dans le cadre de la monade, vous pouvez utiliser TakeLast
:
IEnumerable<int> source = Enumerable.Range(1, 10000);
IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();
J'ai essayé de combiner efficacité et simplicité et je me suis retrouvé avec ceci:
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;
}
À propos des performances: en C #, Queue<T>
est implémenté à l'aide d'un tampon circulaire de sorte qu'aucune instanciation d'objet n'est effectuée à chaque boucle (uniquement lorsque la file d'attente augmente). Je n'ai pas défini la capacité de la file d'attente (en utilisant un constructeur dédié) car quelqu'un pourrait appeler cette extension avec count = int.MaxValue
. Pour des performances supplémentaires, vous pouvez vérifier si l'implémentation source IList<T>
et si oui, extraire directement les dernières valeurs à l'aide des index de tableau.
Il est un peu inefficace de prendre le dernier N d'une collection à l'aide de LINQ car toutes les solutions ci-dessus nécessitent une itération à travers la collection. TakeLast(int n)
dansSystem.Interactive
également ce problème.
Si vous avez une liste, une chose plus efficace à faire est de la découper en utilisant la méthode suivante
/// 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);
}
avec
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;
}
et certains cas de test
[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});
}
Je sais qu'il est trop tard pour répondre à cette question. Mais si vous travaillez avec une collection de type IList <> et que vous ne vous souciez pas de l'ordre de la collection retournée, cette méthode fonctionne plus rapidement. J'ai utilisé la réponse de Mark Byers et j'ai apporté quelques modifications. Alors maintenant, la méthode TakeLast est:
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];
}
}
}
Pour le test, j'ai utilisé la méthode Mark Byers et la réponse de kbrimington . C'est test:
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();
Et voici les résultats pour prendre 10 éléments:
et pour prendre 1000001 éléments, les résultats sont:
Voici ma solution:
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();
}
}
}
}
Le code est un peu gros, mais en tant que composant réutilisable, il devrait fonctionner aussi bien que possible dans la plupart des scénarios, et il gardera le code qui l'utilise agréable et concis. :-)
Mon TakeLast
pour nonIList`1
est basé sur le même algorithme de tampon en anneau que celui des réponses de @Mark Byers et @MackieChan plus haut. Il est intéressant de voir à quel point ils sont similaires - j'ai écrit le mien de manière totalement indépendante. Je suppose qu'il n'y a vraiment qu'une seule façon de faire un tampon en anneau correctement. :-)
En regardant la réponse de @ kbrimington, une vérification supplémentaire pourrait être ajoutée à cela pour IQuerable<T>
revenir à l'approche qui fonctionne bien avec Entity Framework - en supposant que ce que j'ai à ce stade ne fonctionne pas.
Ci-dessous l'exemple réel comment prendre les 3 derniers éléments d'une collection (tableau):
// 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();
Utilisation de cette méthode pour obtenir toute la plage sans erreur
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 ;
}
Petite implémentation différente avec l'utilisation d'un tampon circulaire. Les benchmarks montrent que la méthode est environ deux fois plus rapide que celles utilisant Queue (implémentation de TakeLast dans System.Linq ), mais pas sans coût - elle a besoin d'un tampon qui croît avec le nombre d'éléments demandé, même si vous avez un petite collection, vous pouvez obtenir une énorme allocation de mémoire.
public IEnumerable<T> TakeLast<T>(IEnumerable<T> source, int count)
{
int i = 0;
if (count < 1)
yield break;
if (source is IList<T> listSource)
{
if (listSource.Count < 1)
yield break;
for (i = listSource.Count < count ? 0 : listSource.Count - count; i < listSource.Count; i++)
yield return listSource[i];
}
else
{
bool move = true;
bool filled = false;
T[] result = new T[count];
using (var enumerator = source.GetEnumerator())
while (move)
{
for (i = 0; (move = enumerator.MoveNext()) && i < count; i++)
result[i] = enumerator.Current;
filled |= move;
}
if (filled)
for (int j = i; j < count; j++)
yield return result[j];
for (int j = 0; j < i; j++)
yield return result[j];
}
}