为什么List和IEnumerable的Reverse有两个完全不同的版本?

37
对于 List 对象,我们有一个名为 Reverse() 的方法。
它会就地反转列表的顺序,不会返回任何内容。
对于 IEnumerable 对象,我们有一个扩展方法称为 Reverse()
它返回另一个 IEnumerable。
我需要按相反的顺序遍历一个列表,所以我不能直接使用第二种方法,因为我得到的是一个 List,而我不想反转它,只想反向迭代。
所以我可以这样做:
for(int i = list.Count - 1; i >=0; i--)

或者

foreach(var item in list.AsEnumerable().Reverse())

我发现如果我有一个IEnumerable,它比较难读懂,只需使用以下代码即可:
foreach(var item in list.Reverse())

我不明白为什么这两种方法会用相同的名称实现。这很烦人也很令人困惑。
为什么没有一个名为BackwardsIterator()的扩展,可以适用于所有IEnumerable,而不是使用Reverse()?
我对这个选择的历史原因非常感兴趣,而不仅仅是“如何做到这一点”的内容!

2
你可以使用 Enumerable.Reverse(list)。 - Alexander Efimov
1
为什么不将您的列表键入到您喜欢的类型,然后它将使用您喜欢的方法版本呢? - Brian Warshaw
他可能希望它是一个“List”,因为它是一个“List”--但是,正如他明确表示的那样,他不想使用List.Reverse。 “转换为您喜欢的类型”意味着进行强制转换,这并不比使用AsEnumerable()更美观。 - Jim Balter
3个回答

29
值得注意的是,列表方法比扩展方法早得多。命名可能保持不变,因为 "Reverse" 比 "BackwardsIterator" 更简洁。
如果您想绕过列表版本并转到扩展方法,则需要将列表视为一个 "IEnumerable<T>"。
var numbers = new List<int>();
numbers.Reverse(); // hits list
(numbers as IEnumerable<int>).Reverse(); // hits extension

或者将扩展方法作为静态方法调用:

Enumerable.Reverse(numbers);

请注意,Enumerable版本需要完全迭代底层可枚举对象才能开始反向迭代。如果您计划多次在同一可枚举对象上执行此操作,请考虑永久地反转顺序并正常迭代它。

2
实际上,即使一个人只想枚举一次并且想保持原始列表不被干扰,我相信调用 ToArray() 然后在结果数组上调用 Reverse() 肯定比使用 Enumerable.Reverse() 更快。List<T>.ToArray() 方法可以将数据存储在目标数组中作为一个块,而无需处理单个项,而 Enumerable.Reverse() 则会 - 除非它对源是 List<T> 的情况有特殊处理 - 在反转它们之前必须逐个将项复制到数组中。 - supercat
1
@Supercat 很有可能它有特殊情况。我过去在一些其他方法上使用过 ILSpy,他们很多时候都是在 IList 上有特殊情况。 - Adam Houldsworth
1
确实有很多特殊情况,但也有一些令人讨厌的问题。例如,尽管可以将List<Cat>传递给期望IEnumerable<Animal>的代码,但这样的代码将无法利用任何特殊的“列表性质”。我的“几乎肯定”的说法可能有点过于强烈,不过在枚举之前将其转换为数组的一个优点是,如果我记得正确的话,C#在使用foreach循环时会生成特殊的代码,以处理已知为数组的情况。即使Enumerable.Reverse返回临时数组的枚举器,编译器也不会知道这一点。 - supercat
如果你想玩代码高尔夫,我宁愿调用 numbers.Skip(0).Reverse() 而不是强制转换。从左到右更易读,并且你正在处理一个 IEnumerable。这比 numbers.OfType<int>().Reverse() 更好,因为它不会在每个条目上添加无用的类型检查开销。 - Nameless One
1
还有一个numbers.Reverse<int>(),它是扩展方法的其中一个重载。 - heltonbiker
显示剩余3条评论

5
自己编写BackwardsIterator吧!
public static IEnumerable BackwardsIterator(this List lst)
{
    for(int i = lst.Count - 1; i >=0; i--)
    {
        yield return lst[i];
    }
}

2
如果你要将它制作成一个实用方法,你应该使用更有效的方法(即反向for循环)。Reverse作为一个可枚举对象,效率相对较低,因为它需要遍历可枚举对象,将其存储在缓冲区中,然后反向遍历缓冲区。 - Servy
@Servy 你说得对,特别是如果列表很大!感谢指出。 - Anirudha

2
List<T>.Reverse 的存在早于 IEnumerable<T>.Reverse。它们被命名为相同的名称是因为……无能。这是一个可怕的错误;显然 Linq 中的 IEnumerable<T> 函数应该被赋予不同的名称,例如 Backwards,因为它们具有完全不同的语义。事实上,它为程序员设置了一个可怕的陷阱——有人可能会将 list 的类型从 List<T> 更改为,例如,Collection<T>,然后 list.Reverse(); 不是在原地反转 list,而只是返回一个被丢弃的 IEnumerable<T>。不能过分强调微软给这些方法赋予相同名称的无能之处。
为避免这个问题,您可以定义自己的扩展方法。
public static IEnumerable<T> Backwards<T>(this IEnumerable<T> source) => source.Reverse();

你甚至可以添加一个特殊情况来高效处理可索引列表:
public static IEnumerable<T> Backwards<T>(this IEnumerable<T> source) =>
    source is IList<T> list ? Backwards<T>(list) : source.Reverse();

public static IEnumerable<T> Backwards<T>(this IList<T> list)
{
    for (int x = list.Count; --x >= 0;)
        yield return list[x];
}

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