使用LINQ删除最后一项

39

使用LINQ似乎是一个微不足道的任务(也可能确实如此),但我无法想象如何使用LINQ删除序列的最后一项。当然,使用Take并传递序列长度-1工作得很好。然而,在单行代码中链接多个LINQ时,这种方法似乎相当麻烦。

IEnumerable<T> someList ....

// this works fine
var result = someList.Take(someList.Count() - 1);


// but what if I'm chaining LINQ ?
var result = someList.Where(...).DropLast().Select(...)......;

// Will I have to break this up?
var temp = someList.Where(...);
var result = temp.Take(temp.Count() - 1).Select(...)........;

在Python中,我可以使用seq[0:-1]。我尝试将-1传递给Take方法,但它似乎不是我所需要的。

3个回答

72
对于.NET Core 2+和.NET Standard 2.1 (计划中), 您可以使用 .SkipLast(1)
对于其他平台,您可以编写自己的LINQ查询运算符(即扩展方法,用于IEnumerable<T>),例如:
static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source)
{
    using (var e = source.GetEnumerator())
    {
        if (e.MoveNext())
        {
            for (var value = e.Current; e.MoveNext(); value = e.Current)
            {
                yield return value;
            }
        }
    }
}

与其他方法(例如xs.Take(xs.Count() - 1))不同,以上方法只会处理一次序列。


6
很遗憾,这个答案没有出现在已标记的重复问题中。它比那里被接受的答案更整洁。 - spender
如何优雅地调整代码,以便跳过最后n个项目? - Gerard
@Gerard:你不需要这样做。这个答案的解决方案是针对 n = 1 的情况进行优化的。更一般的情况涉及到缓冲。如果你去看这个问题的重复问题,我记得有一个答案展示了更一般情况的解决方案。 - stakx - no longer contributing

31
someList.Reverse().Skip(1).Reverse()

3
@LukeH,它是O(n)的,并且正如OP所希望的那样,它可以用一行代码回答。 - Saeed Amiri
2
哦,为什么我没想到呢 :) 这是一个聪明而简单的方法,可以直接使用现有的功能。虽然不太确定性能会受到什么影响,但如果我只需要快速且简单地完成任务,这是一个不错的选择。 - Kei
19
请注意,这个方法会将整个序列存储在内存中,并重复存储一次。如果序列非常长,则可能需要大量的内存。是的,这种技术的时间复杂度是O(n),但空间复杂度也是O(n),而本来可以是O(1)的空间复杂度。 - Eric Lippert
2
好的,为了论证假设该序列占用O(n)空间。那么你是否有理由再消耗两倍的空间呢?那个东西可能非常巨大。 - Eric Lippert
1
如果你想要更多现实生活中这种方法会出问题的例子,可以考虑 File.ReadLines 的情况,它返回一个 IEnumerable<string>。因为这段代码有很大可能会引入意外的失败(在测试期间容易被忽视),所以我会犹豫使用它,特别是作为公共函数。如果你非常喜欢一行代码解决问题,我认为 Jamiec 的一行代码更安全。此外,我认为他的代码更明显地表达了其含义,尽管这段代码也很明显。 - Brian
显示剩余13条评论

13

你可以使用扩展方法很容易地实现这个功能,可以使用你已经发现的形式,或者可能是TakeCount的组合。

public static IEnumerable<T> DropLast<T>(this IEnumerable<T> enumerable)
{
  return enumerable.Take(enumerable.Count()-1);
}

1
第二种方法不会编译失败吗?Last()返回T,但Except需要IEnumerable<T>。我有什么遗漏的吗? - Kei
11
使用 Except 方法不适合此处,因为它还会从源序列中删除任何重复项。如果源序列是 {1,2,3,4,5,4,3,2,1},那么原帖作者期望的结果应该是 {1,2,3,4,5,4,3,2};使用您的第二个示例(假设您已经修复了错误)将给出 {2,3,4,5} - LukeH
2
这也可以工作:xs.TakeWhile((x, i) => i < xs.Count()-1) - Rezo Megrelidze
这种方法在问题本身中就被提到了(var result = someList.Take(someList.Count() - 1);),而且它是低效的代码,因为它必须两次枚举可枚举对象。 - Lance U. Matthews
1
哎呀,@RazMegrelidze,不要那样做。对于一个包含n个元素的IEnumerable<>,这个答案中的代码最终会枚举2n-1个元素:一次完整的枚举来计算Count(),然后是一个没有第n个元素的部分枚举。像你这样为每个(TakeWhile())的n个元素调用Count()将会枚举n²+n个元素;对于一个包含1000个元素的序列,这超过了一百万个枚举元素。大家,在点击△或复制粘贴之前,请考虑代码实际在做什么! - Lance U. Matthews
显示剩余3条评论

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