如何使用LINQ获取序列中除最后一个元素之外的所有元素?

问题描述 投票:114回答:21

假设我有一个序列。

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

获取序列并不便宜并且是动态生成的,我想只迭代一次。

我想得到0 - 999999(即除了最后一个元素之外的所有东西)

我知道我可以这样做:

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

但是这导致了两个大序列的枚举。

是否有LINQ结构可以让我这样做:

sequence.TakeAllButTheLastElement();
c# .net linq
21个回答
55
投票

我不知道Linq解决方案 - 但您可以使用生成器(yield return)轻松编写算法代码。

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

或者作为丢弃最后n个项目的一般化解决方案(使用评论中建议的队列):

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

2
投票

如果你能得到一个可枚举的CountLength,在大多数情况下你可以,那么只需要Take(n - 1)

数组示例

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

IEnumerable<T>的示例

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

1
投票

为什么不只是序列上的.ToList<type>(),然后调用count并像你原来那样进行。但是因为它被拉入列表,所以它不应该进行两次昂贵的枚举。对?


1
投票

我用于此问题的解决方案稍微复杂一些。

我的util static类包含一个扩展方法MarkEnd,它转换T-items中的EndMarkedItem<T>-items。每个元素都标有额外的int,它是0;或者(如果一个人对最后3个项目特别感兴趣)最后3个项目的-3,-2或-1。

这可能是有用的,例如,当你想在一个简单的foreach循环中创建一个列表,在每个元素之后用逗号除了最后一个2之后,倒数第二个项后跟一个连词(例如“和”或“或”),以及最后一个元素后跟一个点。

为了生成没有最后n个项目的整个列表,扩展方法ButLast简单地迭代EndMarkedItem<T>s而EndMark == 0

如果您未指定tailLength,则仅标记最后一项(在MarkEnd()中)或删除(在ButLast()中)。

与其他解决方案一样,这可以通过缓冲来实现。

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
投票

我不认为它可以比这更简洁 - 也确保处理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;
            }
        }
    }
}

编辑:技术上与this answer相同。


0
投票

你可以写:

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

0
投票

这是一个通用和恕我直言的优雅解决方案,将正确处理所有情况:

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
投票

我传统的IEnumerable方法:

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

-1
投票

一种简单的方法是只转换到一个队列并出列,直到只留下你想要跳过的项目数。

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

-2
投票

可能:

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

我想它应该像de“Where”但保留顺序(?)。


42
投票

作为创建自己的方法的替代方法,在元素顺序不重要的情况下,下一个将起作用:

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

-2
投票

如果要求速度,那么这种老式的学校方式应该是最快的,即使代码看起来不像linq那样顺利。

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

这要求序列是一个数组,因为它具有固定长度和索引项。


-4
投票

我可能会这样做:

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

这是一次迭代,但每次都检查它不是最后一次。


40
投票

因为我不是明确使用Enumerator的粉丝,所以这是另一种选择。请注意,需要使用包装器方法让无效参数尽早抛出,而不是推迟检查,直到实际枚举序列为止。

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的建议,它很容易推广到n个项目:

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

我现在在屈服之前缓冲而不是屈服之后,所以n == 0案例不需要特殊处理。


12
投票

BCL中没有任何东西(或者我相信的更多),但您可以创建自己的扩展方法。

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

10
投票

对于那些使用更新版本的.net的人来说,在.NET Core 2.0中添加了Enumerable.SkipLast(IEnumerable<TSource>, Int32)方法

var sequence = GetSequence();

var allExceptLast = sequence.SkipLast(1);

资料来源:https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.skiplast


7
投票

如果.NET Framework附带了这样的扩展方法,将会很有帮助。

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

4
投票

如果你没有时间推出自己的扩展,这里有一个更快的方法:

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

3
投票

Joren优雅的解决方案略有扩展:

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实现了一个简单的计数向前丢弃第一个left许多元素和相同的丢弃缓冲区,以丢弃最后的right许多元素。


2
投票

接受的答案略有不同(对我的口味而言)有点简单:

    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);
        }
    }
© www.soinside.com 2019 - 2024. All rights reserved.