Używasz Linq, aby uzyskać ostatnie N elementów kolekcji?

284

Biorąc pod uwagę kolekcję, czy istnieje sposób na uzyskanie ostatnich N elementów tej kolekcji? Jeśli nie ma metody w ramach, jaki byłby najlepszy sposób na napisanie metody rozszerzenia, aby to zrobić?

Matthew Groves
źródło

Odpowiedzi:

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

Takie podejście zachowuje porządek pozycji bez zależności od jakiegokolwiek sortowania i ma szeroką kompatybilność z wieloma dostawcami LINQ.

Ważne jest, aby uważać, aby nie dzwonić Skipz numerem ujemnym. Niektórzy dostawcy, na przykład Entity Framework, wygenerują ArgumentException, gdy zostaną przedstawione z argumentem ujemnym. Wezwanie do Math.Maxtego starannie unika.

Poniższa klasa zawiera wszystkie niezbędne metody metod rozszerzania, którymi są: klasa statyczna, metoda statyczna i użycie thissłowa kluczowego.

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));
    }
}

Krótka uwaga na temat wydajności:

Ponieważ wywołanie Count()może spowodować wyliczenie niektórych struktur danych, takie podejście może spowodować dwa przejścia danych. To nie jest tak naprawdę problem z większością wyliczeń; w rzeczywistości istnieją już optymalizacje dla list, tablic, a nawet zapytań EF w celu oceny Count()operacji w czasie O (1).

Jeśli jednak musisz użyć wyliczenia tylko do przodu i chcesz uniknąć wykonywania dwóch przebiegów, rozważ algorytm jednoprzebiegowy, jak opisują Lasse V. Karlsen lub Mark Byers . Oba te podejścia wykorzystują tymczasowy bufor do przechowywania elementów podczas wyliczania, które są uzyskiwane po znalezieniu końca kolekcji.

Kbrimington
źródło
2
+1, ponieważ działa to w Linq na Entities / SQL. Domyślam się, że jest on również bardziej wydajny w Linq do Objects niż strategia Jamesa Currana.
StriplingWarrior
11
Zależy od charakteru kolekcji. Count () może być O (N).
James Curran,
3
@James: Absolutnie poprawne. Jeśli zajmujemy się ściśle kolekcjami IEnumerable, może to być zapytanie dwuprzebiegowe. Byłbym bardzo zainteresowany widzeniem gwarantowanego algorytmu jednoprzebiegowego. To może być przydatne.
kbrimington,
4
Czy niektóre testy porównawcze. Okazuje się, że LINQ do Objects wykonuje pewne optymalizacje w oparciu o typ używanej kolekcji. Używając tablic, Lists i LinkedLists, rozwiązanie Jamesa jest zwykle szybsze, choć nie o rząd wielkości. Jeśli IEnumerable jest obliczany (np. Przez Enumerable.Range), rozwiązanie Jamesa trwa dłużej. Nie mogę wymyślić żadnego sposobu na zagwarantowanie pojedynczego przejścia bez wiedzy o implementacji lub kopiowania wartości do innej struktury danych.
StriplingWarrior
1
@ RedFilter - Wystarczająco uczciwy. Przypuszczam, że moje nawyki przywoływania ujawniły się tutaj. Dziękuję za bystre oko.
kbrimington
59
coll.Reverse().Take(N).Reverse().ToList();


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

AKTUALIZACJA: Aby rozwiązać problem clintp: a) Użycie zdefiniowanej powyżej metody TakeLast () rozwiązuje problem, ale jeśli naprawdę chcesz to zrobić bez dodatkowej metody, musisz to rozpoznać, gdy Enumerable.Reverse () może być użyty jako metoda rozszerzenia, nie musisz go używać w ten sposób:

List<string> mystring = new List<string>() { "one", "two", "three" }; 
mystring = Enumerable.Reverse(mystring).Take(2).Reverse().ToList();
James Curran
źródło
Problem z tym mam, jeśli mówię: List<string> mystring = new List<string>() { "one", "two", "three" }; mystring = mystring.Reverse().Take(2).Reverse(); dostaję błąd kompilatora, ponieważ .Reverse () zwraca void, a kompilator wybiera tę metodę zamiast Linq, która zwraca IEnumerable. Propozycje?
Clinton Pierce
1
Możesz rozwiązać ten problem, jawnie przekazując mystring do IEnumerable <String>: ((IEnumerable <String>) mystring) .Reverse (). Take (2) .Reverse ()
Jan Hettich
Łatwe i proste, ale wymaga dwukrotnego całkowitego odwrócenia kolejności. To może być najlepszy sposób
shashwat
Podoba mi się to oprócz przyjętej odpowiedzi z Kbrimington. Jeśli nie zależy ci na zamówieniu po uzyskaniu ostatnich Nrekordów, możesz pominąć drugi Reverse.
ZoolWay
@shashwat Nie odwraca kolejności dwa razy „całkowicie”. Drugie odwrócenie dotyczy tylko kolekcji N. przedmiotów. Ponadto, w zależności od sposobu implementacji Reverse (), pierwsze wywołanie może odwrócić tylko N elementów. (Implementacja .NET 4.0 skopiuje kolekcję do tablicy i zindeksuje ją wstecz)
James Curran
47

Uwaga : brakowało mi twojego pytania, które brzmiało Korzystanie z Linq , więc moja odpowiedź w rzeczywistości nie używa Linq.

Jeśli chcesz uniknąć buforowania nieludzkiej kopii całej kolekcji, możesz napisać prostą metodę, która robi to za pomocą połączonej listy.

Poniższa metoda doda każdą wartość znalezioną w oryginalnej kolekcji do listy połączonych i przycina listę połączoną do wymaganej liczby elementów. Ponieważ cały czas utrzymuje połączoną listę przyciętą do tej liczby elementów poprzez iterację w kolekcji, zachowa tylko kopię co najwyżej N elementów z oryginalnej kolekcji.

Nie wymaga znajomości liczby elementów w oryginalnej kolekcji ani powtarzania jej więcej niż jeden raz.

Stosowanie:

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

Metoda przedłużenia:

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;
    }
}
Lasse V. Karlsen
źródło
Nadal uważam, że masz dobrą, prawidłową odpowiedź, nawet jeśli technicznie nie używa Linq, więc nadal daję +1 :)
Matthew Groves
czyste, schludne i rozszerzalne +1!
Yasser Shaikh
1
Myślę, że to jedyne rozwiązanie, które nie powoduje dwukrotnego uruchomienia modułu wyliczającego źródła i nie wymusza zmaterializowania wyliczenia, więc w większości aplikacji powiedziałbym, że byłby znacznie bardziej wydajny pod względem pamięci i prędkości.
Sprotty
30

Oto metoda, która działa na dowolnym wyliczalnym, ale używa tylko tymczasowego magazynu 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];
        }
    }
}

Stosowanie:

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

Działa przy użyciu bufora pierścieniowego o rozmiarze N do przechowywania elementów, tak jak je widzi, zastępując stare elementy nowymi. Po osiągnięciu końca elementu wymiennego bufor bufora zawiera ostatnie N elementów.

Mark Byers
źródło
2
+1: Powinno to mieć lepszą wydajność niż moja, ale powinieneś upewnić się, że robi to dobrze, gdy kolekcja zawiera mniej elementów niż n.
Lasse V. Karlsen,
Cóż, przez większość czasu zakładam, że ludzie będą uważać podczas kopiowania kodu z SO do użytku produkcyjnego, aby samodzielnie dodać takie rzeczy, może to nie stanowić problemu. Jeśli zamierzasz go dodać, rozważ również sprawdzenie zmiennej kolekcjonowania pod kątem wartości null. W przeciwnym razie doskonałe rozwiązanie :) Zastanawiałem się nad użyciem bufora pierścieniowego, ponieważ połączona lista doda GC-presję, ale minęło trochę czasu, odkąd to zrobiłem i nie chciałem mieć kłopotów z testowaniem kodu jeśli zrobię to dobrze. Muszę powiedzieć, że zakochałem się w LINQPad :) linqpad.net
Lasse V. Karlsen
2
Możliwą optymalizacją byłoby sprawdzenie, czy wymienna zaimplementowana IList i użycie trywialnego rozwiązania, jeśli tak jest. Tymczasowe podejście do przechowywania byłoby wtedy potrzebne tylko do prawdziwie „strumieniowego” przesyłania IEnumerables
piers7
1
trywialny nit-pick: twoje argumenty na ArgumentOutOfRangeException są w niewłaściwej kolejności (mówi R #)
piers7
28

.NET Core 2.0+ zapewnia metodę LINQ TakeLast():

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

przykład :

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

// outputs:
// 8
// 9
// 10
Promień
źródło
Używam: NET Standard 2.0 i nie mam go dostępnego. Co jest nie tak? :(
SuperJMN
@ SuperJMN Chociaż możesz odwoływać się do bibliotek standardu .net 2.0, być może nie celujesz w poprawną wersję rdzenia dotnet w swoim projekcie. Ta metoda nie jest dostępna dla v1.x ( netcoreapp1.x), ale tylko dla v2.0 i v2.1 z dotnetcore ( netcoreapp2.x). Możliwe, że celujesz w pełną platformę (np. net472), Która również nie jest obsługiwana. (Standardowe biblioteki .net mogą być używane przez dowolne z powyższych, ale mogą ujawniać tylko niektóre interfejsy API specyficzne dla docelowego środowiska. zobacz docs.microsoft.com/en-us/dotnet/standard/frameworks )
Ray
1
Te muszą być teraz wyżej. Nie trzeba od nowa wymyślać koła
James Woodley,
11

Dziwi mnie, że nikt o tym nie wspominał, ale SkipWhile ma metodę wykorzystującą indeks elementu .

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)

Jedyną zauważalną zaletą tego rozwiązania w porównaniu z innymi jest to, że możesz mieć opcję dodania predykatu, aby utworzyć bardziej wydajne i wydajne zapytanie LINQ, zamiast dwóch osobnych operacji, które dwukrotnie przechodzą przez 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));
}
Nick Babcock
źródło
9

Użyj EnumerableEx.TakeLast w zestawie System.Interactive RX. Jest to implementacja O (N), taka jak @ Mark, ale wykorzystuje raczej kolejkę niż konstrukcję bufora pierścieniowego (i usuwa z kolejki elementy, gdy osiągnie pojemność bufora).

(Uwaga: jest to wersja IEnumerable - nie wersja IObservable, chociaż implementacja obu jest prawie identyczna)

mola7
źródło
To najlepsza odpowiedź. Nie rzucaj własną, jeśli jest odpowiednia biblioteka, która wykonuje zadanie, a zespół RX jest wysokiej jakości.
bradgonesurfing
Jeśli idziesz z tym, zainstaluj go z Nuget - nuget.org/packages/Ix-Async
nikib3ro
Czy C # nie jest Queue<T>implementowane przy użyciu bufora cyklicznego ?
tigrou
@tigrou. nie, nie jest okrągły
citykid
6

Jeśli masz do czynienia z kolekcją z kluczem (np. Wpisy z bazy danych), szybkim (tj. Szybszym niż wybrana odpowiedź) rozwiązaniem byłoby

collection.OrderByDescending(c => c.Key).Take(3).OrderBy(c => c.Key);
dav_i
źródło
+1 działa dla mnie i jest łatwy do odczytania, mam małą liczbę obiektów na mojej liście
fubo
5

Jeśli nie masz nic przeciwko zanurzeniu się w Rx jako części monady, możesz użyć TakeLast:

IEnumerable<int> source = Enumerable.Range(1, 10000);

IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();
Richard Szalay
źródło
2
Nie potrzebujesz AsObservable (), jeśli odwołujesz się do Systemu RX.Interactive zamiast System.Reactive (patrz moja odpowiedź)
piers7
2

Jeśli korzystanie z biblioteki innej firmy jest opcją, MoreLinq określa, TakeLast()co dokładnie to robi.

sm
źródło
2

Starałem się łączyć wydajność z prostotą i w efekcie:

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;
}

Informacje o wydajności: w języku C # Queue<T>jest implementowany za pomocą bufora cyklicznego, więc nie jest wykonywane tworzenie instancji obiektów w każdej pętli (tylko gdy kolejka rośnie). Nie ustawiłem pojemności kolejki (używając dedykowanego konstruktora), ponieważ ktoś może wywołać to rozszerzenie count = int.MaxValue. Aby uzyskać dodatkową wydajność, możesz sprawdzić, czy implementacja źródłowa, IList<T>a jeśli tak, bezpośrednio wyodrębnij ostatnie wartości za pomocą indeksów tablicowych.

tigrou
źródło
1

Trochę nieefektywne jest pobranie ostatniego N kolekcji za pomocą LINQ, ponieważ wszystkie powyższe rozwiązania wymagają iteracji w całej kolekcji. TakeLast(int n)w System.Interactivema również ten problem.

Jeśli masz listę, bardziej wydajną rzeczą jest wycięcie jej przy użyciu następującej metody

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

z

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;
}

i niektóre przypadki testowe

[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});
}
bradgonesurfing
źródło
1

Wiem, że jest za późno, aby odpowiedzieć na to pytanie. Ale jeśli pracujesz z kolekcją typu IList <> i nie obchodzi Cię kolejność zwracanej kolekcji, ta metoda działa szybciej. Użyłem odpowiedzi Marka Byersa i wprowadziłem małe zmiany. Więc teraz metoda TakeLast to:

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];
        }
    }
}

Do testu użyłem metody Mark Byers oraz Kbrimington's andswer . To jest 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();

A oto wyniki pobrania 10 elementów:

wprowadź opis zdjęcia tutaj

a dla pobrania 1000001 elementów wyniki to: wprowadź opis zdjęcia tutaj

Sasha
źródło
1

Oto moje rozwiązanie:

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

Kod jest nieco masywny, ale jako składnik wielokrotnego użytku, powinien działać tak dobrze, jak może w większości scenariuszy, i zachowa kod, który go używa. :-)

Mój TakeLastza nie-IList`1 opiera się na tym samym algorytmie bufora pierścieniowego, co w odpowiedziach @Mark Byers i @MackieChan w dalszej części. Ciekawe, jak podobne są - napisałem mój całkowicie niezależnie. Sądzę, że naprawdę istnieje tylko jeden sposób na prawidłowe wykonanie bufora pierścieniowego. :-)

Patrząc na odpowiedź @ kbrimington, można do tego dodać dodatkowe sprawdzenie, IQuerable<T>aby powrócić do podejścia, które działa dobrze z Entity Framework - zakładając, że to, co mam w tym momencie, nie działa.

Jonathan Gilbert
źródło
0

Poniżej prawdziwy przykład, jak pobrać ostatnie 3 elementy z kolekcji (tablicy):

// 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();
Aleksey Timkov
źródło
0

Korzystanie z tej metody, aby uzyskać cały zakres bez błędów

 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 ;
        }
Ali asghar Fendereski
źródło
0

Trochę inna implementacja z użyciem bufora cyklicznego. Testy porównawcze pokazują, że metoda jest około dwa razy szybsza niż metoda korzystająca z kolejki (implementacja TakeLast w System.Linq ), jednak nie bez kosztów - potrzebuje bufora, który rośnie wraz z wymaganą liczbą elementów, nawet jeśli masz mała kolekcja można uzyskać ogromny przydział pamięci.

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];

    }
}
Romasz
źródło