1. 痛苦的进化史
你肯定遇到过这种情况:不光要遍历集合,还得知道每个元素的编号。比如要输出带编号的列表、只改非零索引的元素,或者每隔三个处理一次。在普通的 for 里,这事儿超简单:
// 都很熟悉,C# 经典写法:
for (int i = 0; i < list.Count; i++)
{
Console.WriteLine($"{i}: {list[i]}");
}
但如果你开始用 LINQ 写代码,突然……索引就没了!那些漂亮的 .Where、.Select、.OrderBy,都不给你元素的编号——只给你元素本身。你肯定想这样写:
list.SelectWithIndex((item, index) => ...);
但其实标准库里根本没有 SelectWithIndex 这种方法。虽然 Select 的重载能拿到索引,但……如果你只想用索引而不想做投影或转换,就得多写一堆 .Select,本来很优雅的 LINQ 代码就变得乱七八糟。
在 .NET 9 之前大家怎么活的
以前 C# 程序员有自己的“黑科技”:
var result = list.Select((item, index) => new { item, index });
还有各种和别的 LINQ 方法组合的骚操作,但总觉得不够“原生”,也不够优雅。
2. LINQ 新方法 Index —— 是啥?有啥用?
官方说明
.NET 9 的开发团队终于听到了无数程序员(好吧,推特上成千上万的吐槽,也不少了)的呼声——给 LINQ 加了个新扩展方法 Index。在 .NET 9 官方文档 上是这么说的:
Index() 会给序列里的每个元素加上它的编号(从 0 开始),返回 (值, 索引) 这样的对,不用你手动 new 匿名对象,也不用写 .Select((item, idx) => new { item, idx }) 这种啰嗦代码。
这功能很强——Index 直接给你每个元素和它的索引一对。
方法签名
public static IEnumerable<(T Element, int Index)> Index<T>(this IEnumerable<T> source);
翻成人话: 集合里的每个元素你都能拿到一个特殊的“元组”——Element 和它的 Index。就这样。再也不用造匿名类型了。
3. Index 方法的用法示例
超简单例子
来,咱们搞个最爱的水果列表。
var fruits = new List<string> { "苹果", "香蕉", "橙子", "猕猴桃" };
foreach (var (fruit, idx) in fruits.Index())
{
Console.WriteLine($"{idx}: {fruit}");
}
输出:
0: 苹果
1: 香蕉
2: 橙子
3: 猕猴桃
就这么简单!你现在可以优雅地拿到“索引-值”对,在任何 LINQ 查询里用。
和其他 LINQ 方法一起玩
Index 是 LINQ 家族的正式成员!可以随便插到“链”里。
例1:按索引过滤奇数位置的元素
var numbers = Enumerable.Range(10, 10); // 10, 11, ... 19
var oddIndexes = numbers.Index()
.Where(pair => pair.Index % 2 == 1)
.Select(pair => pair.Element);
Console.WriteLine(string.Join(", ", oddIndexes));
输出:
11, 13, 15, 17, 19
例2:只改偶数索引的元素
var users = new List<string> { "安娜", "伊戈尔", "卡佳", "丹尼斯" };
var modified = users.Index()
.Select(pair => pair.Index % 2 == 0 ? pair.Element.ToUpper() : pair.Element.ToLower());
foreach (var name in modified)
Console.WriteLine(name);
输出:
安娜
伊戈尔
卡佳
丹尼斯
例3:按索引合并其他集合(像 zip 一样)
var ids = new[] { 101, 102, 103 };
var names = new[] { "Alice", "Bob", "Charlie" };
var merged = names.Index()
.Join(ids.Index(),
namePair => namePair.Index,
idPair => idPair.Index,
(namePair, idPair) => (idPair.Element, namePair.Element));
foreach (var (id, name) in merged)
Console.WriteLine($"{id}: {name}");
输出:
101: Alice
102: Bob
103: Charlie
4. Index 在实际项目里的用法
来,给咱们“永恒”学生管理小项目加个新功能:把所有学生带编号输出(比如让用户可以按编号选学生)。
例子:输出带编号的学生列表
假设你已经有了 Student 类:
public class Student
{
public string Name { get; set; }
public int Grade { get; set; }
}
造个小学生列表:
var students = new List<Student>
{
new Student { Name = "达莎", Grade = 5 },
new Student { Name = "彼佳", Grade = 3 },
new Student { Name = "沃瓦", Grade = 4 },
new Student { Name = "奥莉娅", Grade = 5 }
};
现在用 Index 优雅地带编号输出:
foreach (var (student, idx) in students.Index())
{
Console.WriteLine($"{idx + 1}. {student.Name} — 分数: {student.Grade}");
}
这里
idx + 1 —— 就是让编号从 1 开始,看着更习惯。
结果:
1. 达莎 — 分数: 5
2. 彼佳 — 分数: 3
3. 沃瓦 — 分数: 4
4. 奥莉娅 — 分数: 5
实用性:现在用户想按编号选学生——直接搞定!代码更简单,不用“手动”计数器,易读性拉满,bug 也少。
5. 对比:Index 比老办法爽在哪?
.NET 9 之前:老派写法
以前要拿到元素和索引,只能用 .Select((item, index) => ...) 的重载,还得造匿名类型:
var withIndexes = students.Select((student, index) => new { student, index });
想拿字段还得写 .student、.index,而且类型是匿名的,没有漂亮的命名元组。
.NET 9 起:21 世纪风格
现在——不用管字段、匿名类型,直接来:
foreach (var (student, idx) in students.Index())
{
// 开箱即用,直观、简单、优雅
}
代码更干净。更少代码,更少 bug。LINQ 长链里用起来也不用再想重载的事儿。
6. 使用细节和注意点
适用范围
Index 能用在任何实现了 IEnumerable<T> 的对象上。也就是——所有常见集合、数组、LINQ 查询结果都行。
Index 返回什么类型?
它返回的是元组序列,第一个元素是对象本身(一般叫 Element),第二个是 Index(类型 int)。C# 现代元组语法让你可以直接在循环里写 foreach (var (element, index) in ...),两个值直接进变量。
能用 Query Syntax 吗?
不能,Index 是扩展方法,query syntax(SQL 风格 LINQ)不直接支持。比如这样不行:
// 不行!
var query = from s in students.Index() select ...;
想结合用,只要把方法包进括号,像普通集合一样用就行:
var query = from pair in students.Index()
where pair.Index > 1
select pair.Element;
索引总是从 0 开始
Index 总是从 0 开始计数,就像 C# 里大多数东西一样。如果你想从 1 开始——自己加 1 就行。
8. 易错点和小坑——注意啥
很多同学一开始会把 Index 和 .Select((item, index) => ...) 的重载搞混。常见错误有:
—— 想在 query syntax 里用 Index:“为啥不行?”——它只能当扩展方法用。
—— 以为索引会从 1 开始,其实肯定是 0。
—— 以为 Index 会改原集合——其实和所有 LINQ 方法一样,它返回新序列,原集合不变(不可变风格)。
还有个有意思的点:如果集合是“惰性”的(记住:LINQ 查询默认都是惰性),Index 也是按需计算索引,不会提前算好。这对大集合甚至无限序列特别友好——索引永远是对的,也不会炸内存。
GO TO FULL VERSION