LINQ kullanarak bir sıradaki son eleman hariç tümünü nasıl alabilirim?


131

Diyelim ki bir sekansım var.

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();
// sequence now contains: 0,1,2,3,...,999999,1000000

Sekans elde etmek ucuz değildir ve dinamik olarak üretilir ve bunu yalnızca bir kez yinelemek istiyorum.

0 - 999999 elde etmek istiyorum (yani son öğe hariç her şey)

Şunun gibi bir şey yapabileceğimin farkındayım:

sequence.Take(sequence.Count() - 1);

ancak bu, büyük dizi üzerinde iki numaralandırma ile sonuçlanır.

Şunları yapmama izin veren bir LINQ yapısı var mı:

sequence.TakeAllButTheLastElement();

3
Bir O (2n) zamanı veya O (sayım) alanı verimlilik algoritması arasında seçim yapmanız gerekir, burada ikincisi ayrıca dahili bir dizideki öğeleri taşımalıdır.
Dykam

1
Dario, lütfen o kadar önemli olmayan birine açıklar mısın?
alexn

Ayrıca şu yinelenen soruya bakın: stackoverflow.com/q/4166493/240733
stakx - artık

Koleksiyonu Listeye dönüştürüp ardından arayarak önbelleğe almayı bitirdim sequenceList.RemoveAt(sequence.Count - 1);. Benim durumumda kabul edilebilir, çünkü tüm LINQ işlemlerinden sonra onu diziye veya IReadOnlyCollectionyine de dönüştürmem gerekiyor . Önbelleğe almayı düşünmediğiniz durumlarda kullanım durumunuz nedir merak ediyorum? Görebildiğim gibi, onaylanmış bir cevap bile önbelleğe alma işlemi çok basit Listhale getiriyor, bence çok daha kolay ve daha kısa.
Pavels Ahmadulins

Yanıtlar:


64

Bir Linq çözümü bilmiyorum - Ancak algoritmayı, jeneratörleri kullanarak kendi başınıza kolayca kodlayabilirsiniz (verim dönüşü).

public static IEnumerable<T> TakeAllButLast<T>(this IEnumerable<T> source) {
    var it = source.GetEnumerator();
    bool hasRemainingItems = false;
    bool isFirst = true;
    T item = default(T);

    do {
        hasRemainingItems = it.MoveNext();
        if (hasRemainingItems) {
            if (!isFirst) yield return item;
            item = it.Current;
            isFirst = false;
        }
    } while (hasRemainingItems);
}

static void Main(string[] args) {
    var Seq = Enumerable.Range(1, 10);

    Console.WriteLine(string.Join(", ", Seq.Select(x => x.ToString()).ToArray()));
    Console.WriteLine(string.Join(", ", Seq.TakeAllButLast().Select(x => x.ToString()).ToArray()));
}

Veya son n öğeyi atan genel bir çözüm olarak (yorumlarda önerildiği gibi bir sıra kullanarak):

public static IEnumerable<T> SkipLastN<T>(this IEnumerable<T> source, int n) {
    var  it = source.GetEnumerator();
    bool hasRemainingItems = false;
    var  cache = new Queue<T>(n + 1);

    do {
        if (hasRemainingItems = it.MoveNext()) {
            cache.Enqueue(it.Current);
            if (cache.Count > n)
                yield return cache.Dequeue();
        }
    } while (hasRemainingItems);
}

static void Main(string[] args) {
    var Seq = Enumerable.Range(1, 4);

    Console.WriteLine(string.Join(", ", Seq.Select(x => x.ToString()).ToArray()));
    Console.WriteLine(string.Join(", ", Seq.SkipLastN(3).Select(x => x.ToString()).ToArray()));
}

7
Şimdi, son n hariç hepsini alacak şekilde genelleyebilir misiniz?
Eric Lippert

4
Güzel. Küçük bir hata; Kuyruğun maksimum boyutu olduğundan, kuyruk boyutu n + 1 olarak başlatılmalıdır.
Eric Lippert

ReSharper kodunuzu anlamıyor ( koşullu ifadede atama ) ama bunu beğendim +1
Грозный

44

Kendi yönteminizi oluşturmanın bir alternatifi olarak ve bir durumda eleman sırasının önemli olmadığı durumda, bir sonraki işe yarayacaktır:

var result = sequence.Reverse().Skip(1);

49
Bunun, tüm diziyi depolamak için yeterli belleğe sahip olmanızı gerektirdiğine dikkat edin ve tabii ki STILL, bir kez tersine çevrilmiş diziyi oluşturmak ve bir kez onu yinelemek için tüm diziyi iki kez yineler. Nasıl dilimlediğiniz önemli değil, bu neredeyse Count çözümünden daha kötüdür; daha yavaştır VE çok daha fazla bellek alır.
Eric Lippert

'Tersine çevirme' yönteminin nasıl çalıştığını bilmiyorum, bu yüzden kullandığı bellek miktarından emin değilim. Ama iki yinelemeye katılıyorum. Bu yöntem büyük koleksiyonlarda veya performans önemliyse kullanılmamalıdır.
Kamarey

5
Peki olur sen Ters uygulamak? Genel olarak tüm diziyi kaydetmeden bunu yapmanın bir yolunu bulabilir misiniz ?
Eric Lippert

2
Hoşuma gitti ve diziyi iki kez oluşturmama kriterini karşılıyor.
Amy B

12
ve buna ek olarak, equence.Reverse().Skip(1).Reverse()iyi bir çözüm olmadığı için tüm diziyi tekrar tersine çevirmeniz gerekecek
shashwat

42

Açıkça bir kullanmanın hayranı olmadığım için Enumerator, işte bir alternatif. Sarmalayıcı yöntemlerinin, dizi gerçekten numaralandırılana kadar denetimleri ertelemek yerine geçersiz bağımsız değişkenlerin erken atılmasına izin vermek için gerekli olduğunu unutmayın.

public static IEnumerable<T> DropLast<T>(this IEnumerable<T> source)
{
    if (source == null)
        throw new ArgumentNullException("source");

    return InternalDropLast(source);
}

private static IEnumerable<T> InternalDropLast<T>(IEnumerable<T> source)
{
    T buffer = default(T);
    bool buffered = false;

    foreach (T x in source)
    {
        if (buffered)
            yield return buffer;

        buffer = x;
        buffered = true;
    }
}

Eric Lippert'in önerisine göre, kolayca n maddeye genelleştirilebilir:

public static IEnumerable<T> DropLast<T>(this IEnumerable<T> source, int n)
{
    if (source == null)
        throw new ArgumentNullException("source");

    if (n < 0)
        throw new ArgumentOutOfRangeException("n", 
            "Argument n should be non-negative.");

    return InternalDropLast(source, n);
}

private static IEnumerable<T> InternalDropLast<T>(IEnumerable<T> source, int n)
{
    Queue<T> buffer = new Queue<T>(n + 1);

    foreach (T x in source)
    {
        buffer.Enqueue(x);

        if (buffer.Count == n + 1)
            yield return buffer.Dequeue();
    }
}

Şimdi teslim ettikten sonra yerine teslim etmeden önce arabelleğe aldığım yerde , böylece n == 0vaka özel bir işleme gerek duymaz.


İlk örnekte, buffered=falseatamadan önce başka bir cümle oluşturmak muhtemelen biraz daha hızlı olacaktır buffer. Koşul zaten zaten kontrol ediliyor, ancak bu buffereddöngü boyunca her seferinde gereksiz ayar yapılmasını önleyecektir .
James

Birisi bana bunun seçilen cevaba karşı artılarını / eksilerini söyleyebilir mi?
Sinjai

Uygulamanın girdi kontrollerinden yoksun ayrı bir yöntemde olmasının avantajı nedir? Ayrıca, tek uygulamayı bırakıp çoklu uygulamaya varsayılan bir değer verirdim.
jpmc26

@ jpmc26 Ayrı bir yöntemde kontrol ile, aradığınız anda doğrulama gerçekten alırsınız DropLast. Aksi takdirde, doğrulama yalnızca diziyi gerçekten numaralandırdığınızda gerçekleşir ( MoveNextsonuçtaki ilk çağrıda IEnumerator). IEnumerableOluşturmakla onu gerçekten numaralandırmak arasında keyfi miktarda kod olabileceği zaman, hata ayıklamak için eğlenceli bir şey değil. Bugünlerde 'nin InternalDropLastiçsel bir işlevi olarak DropLastyazıyordum, ancak bu işlevsellik 9 yıl önce bu kodu yazdığımda C #' da yoktu.
Joren

29

Enumerable.SkipLast(IEnumerable<TSource>, Int32)Yöntem, .NET Standard 2.1 ilave edildi. Tam olarak ne istiyorsan onu yapar.

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();

var allExceptLast = sequence.SkipLast(1);

Gönderen https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.skiplast

Kaynak koleksiyonun son sayı öğelerinin atlandığı kaynaktan öğeleri içeren yeni bir numaralandırılabilir koleksiyon döndürür.


2
Bu aynı zamanda MoreLinq
Leperkawn'da

SkipLast için +1. Bunu yakın zamanda .NET Framework'ten geldiğimden beri bilmiyordum ve buraya eklemek için zahmet etmediler.
PRMan

12

BCL'de (veya inandığım MoreLinq'te) hiçbir şey yok, ancak kendi uzantı yönteminizi oluşturabilirsiniz.

public static IEnumerable<T> TakeAllButLast<T>(this IEnumerable<T> source)
{
    using (var enumerator = source.GetEnumerator())
        bool first = true;
        T prev;
        while(enumerator.MoveNext())
        {
            if (!first)
                yield return prev;
            first = false;
            prev = enumerator.Current;
        }
    }
}

Bu kod çalışmayacak ... muhtemelen olmalı if (!first)ve if'den çekilmelidir first = false.
Caleb

Bu kod göze çarpmıyor. Mantık, ' previlk yinelemede başlatılmamış olanı döndür ve bundan sonra sonsuza kadar döngü ' gibi görünüyor .
Phil Miller

Kodda "derleme zamanı" hatası var gibi görünüyor. Düzeltmek isteyebilirsiniz. Ama evet, bir kez yinelenen ve son öğe hariç hepsini döndüren bir genişletici yazabiliriz.
Manish Basantani

@Caleb: Kesinlikle haklısın - Bunu çok aceleyle yazdım! Şimdi düzeltildi. @Amby: Hmm, hangi derleyiciyi kullandığınızı bilmiyorum. Hiçbir şeyim yoktu. : P
Noldorin

@RobertSchmidt Oops, evet. usingŞimdi bir açıklama ekledim .
Noldorin

7

.NET Framework'ün bunun gibi bir uzatma yöntemiyle gönderilmesi faydalı olacaktır.

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int count)
{
    var enumerator = source.GetEnumerator();
    var queue = new Queue<T>(count + 1);

    while (true)
    {
        if (!enumerator.MoveNext())
            break;
        queue.Enqueue(enumerator.Current);
        if (queue.Count > count)
            yield return queue.Dequeue();
    }
}

3

Joren'in zarif çözümünde küçük bir genişleme:

public static IEnumerable<T> Shrink<T>(this IEnumerable<T> source, int left, int right)
{
    int i = 0;
    var buffer = new Queue<T>(right + 1);

    foreach (T x in source)
    {
        if (i >= left) // Read past left many elements at the start
        {
            buffer.Enqueue(x);
            if (buffer.Count > right) // Build a buffer to drop right many elements at the end
                yield return buffer.Dequeue();    
        } 
        else i++;
    }
}
public static IEnumerable<T> WithoutLast<T>(this IEnumerable<T> source, int n = 1)
{
    return source.Shrink(0, n);
}
public static IEnumerable<T> WithoutFirst<T>(this IEnumerable<T> source, int n = 1)
{
    return source.Shrink(n, 0);
}

Shrink, ilk leftbirçok öğeyi bırakmak için ileri doğru basit bir sayım uyguladığında ve son rightbirçok öğeyi bırakmak için aynı atılan arabelleği kullanır .


3

kendi uzantınızı yayınlamaya vaktiniz yoksa, işte daha hızlı bir yol:

var next = sequence.First();
sequence.Skip(1)
    .Select(s => 
    { 
        var selected = next;
        next = s;
        return selected;
    });

2

Kabul edilen yanıtta (zevklerime göre) biraz daha basit olan küçük bir değişiklik:

    public static IEnumerable<T> AllButLast<T>(this IEnumerable<T> enumerable, int n = 1)
    {
        // for efficiency, handle degenerate n == 0 case separately 
        if (n == 0)
        {
            foreach (var item in enumerable)
                yield return item;
            yield break;
        }

        var queue = new Queue<T>(n);
        foreach (var item in enumerable)
        {
            if (queue.Count == n)
                yield return queue.Dequeue();

            queue.Enqueue(item);
        }
    }

2

Eğer bir numaralandırılabilirden Countveya Lengthbirinden alabilirseniz , ki çoğu durumda bunu yapabilirsiniz, o zaman sadeceTake(n - 1)

Dizilerle örnek

int[] arr = new int[] { 1, 2, 3, 4, 5 };
int[] sub = arr.Take(arr.Length - 1).ToArray();

Örnek IEnumerable<T>

IEnumerable<int> enu = Enumerable.Range(1, 100);
IEnumerable<int> sub = enu.Take(enu.Count() - 1);

Soru IEnumerables hakkındadır ve sizin çözümünüz OP'nin kaçınmaya çalıştığı şeydir. Kodunuzun performans etkisi vardır.
nawfal

1

Neden .ToList<type>()sırayla değil, sonra arama sayma ve başlangıçta yaptığınız gibi alın ... ama bir listeye alındığından, iki kez pahalı bir numaralandırma yapmamalı. Sağ?


1

Bu problem için kullandığım çözüm biraz daha karmaşık.

Kullanılan statik sınıfım -items içindeki -items'i MarkEnddönüştüren bir uzantı yöntemi içerir . Her öğe, 0 olan bir ekstra ile işaretlenir ; veya (son 3 maddeyle özellikle ilgilenilmesi durumunda) son 3 madde için -3 , -2 veya -1 .TEndMarkedItem<T>int

Bu, kendi başına yararlı olabilir, örneğinforeach , son 2 hariç her öğeden sonra virgül içeren basit bir döngüde, ikinciden son öğeye ve ardından bir bağlantılı sözcükle (" ve " gibi) bir liste oluşturmak istediğinizde veya " veya ") ve son öğe ve ardından bir nokta.

Son n öğe olmadan tüm listeyi oluşturmak için , uzantı yöntemi ButLastyalnızca EndMarkedItem<T>s while üzerinde yineler EndMark == 0.

Belirtmezseniz tailLength, yalnızca son öğe işaretlenir (içinde MarkEnd()) veya bırakılır (içinde ButLast()).

Diğer çözümler gibi, bu da arabelleğe alarak çalışır.

using System;
using System.Collections.Generic;
using System.Linq;

namespace Adhemar.Util.Linq {

    public struct EndMarkedItem<T> {
        public T Item { get; private set; }
        public int EndMark { get; private set; }

        public EndMarkedItem(T item, int endMark) : this() {
            Item = item;
            EndMark = endMark;
        }
    }

    public static class TailEnumerables {

        public static IEnumerable<T> ButLast<T>(this IEnumerable<T> ts) {
            return ts.ButLast(1);
        }

        public static IEnumerable<T> ButLast<T>(this IEnumerable<T> ts, int tailLength) {
            return ts.MarkEnd(tailLength).TakeWhile(te => te.EndMark == 0).Select(te => te.Item);
        }

        public static IEnumerable<EndMarkedItem<T>> MarkEnd<T>(this IEnumerable<T> ts) {
            return ts.MarkEnd(1);
        }

        public static IEnumerable<EndMarkedItem<T>> MarkEnd<T>(this IEnumerable<T> ts, int tailLength) {
            if (tailLength < 0) {
                throw new ArgumentOutOfRangeException("tailLength");
            }
            else if (tailLength == 0) {
                foreach (var t in ts) {
                    yield return new EndMarkedItem<T>(t, 0);
                }
            }
            else {
                var buffer = new T[tailLength];
                var index = -buffer.Length;
                foreach (var t in ts) {
                    if (index < 0) {
                        buffer[buffer.Length + index] = t;
                        index++;
                    }
                    else {
                        yield return new EndMarkedItem<T>(buffer[index], 0);
                        buffer[index] = t;
                        index++;
                        if (index == buffer.Length) {
                            index = 0;
                        }
                    }
                }
                if (index >= 0) {
                    for (var i = index; i < buffer.Length; i++) {
                        yield return new EndMarkedItem<T>(buffer[i], i - buffer.Length - index);
                    }
                    for (var j = 0; j < index; j++) {
                        yield return new EndMarkedItem<T>(buffer[j], j - index);
                    }
                }
                else {
                    for (var k = 0; k < buffer.Length + index; k++) {
                        yield return new EndMarkedItem<T>(buffer[k], k - buffer.Length - index);
                    }
                }
            }    
        }
    }
}

1
    public static IEnumerable<T> NoLast<T> (this IEnumerable<T> items) {
        if (items != null) {
            var e = items.GetEnumerator();
            if (e.MoveNext ()) {
                T head = e.Current;
                while (e.MoveNext ()) {
                    yield return head; ;
                    head = e.Current;
                }
            }
        }
    }

1

Bundan daha özlü olabileceğini düşünmüyorum - ayrıca aşağıdakileri ortadan kaldırmayı da garanti ediyor IEnumerator<T>:

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source)
{
    using (var it = source.GetEnumerator())
    {
        if (it.MoveNext())
        {
            var item = it.Current;
            while (it.MoveNext())
            {
                yield return item;
                item = it.Current;
            }
        }
    }
}

Düzenleme: teknik olarak bu cevapla aynı .


1

C # 8.0 ile bunun için Aralıkları ve endeksleri kullanabilirsiniz .

var allButLast = sequence[..^1];

Varsayılan olarak C # 8.0, .NET Core 3.0 veya .NET Standard 2.1 (veya üzeri) gerektirir. Daha eski uygulamalarla kullanmak için bu diziyi kontrol edin .


0

Yazabilirsin:

var list = xyz.Select(x=>x.Id).ToList();
list.RemoveAt(list.Count - 1);

0

Bu, tüm durumları doğru şekilde ele alacak genel ve IMHO zarif bir çözümdür:

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
    public static void Main()
    {
        IEnumerable<int> r = Enumerable.Range(1, 20);
        foreach (int i in r.AllButLast(3))
            Console.WriteLine(i);

        Console.ReadKey();
    }
}

public static class LinqExt
{
    public static IEnumerable<T> AllButLast<T>(this IEnumerable<T> enumerable, int n = 1)
    {
        using (IEnumerator<T> enumerator = enumerable.GetEnumerator())
        {
            Queue<T> queue = new Queue<T>(n);

            for (int i = 0; i < n && enumerator.MoveNext(); i++)
                queue.Enqueue(enumerator.Current);

            while (enumerator.MoveNext())
            {
                queue.Enqueue(enumerator.Current);
                yield return queue.Dequeue();
            }
        }
    }
}

-1

Geleneksel IEnumerableyaklaşımım:

/// <summary>
/// Skips first element of an IEnumerable
/// </summary>
/// <typeparam name="U">Enumerable type</typeparam>
/// <param name="models">The enumerable</param>
/// <returns>IEnumerable of type skipping first element</returns>
private IEnumerable<U> SkipFirstEnumerable<U>(IEnumerable<U> models)
{
    using (var e = models.GetEnumerator())
    {
        if (!e.MoveNext()) return;
        for (;e.MoveNext();) yield return e.Current;
        yield return e.Current;
    }
}

/// <summary>
/// Skips last element of an IEnumerable
/// </summary>
/// <typeparam name="U">Enumerable type</typeparam>
/// <param name="models">The enumerable</param>
/// <returns>IEnumerable of type skipping last element</returns>
private IEnumerable<U> SkipLastEnumerable<U>(IEnumerable<U> models)
{
    using (var e = models.GetEnumerator())
    {
        if (!e.MoveNext()) return;
        yield return e.Current;
        for (;e.MoveNext();) yield return e.Current;
    }
}

SkipLastEnumerable'ınız geleneksel olabilir, ancak bozuktur. Döndürdüğü ilk öğe her zaman tanımlanmamış bir U'dur - "modeller" 1 öğesi olsa bile. İkinci durumda, boş bir sonuç beklerdim.
Robert Schmidt

Ayrıca, IEnumerator <T> IDisposable'dır.
Robert Schmidt

Doğru / kaydetti. Teşekkür ederim.
Chibueze Opata

-1

Basit bir yol, yalnızca bir kuyruğa dönüştürmek ve yalnızca atlamak istediğiniz öğe sayısı kalana kadar sırayı kaldırmaktır.

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int n)
{
    var queue = new Queue<T>(source);

    while (queue.Count() > n)
    {
        yield return queue.Dequeue();
    }
}

Bilinen sayıda öğeyi almak için kullanılan Take vardır . Ve yeterince büyük numaralandırılabilir kuyruk berbattır.
Sinatr

-2

Olabilirdi:

var allBuLast = sequence.TakeWhile(e => e != sequence.Last());

Sanırım de "Nerede" gibi olmalı ama düzeni koruyarak (?).


3
Bu, bunu yapmanın çok verimsiz bir yolu. Sırayı değerlendirmek için Son (), dizideki her öğe için bunu yaparak tüm diziyi geçmelidir. O (n ^ 2) verimliliği.
Mike

Haklısın. Bu XD'yi yazarken ne düşündüğümü bilmiyorum. Her neyse, Last () 'in tüm diziyi geçeceğinden emin misiniz? IEnumerable'ın (Array gibi) bazı uygulamaları için bu O (1) olmalıdır. List uygulamasını kontrol etmedim, ancak son öğeden başlayan ve O (1) 'i de alacak bir "ters" yineleyiciye sahip olabilir. Ayrıca, en azından teknik olarak O (n) = O (2n) olduğunu hesaba katmalısınız. Dolayısıyla, bu prosedür uygulama performansınız için kesinlikle kritik değilse, çok daha net bir sıraya bağlı kalırım. (Sequence.Count () - 1) alın.
Guillermo Ares

@Mike Seninle aynı fikirde değilim mate, sequence.Last (), O (1) 'dir, böylece tüm diziyi geçmesine gerek kalmaz. stackoverflow.com/a/1377895/812598
GoRoS

1
@GoRoS, eğer dizi ICollection / IList uyguluyorsa veya bir Array ise sadece O (1). Diğer tüm diziler O (N) 'dir. Soruma göre, O (1) kaynaklarından birinin
Mike

3
Dizi, bu koşulu karşılayan birkaç öğeye sahip olabilir e ==
sıra.Last

-2

Hız bir zorunluluksa, kod linq'in yapabileceği kadar pürüzsüz görünmese de, bu eski okul yolu en hızlı yol olmalıdır.

int[] newSequence = int[sequence.Length - 1];
for (int x = 0; x < sequence.Length - 1; x++)
{
    newSequence[x] = sequence[x];
}

Bu, dizinin sabit bir uzunluğa ve dizinlenmiş öğelere sahip olması nedeniyle bir dizi olmasını gerektirir.


2
Bir dizin aracılığıyla öğelere erişime izin vermeyen bir IEnumerable ile uğraşıyorsunuz. Çözümünüz işe yaramıyor. Doğru yaptığınızı varsayarsak, uzunluğu belirlemek için diziyi geçmenizi, n-1 uzunluğunda bir dizi tahsis etmeyi, tüm öğeleri kopyalamayı gerektirir. - 1. 2n-1 işlemleri ve (2n-1) * (4 veya 8 bayt) bellek. Bu hızlı bile değil.
Tarık

-4

Muhtemelen şöyle bir şey yapardım:

sequence.Where(x => x != sequence.LastOrDefault())

Bu, her seferinde son olmadığını kontrol eden bir yinelemedir.


3
Bunun yapılacak iyi bir şey olmadığı iki neden var. 1) .LastOrDefault (), tüm dizinin yinelenmesini gerektirir ve bu, dizideki her öğe için çağrılır (.Where () öğesinde). 2) Dizi [1,2,1,2,1,2] ise ve tekniğinizi kullandıysanız, [1,1,1] ile kalacaksınız.
Mike
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.