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

假设我有一个序列。

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

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

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

我意识到我可以这样做:

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

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

有没有一个 LINQ 结构可以让我这样做:

sequence.TakeAllButTheLastElement();
70480 次浏览

我不知道 Linq 的解决方案-但是你可以使用生成器轻松地自己编写算法(收益返回)。

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

BCL 中没有(我相信是 MoreLinq) ,但是您可以创建自己的扩展方法。

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

作为创建自己的方法的一种替代方法,在某些情况下,元素顺序并不重要,下一个方法将会奏效:

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

因为我不喜欢明确地使用 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的情况下不需要特殊的处理。

为什么不只是 .ToList<type>()的序列,然后调用计数和采取像您最初做的。.但是因为它已经被放入了一个列表中,所以它不应该执行两次昂贵的枚举。对吧?

对乔伦优雅解决方案的一个小小的扩展:

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

其中,收缩实现了一个简单的前向计数,以删除第一个 left多个元素和相同的丢弃缓冲区,以删除最后的 right多个元素。

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

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

我可能会这样做:

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

这是一个带有检查的迭代,但不是每次的最后一个。

可能是:

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

我想它应该像 de“ Where”一样,但是保留了顺序(?)。

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

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

对于公认答案的一个细微变化,(就我的口味而言)要简单一些:

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

如果速度是一个必要条件,那么这种老式的方法应该是最快的,即使代码看起来不像 linq 可以做到的那样流畅。

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

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

我用来解决这个问题的方法稍微复杂一些。

我的 util 静态类包含一个扩展方法 MarkEnd,它可以转换 EndMarkedItem<T>-item 中的 T-item。每个元素都标记了一个额外的 int,它是 0; 或者(如果一个元素对最后3个元素特别感兴趣的话)对于最后3个元素是 -3-2或者 -1

如果您想在一个简单的 foreach循环中创建一个列表,除了最后2个元素之外,每个元素后面都有逗号,倒数第二个元素后面跟一个连词(比如“ 还有”或“ 或者”) ,最后一个元素后面跟一个点,那么 例如:。本身就很有用。

为了生成不包含最后一个 N项的整个列表,扩展方法 ButLast只是在 EndMarkedItem<T>上迭代,而 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);
}
}
}
}
}
}
    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;
}
}
}
}

如果您可以获得可枚举数的 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);

这是一个通用的、恕我直言的优雅解决方案,能够正确处理所有案件:

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

你可以写:

var list = xyz.Select(x=>x.Id).ToList();
list.RemoveAt(list.Count - 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;
}
}

我认为没有比这更简洁的了——同时也确保了 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;
}
}
}
}

编辑: 技术上与 这个答案相同。

一个简单的方法是直接转换为队列并退出队列,直到只剩下要跳过的项数。

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

Enumerable.SkipLast(IEnumerable<TSource>, Int32)方法是在.NET Standard 2.1中添加的。

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();


var allExceptLast = sequence.SkipLast(1);

来自 https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.skiplast

返回一个新的可枚举集合,该集合包含来自源的元素,其中省略了源集合的最后一个 count 元素。

使用 C # 8.0,您可以使用 范围和指数

var allButLast = sequence[..^1];

默认情况下,C # 8.0需要。NET Core 3.0或。NET 标准2.1(或以上)。选中 这根线以使用较旧的实现。