能否通过foreach反向迭代?

183

我知道我可以使用for语句来达到相同的效果,但是在C#中我能通过反向循环foreach循环吗?


11
在使用 for each 之前,您可以使用 list.Reverse() 将列表中的元素反转。 - Akaanthan Ccoder
1
另请参见为什么C#中没有ReverseEnumerator - nawfal
你需要实例化枚举中的所有元素,以便可以反转它们的顺序。这可能是不可能的。考虑以下序列:IEnumerable<int> Infinity() { int i = 1; while (true) yield return i++; } 如何反转它? - Suncat2000
13个回答

178

如果你使用的是 .NET 3.5,你可以这样做:

IEnumerable<int> enumerableThing = ...;
foreach (var x in enumerableThing.Reverse())

这种方法效率不高,因为它需要正向遍历枚举器,将所有内容放入堆栈中,然后以相反的顺序弹出所有内容。

如果您有一个可以直接索引的集合(例如 IList),则应该使用 for 循环。

如果您使用的是 .NET 2.0 并且无法使用 for 循环(即只有 IEnumerable),那么您就必须编写自己的 Reverse 函数。代码如下:

static IEnumerable<T> Reverse<T>(IEnumerable<T> input)
{
    return new Stack<T>(input);
}

这种方法依赖于一些可能不太明显的行为。当您将IEnumerable传递给stack构造函数时,它会遍历它并将项目推入堆栈。当您遍历堆栈时,它会以相反的顺序弹出内容。

如果将一个永远不停止返回项目的IEnumerable传递给它,则此方法和.NET 3.5的 Reverse() 扩展方法显然会崩溃。


5
有趣的.NET 2.0解决方案。 - RichardOD
12
我是否遗漏了什么,还是你的 .Net 3.5 解决方案实际上并不起作用?Reverse() 会就地反转列表,并不返回它。很遗憾,我原本希望找到这样的解决方案。 - user12861
2
有些管理员标记此答案为错误。Reverse()是一个void[1],因此上面的示例会导致编译错误。 [1] http://msdn.microsoft.com/en-us/library/b0axc2h2(v=vs.110).aspx - user585968
9
@user12861,Micky Duncan:答案并没有错,你漏掉了一些东西。System.Collections.Generic.List<T>有一个名为Reverse的方法,它可以进行原地反转。在 .Net 3.5中,IEnumerable<T>有一个名为Reverse的扩展方法。我已将示例从var更改为IEnumerable<int>,以使其更明确。 - Matt Howells
参见Bryan在类似问题的回答,更容易理解两种方法,一个适用于列表(Lists),另一个适用于其他IEnumerables。 - ToolmakerSteve
显示剩余3条评论

97

当使用列表(直接索引)时,不能像使用for循环那样高效。

编辑:通常情况下,当您能够使用for循环时,它可能是正确的方法。此外,尽管foreach按顺序实现,但该结构本身是用于表达独立于元素索引和迭代顺序的循环的,这在并行编程中特别重要。我认为依赖顺序进行迭代的循环不应使用foreach


6
我觉得你最后的陈述有些太笼统了。当然有些情况下,例如需要按顺序遍历某种IEnumerable时,是需要使用foreach的,你不会在这种情况下使用foreach吗?那你会用什么呢? - avl_sweden
2
更新:请参考Bryan在类似问题上的回答,使用Linq提供更现代的答案,并讨论列表和其他可枚举对象。 - ToolmakerSteve
2
更新:Jon Skeet提供了更加优雅的答案,现在可以使用"yield return"实现。 - ToolmakerSteve
4
我和 @avl_sweden 一样,对“依赖顺序的迭代不应使用 foreach”这句话有相同的初步反应,听起来太过笼统。是的,foreach 适用于表达独立于顺序的并行任务,但它也是现代 基于迭代器的编程 的重要组成部分。也许这句话的意思是,如果有两个不同的关键字来明确是否断言迭代是独立于顺序的,那就会更清晰/更安全?[在像 Eiffel 这样可以传播合同的语言中,这些断言可以根据给定的代码被证明为真或假。] - ToolmakerSteve
4
“foreach用于表示与元素索引和迭代顺序无关的循环”这种想法是不正确的。C#语言规范要求foreach按顺序处理元素,可以通过在迭代器上使用MoveNext或者在数组上开始为零递增一来处理索引以实现顺序处理。 - Donald Rich

72

像280Z28所说的那样,对于一个 IList<T>,您可以直接使用索引。您可以将其隐藏在扩展方法中:

public static IEnumerable<T> FastReverse<T>(this IList<T> items)
{
    for (int i = items.Count-1; i >= 0; i--)
    {
        yield return items[i];
    }
}

这比Enumerable.Reverse()更快,后者会先缓冲所有数据。(我认为Reverse没有应用任何类似Count()的优化。) 请注意,这意味着在您首次开始迭代时,数据将被完全读取,而FastReverse将“看到”您在迭代过程中进行的任何更改。(如果您在迭代之间删除多个项,它也会出错。)

对于一般序列,没有办法倒序迭代 - 序列可能是无限的,例如:

public static IEnumerable<T> GetStringsOfIncreasingSize()
{
    string ret = "";
    while (true)
    {
        yield return ret;
        ret = ret + "x";
    }
}
你如果尝试反向迭代,你会期待什么结果?

只是好奇,为什么要使用“>= 0”而不是“> -1”? - Chris S
21
为什么要使用“>= 0”而不是“> -1”? 因为“>= 0”更好地传达了代码读者的意图。如果编译器能够优化到等效的“> -1”,以提高性能,则应该采用优化。 - Mark Maslar
1
FastReverse(this IList<T> items) 应该改为 FastReverse<T>(this IList<T> items)。 :) - Rob
1
将分割棍子的0<=i变得更好。多年来教授许多孩子数学和帮助人们编写代码后,我发现如果他们总是将a>b交换为b<a,则可以大大减少人们犯错的机会,因为这样事物就按照数字行(或坐标系的X轴)的自然顺序排列了。唯一的缺点是,如果您需要将方程粘贴到XML中,例如.config,则可能会有问题。 - Eske Rahn

20

在使用 foreach 进行迭代之前,通过 reverse 方法反转列表:

    myList.Reverse();
    foreach( List listItem in myList)
    {
       Console.WriteLine(listItem);
    }

3
请参考 Matt Howells' answer from '09 - Martin Schneider
关于myList的说明会很有帮助。IEnumerable.Reverse在这里行不通。 - nawfal
2
@MA-Maddin 不,这两个是不同的。我猜回答者依赖于 List<T>.Reverse,它是原地操作的。 - nawfal
其实,我不知道为什么会这样说。我应该添加更多细节...无论如何,这段代码很混乱,因为 myList 似乎是类型为 System.Collections.Generic.List<System.Windows.Documents.List>(或任何其他自定义的 List 类型),否则这段代码就无法工作 :P - Martin Schneider

7
如果您使用List<T>,您也可以使用以下代码:
List<string> list = new List<string>();
list.Add("1");
list.Add("2");
list.Add("3");
list.Reverse();

这是一种在列表本身中反转写入的方法。

现在来看foreach:

foreach(string s in list)
{
    Console.WriteLine(s);
}

输出结果如下:
3
2
1

6
有时您无法使用索引,或者您想要反转Linq查询的结果,或者您不希望修改源集合,如果这些任何一个都是真的,那么Linq可以帮助您。
使用匿名类型和Linq Select来提供排序键的Linq扩展方法,用于对Linq OrderByDescending进行排序。
    public static IEnumerable<T> Invert<T>(this IEnumerable<T> source)
    {
        var transform = source.Select(
            (o, i) => new
            {
                Index = i,
                Object = o
            });

        return transform.OrderByDescending(o => o.Index)
                        .Select(o => o.Object);
    }

使用方法:

    var eable = new[]{ "a", "b", "c" };

    foreach(var o in eable.Invert())
    {
        Console.WriteLine(o);
    }

    // "c", "b", "a"

它被命名为"Invert",因为它与"Reverse"同义,并且使得与List Reverse实现的歧义消除。
也可以反转集合的特定范围,因为Int32.MinValue和Int32.MaxValue超出了任何类型的集合索引范围,我们可以利用它们进行排序。如果元素索引低于给定范围,则将其分配为Int32.MaxValue,以便在使用OrderByDescending时其顺序不会改变;类似地,索引大于给定范围的元素将被分配为Int32.MinValue,以便它们出现在排序过程的末尾。给定范围内的所有元素都分配了它们正常的索引并相应地被反转。
    public static IEnumerable<T> Invert<T>(this IEnumerable<T> source, int index, int count)
    {
        var transform = source.Select(
            (o, i) => new
            {
                Index = i < index ? Int32.MaxValue : i >= index + count ? Int32.MinValue : i,
                Object = o
            });

        return transform.OrderByDescending(o => o.Index)
                        .Select(o => o.Object);
    }

使用方法:

    var eable = new[]{ "a", "b", "c", "d" };

    foreach(var o in eable.Invert(1, 2))
    {
        Console.WriteLine(o);
    }

    // "a", "c", "b", "d"

我不确定这些Linq实现的性能是否比使用临时List来包装反转集合更好。


在写作时,我不知道Linq自己的Reverse实现,但是,尝试这个很有趣。 https://msdn.microsoft.com/en-us/library/vstudio/bb358497(v=vs.100).aspx


5
进一步解释Jon Skeet的精彩答案,这可能很多用途:
public static IEnumerable<T> Directional<T>(this IList<T> items, bool Forwards) {
    if (Forwards) foreach (T item in items) yield return item;
    else for (int i = items.Count-1; 0<=i; i--) yield return items[i];
}

然后使用作为

foreach (var item in myList.Directional(forwardsCondition)) {
    .
    .
}

4
如果您能更改实现IEnumerable或IEnumerable的集合代码(例如,自己实现IList),那么这是可能的。
为此,可以创建一个迭代器来执行此操作,例如通过IEnumerable接口进行以下实现(假设“items”是此示例中的List字段):
创建迭代器以为您完成此任务,例如通过IEnumerable接口进行以下实现。
public IEnumerator<TObject> GetEnumerator()
{
    for (var i = items.Count - 1; i >= 0; i--)
    { 
        yield return items[i];
    }
}

IEnumerator IEnumerable.GetEnumerator()
{
    return GetEnumerator();
}

因此,您的列表将通过您的列表反向迭代。
提示:您应该在文档中清楚地说明列表的这种特殊行为(最好选择一个自我解释的类名,如Stack或Queue)。

2
不行。ForEach只是遍历集合中的每个项目,其顺序取决于它是否使用IEnumerable或GetEnumerator()。

1
根据集合类型,确实存在顺序保证。 - Jon Skeet

1

当我们将List.Reverse()方法和foreach结合使用时,这变得非常简单。

 List<int> numbers = new List<int>() { 1, 2, 3, 4, 5, 6, 7 };

        // Reverse the order of the elements            
        foreach (int i in numbers.Reverse())
        {
            // Display the element
            Console.WriteLine(i);
        }            

你也可以参考这个文章,了解其他选项。

当您引用自己的内容时,请务必阅读Stack Overflow的自我推广政策 - Jeremy Caney
目前你的回答不够清晰,请编辑并添加更多细节,以帮助其他人理解它如何回答问题。你可以在帮助中心找到有关如何编写好答案的更多信息。 - Community

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