为什么foreach不直接提供索引?以及该如何处理?
\n\n你是否曾经在C#中编写foreach循环时,突然意识到:“我需要当前元素的编号!”手不由自主地伸向键盘,准备写下for (int i = 0; i < list.Count; i++)……等等!如果集合是没有索引器的IEnumerable呢?或者你正在处理HashSet?这时,老牌的for循环就无能为力了。
这个问题在Stack Overflow上可是热门话题(数百万次浏览!)。这并非偶然。foreach是遍历集合的王者:它简洁、安全,并且适用于任何可枚举类型。但它的缺点是没有内置的计数器。今天我们就来解决这个问题。我将向你展示4种在foreach中获取索引的经过验证的方法,并分析它们的优缺点和性能。开始吧!
方法一:经典方法——外部计数器
\n\n最简单直观的方法。在循环之前声明一个计数器变量,并在循环体中递增它。
\n\nList<string> fruits = new List<string>() { "苹果", "香蕉", "樱桃", "甜瓜" };\nint index = 0;\nforeach (var fruit in fruits)\n{\n Console.WriteLine($"索引 {index}: {fruit}");\n index++;\n}\n\n优点:
\n- \n
- 极其清晰。代码一目了然。 \n
- 不需要额外的库或LINQ。 \n
- 适用于任何
IEnumerable。 \n
缺点:
\n- \n
- 需要记得声明和递增计数器。 \n
- 如果循环体内有
continue,计数器仍然会递增(这是你期望的吗?)。 \n - 在嵌套循环中,容易混淆哪个index对应哪个循环。 \n
这个方法就像瑞士军刀:可靠但基础。对于80%的任务来说,它已经足够用了。
\n\n方法二:带索引的LINQ Select——一行代码的优雅
\n\n如果你喜欢函数式编程风格,LINQ可以帮到你。Select()方法有一个重载,可以传递元素的索引。
List<string> fruits = new List<string>() { "苹果", "香蕉", "樱桃", "甜瓜" };\n\nforeach (var (fruit, index) in fruits.Select((item, i) => (item, i)))\n{\n Console.WriteLine($"索引 {index}: {fruit}");\n}\n\n注意在循环头中直接进行的元组解构。这是现代C#(从7.0版本开始)最漂亮的特性之一。
\n\n优点:
\n- \n
- 代码变得声明式:“获取集合,为每个元素添加其索引”。 \n
- 计数器不会泄漏到外部——它被封装在LINQ内部。 \n
- 与其他LINQ方法链式结合效果很好。 \n
缺点:
\n- \n
- 创建元组会带来少量开销。对于不超过10,000个元素的集合,这几乎察觉不到,但对于巨大的列表可能至关重要。 \n
- 需要导入
System.Linq。 \n
这种方法适合那些编写“干净”代码的人。当索引不是用于控制循环,而是用于传递给另一个方法时,它尤其好用。
\n\n方法三:通过ElementAt()使用索引器——当集合是IEnumerable时
\n\n有时你只有IEnumerable,没有索引器,但非常想通过编号获取元素。技术上可以这样做:
IEnumerable<string> fruits = new List<string>() { "苹果", "香蕉", "樱桃", "甜瓜" };\n\nfor (int i = 0; i < fruits.Count(); i++) // 注意:这是糟糕的代码!\n{\n var fruit = fruits.ElementAt(i);\n Console.WriteLine($"索引 {i}: {fruit}");\n}\n\n打住!永远不要这样做。Count()和ElementAt()对于IEnumerable各自的时间复杂度都是O(n)。你的循环将变成O(n²)——平方复杂度。对于1000