循环中如何确定最后一次迭代:Foreach循环

319

我有一个foreach循环,并需要在List中选择最后一项时执行一些逻辑,例如:

 foreach (Item result in Model.Results)
 {
      //if current result is the last item in Model.Results
      //then do something in the code
 }

我可以在不使用for循环和计数器的情况下知道哪个循环是最后一个吗?


1
请查看我在这里发布的解决方案,链接为:https://dev59.com/m3VD5IYBdhLWcg3wO5ED#3293486,该解决方案是针对一个相关问题的回答。 - Brian Gideon
24个回答

380

如果你只是需要对最后一个元素做一些操作(而不是对最后一个元素做一些不同的操作),那么使用LINQ会有所帮助:

Item last = Model.Results.Last();
// do something with last
如果你需要对最后一个元素进行不同的操作,那么你需要像这样做:
Item last = Model.Results.Last();
foreach (Item result in Model.Results)
{
    // do something with each item
    if (result.Equals(last))
    {
        // do something different with the last item
    }
    else
    {
        // do something different with every item but the last
    }
}

虽然您可能需要编写自定义比较器来确保您能够确定该项与 Last() 返回的项相同,但这种方法应该谨慎使用。

这种方法应该谨慎使用,因为 Last 可能需要遍历整个集合。虽然对于小集合可能没有问题,但如果集合很大,它可能会影响性能。如果列表包含重复的项,它也将失败。在这种情况下,可能更适合像这样的方法:

int totalCount = result.Count();
for (int count = 0; count < totalCount; count++)
{
    Item result = Model.Results[count];

    // do something with each item
    if ((count + 1) == totalCount)
    {
        // do something different with the last item
    }
    else
    {
        // do something different with every item but the last
    }
}

1
我需要的是: 当循环遍历到最后一个项目时:foreach (Item result in Model.Results) { if (result == Model.Results.Last()) {
last
; }
看起来你的意思基本相同。
- mishap
11
如果集合不小的话,你的代码将会对整个集合进行两次迭代——这是不好的。请参考此答案 - Shimmy Weitzhandler
70
如果在你的集合中存在重复项,这种方法就不太可行。例如,如果你正在处理一个字符串集合,并且其中有任何重复项,那么“与上一项不同”的代码将针对列表中最后一项的每个出现执行。 - muttley91
11
这个答案有点老了,但是对于其他查看此答案的人来说,您可以通过使用以下方式获取最后一个元素并确保您不必循环遍历所有元素: Item last = Model.Results[Model.Results.Count - 1] 列表的Count属性不需要循环遍历。如果列表中存在重复项,则可以在for循环中使用迭代器变量。普通的for循环也不错。 - Michael Harris
我建议使用 var last = Model.Result[Model.Result.Count - 1]; 而不是使用 Last(),因为前者更快。 - Tân
只有当列表/集合具有唯一值时,此方法才有效。 - melleck

231

那用一种传统的for循环怎么样?

for (int i = 0; i < Model.Results.Count; i++) {

     if (i == Model.Results.Count - 1) {
           // this is the last item
     }
}

或者使用 Linq 和 foreach:

foreach (Item result in Model.Results)   
{   
     if (Model.Results.IndexOf(result) == Model.Results.Count - 1) {
             // this is the last item
     }
}

22
很多人在一个简单的问题上过度思考,当 for 循环已经完全能够解决它。:\ - Andrew Hoffman
Linq解决方案是我绝对的最爱!谢谢分享。 - mecograph
这个回答比被接受的更为恰当。 - Ratul
3
注意:如果你想要在字符串(或值类型)集合上使用LINQ解决方案,它一般不会起作用,因为如果列表中的最后一个字符串也早先出现在列表中,则"=="比较将失败。只有当你使用保证没有重复字符串的列表时,它才能起作用。 - Tawab Wakil
5
如果 Model.Results 是一个 IEnumerable,那么您无法使用这个巧妙的解决方案。您可以在循环之前调用 Count(),但这可能会导致对整个序列进行完整迭代。 - Luca Cremonesi

62

Last() 在某些类型上使用会循环遍历整个集合!这意味着如果你使用 foreach 并调用 Last(),你将会循环两次!对于大型集合来说,这是需要避免的。

解决方案是使用 while 循环:

using var enumerator = collection.GetEnumerator();

var last = !enumerator.MoveNext();
T current;

while (!last)
{
  current = enumerator.Current;        

  //process item

  last = !enumerator.MoveNext();        
  if(last)
  {
    //additional processing for last item
  }
}

因此,除非集合类型为 IList<T> 类型,否则 Last() 函数将遍历所有集合元素。

测试

如果您的集合提供随机访问(例如实现了 IList<T>),您还可以按如下方式检查您的项。

if(collection is IList<T> list)
  return collection[^1]; //replace with collection.Count -1 in pre-C#8 apps

1
你确定枚举器需要 using 语句吗?我认为只有在对象处理操作系统资源时才需要,而不是针对托管数据结构。 - Crouching Kitten
2
IEnumerator没有实现IDisposable接口,所以使用using语句会导致编译时错误!对于解决方案点个赞,大多数情况下我们不能简单地使用for循环代替foreach,因为可枚举集合的项在运行时计算或序列不支持随机访问。 - Saleh
2
通用的枚举器 可以实现该功能。 - Shimmy Weitzhandler

59

正如Chris所展示的那样,Linq可以解决这个问题;只需使用Last()获取可枚举对象中的最后一个引用,只要您不使用该引用,就可以继续进行正常的代码编写,但如果您正在使用该引用,则需要执行额外的操作。缺点是它的时间复杂度始终为O(N)。

您还可以使用Count()(如果IEnumerable也是ICollection,则为O(1);对于大多数常见的内置IEnumerables,这是真的),并将foreach与计数器混合使用:

var i=0;
var count = Model.Results.Count();
foreach (Item result in Model.Results)
{
    if (++i == count) //this is the last item
}

32
var last = objList.LastOrDefault();
foreach (var item in objList)
{
  if (item.Equals(last))
  {
  
  }
}

你好,这是目前最好的方法!简单明了,符合程序员思维方式。为什么不选择并给予这个方法更多的赞扬呢? - Hanny Setiawan
1
foreach块之前应该只找到最后一个项目一次(促进记忆化)。像这样:var lastItem = objList.LastOrDeafault();。然后从foreach循环的内部,您可以这样检查它:f (item.Equals(lastItem)) { ... }。在您原始的答案中,objList.LastOrDefault()将在每个“foreach”迭代中迭代集合(涉及多项式复杂度)。 - AlexMelw
3
错误答案。复杂度为n^2而不是n。 - Shimmy Weitzhandler
2
这是不正确的,因为@ShimmyWeitzhandler提到的问题,不应使用。 所有这些语句的值通常预计在循环外部准备好。 - Artfaith
1
我已经更新了答案,以避免人们掉入那个陷阱。 - Shimmy Weitzhandler

16

正如Shimmy所指出的那样,使用Last()可能会导致性能问题,例如如果你的集合是LINQ表达式的实时结果。为了避免多次迭代,你可以使用一个"ForEach"扩展方法,像这样:

var elements = new[] { "A", "B", "C" };
elements.ForEach((element, info) => {
    if (!info.IsLast) {
        Console.WriteLine(element);
    } else {
        Console.WriteLine("Last one: " + element);
    }
});

扩展方法如下(额外的好处是,它还会告诉你索引和是否正在查看第一个元素):
public static class EnumerableExtensions {
    public delegate void ElementAction<in T>(T element, ElementInfo info);

    public static void ForEach<T>(this IEnumerable<T> elements, ElementAction<T> action) {
        using (IEnumerator<T> enumerator = elements.GetEnumerator())
        {
            bool isFirst = true;
            bool hasNext = enumerator.MoveNext();
            int index = 0;
            while (hasNext)
            {
                T current = enumerator.Current;
                hasNext = enumerator.MoveNext();
                action(current, new ElementInfo(index, isFirst, !hasNext));
                isFirst = false;
                index++;
            }
        }
    }

    public struct ElementInfo {
        public ElementInfo(int index, bool isFirst, bool isLast)
            : this() {
            Index = index;
            IsFirst = isFirst;
            IsLast = isLast;
        }

        public int Index { get; private set; }
        public bool IsFirst { get; private set; }
        public bool IsLast { get; private set; }
    }
}

8

进一步改进Daniel Wolf的答案,您可以堆叠另一个IEnumerable以避免多次迭代和lambda表达式,例如:

var elements = new[] { "A", "B", "C" };
foreach (var e in elements.Detailed())
{
    if (!e.IsLast) {
        Console.WriteLine(e.Value);
    } else {
        Console.WriteLine("Last one: " + e.Value);
    }
}

扩展方法实现:
public static class EnumerableExtensions {
    public static IEnumerable<IterationElement<T>> Detailed<T>(this IEnumerable<T> source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));

        using (var enumerator = source.GetEnumerator())
        {
            bool isFirst = true;
            bool hasNext = enumerator.MoveNext();
            int index = 0;
            while (hasNext)
            {
                T current = enumerator.Current;
                hasNext = enumerator.MoveNext();
                yield return new IterationElement<T>(index, current, isFirst, !hasNext);
                isFirst = false;
                index++;
            }
        }
    }

    public struct IterationElement<T>
    {
        public int Index { get; }
        public bool IsFirst { get; }
        public bool IsLast { get; }
        public T Value { get; }

        public IterationElement(int index, T value, bool isFirst, bool isLast)
        {
            Index = index;
            IsFirst = isFirst;
            IsLast = isLast;
            Value = value;
        }
    }
}

2
另一个答案没有多次迭代源,所以你不需要解决这个问题。你确实允许使用foreach,这是一种改进。 - Servy
1
@Servy 我是这个意思。除了原始回答的单次迭代外,我也避免使用lambda表达式。 - Fabricio Godoy

7

迭代器实现并没有提供这个功能。你的集合可能是一个可以通过O(1)索引访问的IList。在这种情况下,你可以使用普通的for循环:

for(int i = 0; i < Model.Results.Count; i++)
{
  if(i == Model.Results.Count - 1) doMagic();
}

如果你知道数量,但无法通过索引访问(因此,结果是一个ICollection),你可以通过在foreach的主体中递增i并将其与长度进行比较来计数。
所有这些并不完美优雅。Chris的解决方案可能是我目前看到的最好的。

在比较你使用foreach方法的计数器和Chris的解决方案的性能时,我想知道哪个会更耗费资源——是单个Last()调用,还是所有增量操作的总和。我猜它们的差距会很小。 - TTT

5
最佳方法可能是在循环后执行该步骤:例如。
foreach(Item result in Model.Results)
{
   //loop logic
}

//Post execution logic

如果您需要对最后的结果进行操作,则可以这样做。
foreach(Item result in Model.Results)
{
   //loop logic
}

Item lastItem = Model.Results[Model.Results.Count - 1];

//Execute logic on lastItem here

如果您需要针对最后一个和非最后一个元素执行不同的操作,则此方法将无法奏效。例如,在我的情况下,我需要为非最后一个元素按“下一步”,而对于最后一个元素则按“完成”。 - ivan_pozdeev

5

那么,有没有更简单的方法呢?

Item last = null;
foreach (Item result in Model.Results)
{
    // do something with each item

    last = result;
}

//Here Item 'last' contains the last object that came in the last of foreach loop.
DoSomethingOnLastElement(last);

2
我不知道为什么有人给你点了踩。考虑到你已经在执行 foreach 并且正在承担 O(n) 的成本,这是完全可以接受的。 - arviman
3
尽管这个答案完美地解决了寻找最后一个项目的问题,但它并不适用于提问者的情况 "..., 确定循环的最后一次迭代". 因此,你无法确定最后一次迭代实际上是最后一次,因此无法以不同的方式处理它甚至忽略它。这就是有人给你投反对票的原因。@arviman,你对此非常好奇。 - AlexMelw
1
你是对的,我完全没注意到 @Andrey-WD。我猜修复的解决方案就是在循环之前调用 "last" 一次(无法在循环内部执行,因为它会变成 O(N^2)),然后检查引用是否匹配它。 - arviman
“last” 实际上是 “lastOrDefault”,因为 “Model.Results” 可能为空。 - Theodor Zoulias

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