C# 每日面试题:索引器(Indexer)和迭代器(Iterator)的区别
这是 C# 中非常高频的一道面试题,尤其在 .NET 中高级/资深岗位中,几乎每次都会问到,因为它考察了你对 C# 语言特性本质、语法糖实现、使用场景以及底层机制的理解。
下面从多个维度完整对比,方便你快速记忆和回答。
一、核心对比表(面试最常用回答框架)
| 维度 | 索引器(Indexer) | 迭代器(Iterator) | 关键区别一句话总结 |
|---|---|---|---|
| 语法关键字 | this[参数] | yield return / yield break | — |
| 本质 | 一种特殊的属性(Property-like) | 一种状态机生成器(编译器生成的 IEnumerator) | 属性 vs 方法生成器 |
| 作用对象 | 类/结构体/接口 | 方法(返回 IEnumerable 或 IEnumerator) | 作用于类型 vs 作用于方法 |
| 主要目的 | 像数组一样用 [] 访问对象内部数据 | 让对象能被 foreach 遍历 | 自定义索引访问 vs 自定义遍历行为 |
| 返回类型 | 任意类型(get 返回,set 接收) | IEnumerable / IEnumerator / IAsyncEnumerable | — |
| 是否可以有参数 | 是(可以多个参数,甚至重载) | 通常无参(foreach 不传参) | 索引器可以参数化 |
| 是否可以只读/只写 | 可以(只写 get 或只写 set) | 通常只读(yield return 产生值) | 索引器更灵活 |
| 编译后真实类型 | 编译成 get_Item / set_Item 方法 | 编译成状态机类(嵌套类 + MoveNext 实现) | 方法 vs 状态机 |
| 是否支持泛型 | 支持 | 支持(IEnumerable) | — |
| 典型使用场景 | 自定义集合、字典式访问、矩阵、配置项 | 自定义集合、延迟计算、文件逐行读取、树遍历 | [] 访问 vs foreach 遍历 |
| 是否可以异步 | 同步为主(C# 8+ 支持索引器 async get) | 支持 async(yield return + IAsyncEnumerable) | 迭代器异步更自然 |
| 性能开销 | 几乎无(直接方法调用) | 有状态机开销(但现代 JIT 优化很好) | 索引器更轻量 |
二、最常见的面试追问 & 标准回答
Q1:请写出索引器和迭代器的典型代码
索引器示例(最常见写法)
public class StringDictionary
{
private Dictionary<string, string> _data = new();
// 索引器(最常见单参数形式)
public string this[string key]
{
get => _data.TryGetValue(key, out var value) ? value : null;
set => _data[key] = value;
}
// 支持多参数索引器(矩阵示例)
public int this[int row, int col]
{
get => /* 计算逻辑 */;
set => /* 计算逻辑 */;
}
}
// 使用
var dict = new StringDictionary();
dict["name"] = "张三"; // 调用 set_Item
string name = dict["name"]; // 调用 get_Item
迭代器示例(最常见两种写法)
// 方式1:返回 IEnumerable<T>(最常用)
public IEnumerable<int> GetFibonacci(int count)
{
int a = 0, b = 1;
for (int i = 0; i < count; i++)
{
yield return a;
int temp = a + b;
a = b;
b = temp;
}
}
// 方式2:返回 IEnumerator<T>(较少见,手动控制)
public IEnumerator<int> GetEnumerator()
{
yield return 1;
yield return 1;
yield return 2;
yield return 3;
// ...
}
// 使用
foreach (var num in GetFibonacci(10))
{
Console.WriteLine(num);
}
Q2:索引器底层编译成什么?迭代器底层编译成什么?
- 索引器 → 编译成两个方法:
public string get_Item(string key)public void set_Item(string key, string value)- 迭代器 → 编译器生成一个状态机类(嵌套的密封类),实现
IEnumerator<T>接口,包含: - 状态字段(-1=初始,0=开始,1=第一个 yield,…)
- MoveNext() 方法(核心状态跳转逻辑)
- Current 属性
- Dispose 方法(用于 finally 块)
Q3:什么时候用索引器?什么时候用迭代器?
- 用索引器:
- 想让你的类像数组/字典一样使用
obj[key]访问 - 需要对内部集合进行封装访问(类似 Dictionary、List)
- 多维索引(如矩阵、表格、配置表)
- 用迭代器:
- 需要让你的类支持
foreach遍历 - 想实现延迟计算(yield return 按需产生元素)
- 处理大数据量(如逐行读取文件、遍历树、生成无穷序列)
- 希望代码写得更简洁(避免手动实现 IEnumerator)
Q4:索引器和属性(Property)的本质区别?
- 本质相同:索引器就是带参数的属性
- 语法区别:属性用
.访问,索引器用[]访问 - 使用习惯:属性通常表示“成员”,索引器通常表示“集合中的元素”
Q5:C# 中支持异步迭代器的场景?
C# 8.0+ 支持 IAsyncEnumerable<T> + yield return
public async IAsyncEnumerable<int> GetNumbersAsync()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(100);
yield return i;
}
}
// 使用
await foreach (var num in GetNumbersAsync())
{
Console.WriteLine(num);
}
三、总结一句话回答模板(面试 30 秒版本)
“索引器是让类支持像数组一样使用 obj[key] 访问的语法糖,本质是编译成 get_Item / set_Item 方法,主要用于自定义集合的元素访问;迭代器是让类支持 foreach 遍历的语法糖,通过 yield return 实现,本质是编译器生成一个状态机类(实现 IEnumerator),主要用于延迟计算和自定义遍历逻辑。简单说:索引器 = 自定义 [] 访问,迭代器 = 自定义 foreach 行为。”
需要我继续补充:
- 索引器重载、接口索引器写法
- 迭代器在 LINQ 中的作用
- yield return 的性能分析与状态机反编译
- 实际项目中常见的坑
告诉我,我们继续深入!