Menggunakan Linq untuk mendapatkan elemen N terakhir dari koleksi?

284

Diberikan koleksi, apakah ada cara untuk mendapatkan elemen N terakhir dari koleksi itu? Jika tidak ada metode dalam kerangka kerja, apa cara terbaik untuk menulis metode ekstensi untuk melakukan ini?

Matthew Groves
sumber

Jawaban:

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

Pendekatan ini mempertahankan pesanan barang tanpa ketergantungan pada penyortiran apa pun, dan memiliki kompatibilitas luas di beberapa penyedia LINQ.

Penting untuk berhati-hati agar tidak menelepon Skipdengan nomor negatif. Beberapa penyedia, seperti Kerangka Entitas, akan menghasilkan ArgumentException ketika disajikan dengan argumen negatif. Panggilan untuk Math.Maxmenghindari ini dengan rapi.

Kelas di bawah ini memiliki semua hal penting untuk metode ekstensi, yaitu: kelas statis, metode statis, dan penggunaan thiskata kunci.

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

Catatan singkat tentang kinerja:

Karena panggilan ke Count()dapat menyebabkan enumerasi struktur data tertentu, pendekatan ini memiliki risiko menyebabkan dua lewati data. Ini sebenarnya bukan masalah bagi sebagian besar enumerable; pada kenyataannya, optimisasi sudah ada untuk Daftar, Array, dan bahkan kueri EF untuk mengevaluasi Count()operasi dalam waktu O (1).

Namun, jika Anda harus menggunakan enumerable hanya maju dan ingin menghindari membuat dua lintasan, pertimbangkan algoritma satu lintasan seperti dijelaskan Lasse V. Karlsen atau Mark Byers . Kedua pendekatan ini menggunakan buffer sementara untuk menahan item saat enumerasi, yang dihasilkan setelah akhir koleksi ditemukan.

kbrimington
sumber
2
+1, karena ini berfungsi di Linq to Entities / SQL. Saya menduga itu juga lebih performant di Linq to Objects daripada strategi James Curran.
StriplingWarrior
11
Tergantung pada sifat koleksi. Hitung () mungkin O (N).
James Curran
3
@ James: Benar sekali. Jika berurusan secara ketat dengan koleksi IEnumerable, ini bisa menjadi kueri dua-pass. Saya akan sangat tertarik melihat algoritma 1-pass yang dijamin. Mungkin bermanfaat.
kbrimington
4
Melakukan beberapa tolok ukur. Ternyata LINQ to Objects melakukan beberapa optimasi berdasarkan pada jenis koleksi yang Anda gunakan. Menggunakan array, Lists, dan LinkedLists, solusi James cenderung lebih cepat, meskipun bukan dengan urutan besarnya. Jika IEnumerable dihitung (melalui Enumerable.Range, misal), solusi James membutuhkan waktu lebih lama. Saya tidak bisa memikirkan cara apa pun untuk menjamin satu pass tanpa mengetahui sesuatu tentang implementasi atau menyalin nilai ke struktur data yang berbeda.
StriplingWarrior
1
@RedFilter - Cukup adil. Saya kira kebiasaan paging saya bocor di sini. Terima kasih untuk mata kamu yang tajam.
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();
}

UPDATE: Untuk mengatasi masalah clintp: a) Menggunakan metode TakeLast () yang saya definisikan di atas menyelesaikan masalah, tetapi jika Anda benar-benar ingin melakukannya tanpa metode tambahan, maka Anda hanya harus mengenalinya sementara Enumerable.Reverse () dapat berupa digunakan sebagai metode ekstensi, Anda tidak perlu menggunakannya seperti itu:

List<string> mystring = new List<string>() { "one", "two", "three" }; 
mystring = Enumerable.Reverse(mystring).Take(2).Reverse().ToList();
James Curran
sumber
Masalah yang saya miliki dengan ini, adalah jika saya mengatakan: List<string> mystring = new List<string>() { "one", "two", "three" }; mystring = mystring.Reverse().Take(2).Reverse(); Saya mendapatkan kesalahan kompiler karena .Reverse () mengembalikan batal dan kompiler memilih metode itu daripada Linq yang mengembalikan IEnumerable. Saran?
Clinton Pierce
1
Anda dapat memecahkan masalah ini dengan secara eksplisit mengirimkan mystring ke IEnumerable <String>: ((IEnumerable <String>) mystring) .Reverse (). Ambil (2) .Reverse ()
Jan Hettich
Mudah dan cukup sederhana tetapi membutuhkan membalik urutan dua kali sepenuhnya. Ini bisa menjadi cara terbaik
shashwat
Saya suka selain jawaban yang diterima dari kbrimington. Jika Anda tidak peduli tentang pesanan setelah Anda memiliki Ncatatan terakhir, Anda dapat melewati yang kedua Reverse.
ZoolWay
@shashwat Itu tidak membalik urutan dua kali "sepenuhnya". Pembalikan kedua hanya berlaku untuk koleksi item N. Lebih lanjut, tergantung bagaimana Reverse () diterapkan panggilan pertama untuk itu hanya dapat membalikkan N item. (Implementasi .NET 4.0 akan menyalin koleksi ke array, dan indeks mundur dari itu)
James Curran
47

Catatan : Saya melewatkan judul pertanyaan Anda yang mengatakan Menggunakan Linq , jadi jawaban saya sebenarnya tidak menggunakan Linq.

Jika Anda ingin menghindari caching salinan non-malas dari seluruh koleksi, Anda bisa menulis metode sederhana yang melakukannya dengan menggunakan daftar tertaut.

Metode berikut akan menambahkan setiap nilai yang ditemukan dalam koleksi asli ke dalam daftar tertaut, dan memangkas daftar tertaut ke jumlah item yang diperlukan. Karena itu menjaga daftar yang ditautkan terpotong ke jumlah item ini sepanjang waktu melalui iterasi melalui koleksi, itu hanya akan menyimpan salinan paling banyak N item dari koleksi asli.

Anda tidak perlu tahu jumlah item dalam koleksi asli, atau mengulanginya lebih dari satu kali.

Pemakaian:

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

Metode ekstensi:

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
sumber
Saya masih berpikir Anda memiliki jawaban yang baik dan valid bahkan jika itu tidak secara teknis menggunakan Linq, jadi saya masih memberi Anda +1 :)
Matthew Groves
bersih, rapi, dan dapat diperpanjang +1!
Yasser Shaikh
1
Saya pikir ini satu-satunya solusi yang tidak menyebabkan enumerator sumber dijalankan melalui dua kali (atau lebih), dan tidak memaksa materialisasi enumerasi, jadi dalam kebanyakan aplikasi saya akan mengatakan itu akan jauh lebih efisien dalam hal memori dan kecepatan.
Sprotty
30

Berikut adalah metode yang berfungsi pada enumerable apa pun tetapi hanya menggunakan penyimpanan sementara 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];
        }
    }
}

Pemakaian:

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

Ia bekerja dengan menggunakan penyangga cincin ukuran N untuk menyimpan elemen-elemen seperti yang dilihatnya, menimpa elemen lama dengan yang baru. Ketika akhir enumerable tercapai, buffer cincin berisi elemen N terakhir.

Mark Byers
sumber
2
+1: Ini harus memiliki kinerja yang lebih baik daripada milik saya, tetapi Anda harus memastikan itu melakukan hal yang benar ketika koleksi mengandung lebih sedikit elemen daripada n.
Lasse V. Karlsen
Yah, sebagian besar waktu saya berasumsi orang akan berhati-hati ketika menyalin kode dari SO untuk penggunaan produksi untuk menambahkan hal-hal seperti itu sendiri, mungkin tidak menjadi masalah. Jika Anda akan menambahkannya, pertimbangkan juga memeriksa variabel koleksi untuk null. Kalau tidak, solusi yang sangat bagus :) Saya sendiri sedang mempertimbangkan untuk menggunakan buffer-cincin, karena daftar yang ditautkan akan menambah tekanan-GC, tapi sudah lama sejak saya melakukannya dan saya tidak ingin repot dengan kode-tes untuk mencari tahu jika saya melakukannya dengan benar. Saya harus mengatakan saya jatuh cinta dengan LINQPad :) :) linqpad.net
Lasse V. Karlsen
2
Kemungkinan pengoptimalan adalah memeriksa apakah enistable mengimplementasikan IList, dan menggunakan solusi sepele jika ya. Pendekatan penyimpanan sementara hanya akan diperlukan untuk benar-benar 'streaming' IEnumerables
piers7
1
trivial nit-pick: argumen Anda ke ArgumentOutOfRangeException berada di urutan yang salah (R # mengatakan)
piers7
28

.NET Core 2.0+ menyediakan metode LINQ TakeLast():

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

contoh :

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

// outputs:
// 8
// 9
// 10
sinar
sumber
Saya menggunakan: NET Standard 2.0 dan saya tidak memilikinya. Apa yang salah? :(
SuperJMN
@SuperJMN Meskipun Anda mungkin merujuk pustaka .net standar 2.0, Anda mungkin tidak menargetkan versi inti dotnet yang benar dalam proyek Anda. Metode ini tidak tersedia untuk v1.x ( netcoreapp1.x) tetapi hanya untuk v2.0 & v2.1 dari dotnetcore ( netcoreapp2.x). Mungkin saja Anda menargetkan kerangka kerja lengkap (mis. net472) Yang juga tidak didukung. (.net lib standar dapat digunakan oleh salah satu di atas tetapi hanya dapat mengekspos API tertentu khusus untuk kerangka kerja target. lihat docs.microsoft.com/en-us/dotnet/standard/frameworks )
Ray
1
Ini harus lebih tinggi sekarang. Tidak perlu menemukan kembali roda
James Woodley
11

Saya terkejut bahwa tidak ada yang menyebutkannya, tetapi SkipWhile memang memiliki metode yang menggunakan indeks elemen .

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)

Satu-satunya manfaat yang dapat dirasakan yang disajikan solusi ini daripada yang lain adalah bahwa Anda dapat memiliki opsi untuk menambahkan predikat untuk membuat kueri LINQ yang lebih kuat dan efisien, alih-alih memiliki dua operasi terpisah yang melintasi IEnumerable dua kali.

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
sumber
9

Gunakan EnumerableEx.TakeLast di Sistem RX.Rakitan interaktif. Ini adalah implementasi O (N) seperti @ Mark's, tetapi menggunakan antrian alih-alih konstruksi cincin-buffer (dan mengeluarkan item ketika mencapai kapasitas buffer).

(NB: Ini adalah versi IEnumerable - bukan versi IObservable, meskipun implementasi keduanya cukup identik)

dermaga7
sumber
Ini jawaban terbaik. Jangan menggulung sendiri jika ada perpustakaan yang cocok yang berfungsi dan tim RX berkualitas tinggi.
bradgonesurfing
Jika Anda akan melakukannya, instal dari Nuget - nuget.org/packages/Ix-Async
nikib3ro
Bukankah C # Queue<T>diimplementasikan menggunakan buffer lingkaran ?
tigrou
@tigrou. tidak, ini bukan bundar
citykid
6

Jika Anda berurusan dengan koleksi dengan kunci (misalnya entri dari database) solusi cepat (yaitu lebih cepat dari jawaban yang dipilih) akan menjadi

collection.OrderByDescending(c => c.Key).Take(3).OrderBy(c => c.Key);
dav_i
sumber
+1 berfungsi untuk saya dan mudah dibaca, saya memiliki sejumlah kecil objek dalam daftar saya
fubo
5

Jika Anda tidak keberatan menggunakan Rx sebagai bagian dari monad, Anda dapat menggunakan TakeLast:

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

IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();
Richard Szalay
sumber
2
Anda tidak perlu AsObservable () jika Anda mereferensikan System's RX.Interactive alih-alih System.Reactive (lihat jawaban saya)
piers7
2

Jika menggunakan pustaka pihak ketiga adalah sebuah opsi, MoreLinq menentukan TakeLast()mana yang melakukan hal ini.

sm
sumber
2

Saya mencoba menggabungkan efisiensi dan kesederhanaan dan akhirnya dengan ini:

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

Tentang kinerja: Di C #, Queue<T>diimplementasikan menggunakan buffer lingkaran sehingga tidak ada instantiasi objek yang dilakukan setiap loop (hanya ketika antrian tumbuh dewasa). Saya tidak menetapkan kapasitas antrian (menggunakan konstruktor khusus) karena seseorang mungkin memanggil ekstensi ini dengan count = int.MaxValue. Untuk kinerja tambahan, Anda dapat memeriksa apakah sumber mengimplementasikan IList<T>dan jika ya, ekstrak langsung nilai terakhir menggunakan indeks array.

tigrou
sumber
1

Agak tidak efisien untuk mengambil N terakhir dari sebuah koleksi menggunakan LINQ karena semua solusi di atas memerlukan iterasi di seluruh koleksi. TakeLast(int n)diSystem.Interactive juga memiliki masalah ini.

Jika Anda memiliki daftar, hal yang lebih efisien untuk dilakukan adalah mengirisnya menggunakan metode berikut

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

dengan

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

dan beberapa kasus uji

[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
sumber
1

Saya tahu sudah terlambat untuk menjawab pertanyaan ini. Tetapi jika Anda bekerja dengan koleksi tipe IList <> dan Anda tidak peduli dengan urutan koleksi yang dikembalikan, maka metode ini bekerja lebih cepat. Saya telah menggunakan jawaban Mark Byers dan membuat sedikit perubahan. Jadi sekarang metode TakeLast adalah:

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

Untuk tes saya telah menggunakan metode Mark Byers dan kbrimington's andswer . Ini adalah tes:

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

Dan berikut ini adalah hasil untuk mengambil 10 elemen:

masukkan deskripsi gambar di sini

dan untuk mengambil 1000001 hasil elemen adalah: masukkan deskripsi gambar di sini

Sasha
sumber
1

Inilah solusi saya:

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

Kode ini sedikit chunky, tetapi sebagai komponen drop-in yang dapat digunakan kembali, harus berfungsi sebaik mungkin di sebagian besar skenario, dan itu akan membuat kode yang menggunakannya bagus dan ringkas. :-)

Saya TakeLastuntuk non-IList`1 didasarkan pada algoritma buffer ring yang sama dengan yang dijawab oleh @Mark Byers dan @MackieChan. Sangat menarik betapa miripnya mereka - saya menulis milik saya sepenuhnya secara independen. Kira hanya ada satu cara untuk melakukan buffer cincin dengan benar. :-)

Melihat jawaban @ kbrimington, pemeriksaan tambahan dapat ditambahkan ke sini untuk IQuerable<T>kembali ke pendekatan yang bekerja dengan baik dengan Kerangka Entitas - dengan asumsi bahwa apa yang saya miliki saat ini tidak.

Jonathan Gilbert
sumber
0

Di bawah contoh nyata bagaimana mengambil 3 elemen terakhir dari koleksi (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();
Aleksey Timkov
sumber
0

Menggunakan Metode Ini Untuk Mendapatkan Semua Jangkauan Tanpa Kesalahan

 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
sumber
0

Implementasinya sedikit berbeda dengan penggunaan buffer lingkaran. Benchmark menunjukkan bahwa metode ini sekitar dua kali lebih cepat daripada yang menggunakan Antrian (implementasi TakeLast di System.Linq ), namun bukan tanpa biaya - perlu buffer yang tumbuh bersama dengan jumlah elemen yang diminta, bahkan jika Anda memiliki koleksi kecil Anda bisa mendapatkan alokasi memori yang sangat besar.

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
sumber