不要使用LINQ C#中的最后一条记录。

4
我们都知道Skip()可以省略集合开头不需要的记录。
但是有没有一种方法可以在集合末尾跳过记录呢?
如何不选取集合中的最后一条记录?
还是必须通过Take()来实现?
例如以下代码:
var collection = MyCollection

var listCount = collection.Count();

var takeList = collection.Take(listCount - 1);

这是在集合中排除最后一条记录的唯一方法吗?


1
有一个名为MoreLinq的库提供了许多功能,其中之一是SkipLast,它本质上与您所做的相同。 - Dialecticus
如果无法在不枚举的情况下确定计数,则“SkipLast”还会执行一些额外的魔法,因此代码仅枚举一次,而不是两次。 - Dialecticus
@Dialecticus,SkipLast在我的MoreLing库中没有显示出来。它是一个单独的属性吗? - KyloRen
1
它位于 MoreLinq.MoreEnumerable 命名空间中,就像其他所有内容一样。如果它不在那里,您可能有旧版本的库。您还可以从我提供的第二个链接中复制粘贴代码。只需复制几个源,因为它们依赖于库的其他部分。 - Dialecticus
请查看标记的重复项。两者都包括通用的“跳过最后N个”实现,类似于下面提出的那些。 - Peter Duniho
@Dialecticus,谢谢,我找到了。 我在我的测试项目中没有引用MoreLinq。 - KyloRen
4个回答

5
使用enumerator,您可以通过一次枚举有效地延迟产生输出。
public static IEnumerable<T> WithoutLast<T>(this IEnumerable<T> source)
{
    using (IEnumerator<T> e = source.GetEnumerator()) 
    {
        if (e.MoveNext() == false) yield break;

        var current = e.Current;
        while (e.MoveNext())
        {
            yield return current;
            current = e.Current;
        }
    }   
}

用法

var items = new int[] {};
items.WithoutLast(); // returns empty

var items = new int[] { 1 };
items.WithoutLast(); // returns empty

var items = new int[] { 1, 2 };
items.WithoutLast(); // returns { 1 }

var items = new int[] { 1, 2, 3 };
items.WithoutLast(); // returns { 1, 2 }

1
请解释一下“踩”,如果我的回答有误,我很乐意进行更正或删除。 - Fabio
@Fabio:但这只能跳过最后一个。那么如何跳过最后5个项目呢? - user1562155
2
@HenrikHansen,OP的问题是关于跳过最后一条记录:“如何不获取集合中的最后一条记录?”我想知道为什么每个人都试图实现跳过多个项目。YAGNI? ;) - Fabio
对于多个项目,您需要在开始逐个生成它们之前存储所需数量的项目,当存储大小超过给定计数时。其他答案已经提供了有价值的解决方案。 - Fabio
@PeterDuniho,你选择了适合你方法的问题句子,我选择了另一个 ;) - Fabio
显示剩余2条评论

3

Henrik Hansen的答案稍有不同:

static public IEnumerable<TSource> SkipLast<TSource>(
    this IEnumerable<TSource> source, int count)
{
    if (count < 0) count = 0;
    var queue = new Queue<TSource>(count + 1);
    foreach (TSource item in source)
    {
        queue.Enqueue(item);
        if (queue.Count > count) yield return queue.Dequeue();
    }
}

2

这个怎么样:

最初的回答
static public IEnumerable<T> SkipLast<T>(this IEnumerable<T> data, int count)
{
  if (data == null || count < 0) yield break;

  Queue<T> queue = new Queue<T>(data.Take(count));

  foreach (T item in data.Skip(count))
  {
    queue.Enqueue(item);
    yield return queue.Dequeue();
  }
}

更新

在一些评论的帮助下,基于相同思路的优化版本可以是:

static public IEnumerable<T> SkipLast<T>(this IEnumerable<T> data, int count)
{
  if (data == null) throw new ArgumentNullException(nameof(data));
  if (count <= 0) return data;

  if (data is ICollection<T> collection)
    return collection.Take(collection.Count - count);

  IEnumerable<T> Skipper()
  {
    using (var enumer = data.GetEnumerator())
    {
      T[] queue = new T[count];
      int index = 0;

      while (index < count && enumer.MoveNext())
        queue[index++] = enumer.Current;

      index = -1;
      while (enumer.MoveNext())
      {
        index = (index + 1) % count;
        yield return queue[index];
        queue[index] = enumer.Current;
      }
    }
  }

  return Skipper();
}

2
就我个人而言,我喜欢这种方法。在这里,我本以为可以度过一个慵懒的周末,但你的代码却要求我必须保持清醒,认真地看着它。对我来说,这是一个真正的头脑风暴...;-) - user2819245
@elgonzo:谢谢你的评论。显然,我无法将伊恩·默瑟先生从他周末的懒散状态中唤醒,以便他能提供一个无效的例子 :-) - user1562155
2
是的,抱歉,你说得对;它确实可以工作,但可能会枚举前count个元素两次,这并不理想。最好在迭代所有元素时,只有在通过count个元素后才从队列中yield return - Ian Mercer
@TheodorZoulias:同意:第一个版本不好,但您能详细说明一下您对第二个版本的评论吗? - user1562155
1
ICollection 优化非常有用。而显式的循环缓冲区则不太实用,因为它本质上就是 Queue<T> 的内部实现。你可以直接使用 Queue<T>,这样可以获得相同的性能,但代码更加简单易懂。 - Peter Duniho
显示剩余2条评论

1
一种方法是:
var result = l.Reverse().Skip(1);

如果需要的话,可以进行另一次反转以将它们恢复到原始顺序。

1
Reverse 方法会将源序列的所有元素存储在缓冲区中,然后按相反的顺序枚举缓冲区。这种方法对于解决问题来说效率非常低下。 - Theodor Zoulias
@TheodorZoulias 这是一个简单的一行代码就能实现目标。当然,如果集合非常大,您可能需要更精细的解决方案,但这可能已经足够了。您可以从简单开始,如果需要的话再让它变得更加复杂。 - Magnus
好的。 - Theodor Zoulias

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接