C#每日面试题-索引器和迭代器的区别
在C#中,索引器(Indexer)和迭代器(Iterator)是两个易混淆但功能完全不同的特性,二者均用于简化数据访问,但设计目标、使用场景和底层实现存在本质差异。本文将从概念、用法、底层逻辑三个维度,用通俗案例拆解二者区别,帮你快速掌握面试核心考点。
一、核心概念:各自解决什么问题?
1. 索引器:让对象像数组一样被访问
索引器本质是一种特殊的属性,允许类或结构体的实例通过"数组下标"的方式访问内部数据,无需暴露底层存储结构(如数组、集合)。其核心作用是简化对象的元素访问语法,让自定义类型具备类似数组、List的访问体验。
2. 迭代器:简化集合的遍历逻辑
迭代器用于定义集合(或可遍历对象)的遍历规则,允许开发者自定义"如何逐个获取元素",无需手动维护遍历状态(如下标、指针)。其核心作用是解耦集合的存储结构与遍历逻辑,支持foreach循环遍历自定义类型。
二、用法对比:代码案例直观感受
1. 索引器的典型用法
假设我们实现一个"学生集合类",内部用List存储学生信息,通过索引器让外部可按下标访问学生,同时隐藏List的直接操作:
csharp
public class StudentCollection
{
// 底层存储(私有,隐藏实现)
private List<string> _students = new List<string>();
// 索引器:允许通过下标访问
public string this[int index]
{
get
{
// 可添加越界判断、权限校验等逻辑
if (index < 0 || index >= _students.Count)
throw new ArgumentOutOfRangeException(nameof(index));
return _students[index];
}
set
{
_students.Insert(index, value);
}
}
public int Count => _students.Count;
}
// 调用方式
var students = new StudentCollection();
students[0] = "张三"; // 像数组一样赋值
students[1] = "李四";
Console.WriteLine(students[0]); // 像数组一样取值
关键特性:索引器通过this[int index]定义,支持get/set访问器,可自定义参数类型(不仅是int,还能是string等,如按姓名索引),本质是属性的特殊形式。
2. 迭代器的典型用法
同样实现学生集合,通过迭代器让该类支持foreach遍历,无需暴露底层List:
csharp
public class StudentCollection : IEnumerable<string>
{
private List<string> _students = new List<string> { "张三", "李四", "王五" };
// 迭代器实现:通过yield关键字定义遍历规则
public IEnumerator<string> GetEnumerator()
{
foreach (var student in _students)
{
yield return student; // 逐个返回元素,自动维护遍历状态
}
}
// 非泛型接口实现(必要)
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// 可选:自定义遍历逻辑(如倒序)
public IEnumerable<string> Reverse()
{
for (int i = _students.Count - 1; i >= 0; i--)
{
yield return _students[i];
}
}
}
// 调用方式
var students = new StudentCollection();
foreach (var student in students) // 直接遍历
{
Console.WriteLine(student);
}
foreach (var student in students.Reverse()) // 自定义遍历
{
Console.WriteLine(student);
}
关键特性:迭代器依赖IEnumerable/IEnumerable<T>接口,通过yield return关键字简化遍历状态管理,无需手动实现IEnumerator的MoveNext、Current等方法。
三、核心区别:从本质到细节
为了清晰对比,整理以下核心差异点,覆盖面试高频考点:
1. 设计目标不同
-
索引器:聚焦"元素访问",让对象具备数组式下标访问能力,解决"如何快速定位单个元素"的问题。
-
迭代器:聚焦"元素遍历",定义集合的逐个访问规则,解决"如何批量获取所有元素"的问题。
2. 语法与接口依赖不同
-
索引器 :无强制接口依赖,通过
this[参数]定义,支持get/set访问器,参数类型灵活(int、string、自定义类型均可)。 -
迭代器 :必须依赖
IEnumerable/IEnumerable<T>接口(需实现GetEnumerator方法),核心是yield关键字,无set逻辑,仅负责读取元素。
3. 底层实现逻辑不同
-
索引器:编译后会生成get_Item和set_Item方法(对应get/set访问器),本质是属性的语法糖,访问时直接调用对应方法,无状态维护。
-
迭代器 :编译时会自动生成一个"状态机类"(实现IEnumerator接口),
yield return会被拆解为状态切换逻辑(初始、遍历中、结束),自动维护Current、MoveNext等状态,遍历结束后自动释放资源。
4. 适用场景不同
-
索引器:适用于自定义"可通过下标定位元素"的类型,如字典(按键索引)、自定义集合(按索引/标识定位),强调"随机访问"能力。
-
迭代器:适用于所有需要支持foreach遍历的集合类型,尤其是底层存储结构复杂(如树、链表)、需要自定义遍历规则(如过滤、排序、分页)的场景,强调"顺序遍历"能力。
5. 访问方式不同
-
索引器:支持随机访问,可直接通过下标获取任意位置元素(如students[5]),无需遍历前置元素。
-
迭代器:仅支持顺序访问,必须从第一个元素开始逐个获取,无法直接跳转到指定位置(除非自定义逻辑)。
四、深度拓展:易混淆场景辨析
1. 索引器和属性的关系?
索引器是特殊的属性,区别在于:属性对应单个值,索引器对应一组值(通过参数定位);属性有名称,索引器无名称(仅用this标识)。
2. 迭代器必须用yield吗?
不是。手动实现IEnumerator接口(维护Current、MoveNext、Reset方法)也能实现迭代器,但yield是语法糖,可大幅简化代码,避免手动管理状态机逻辑。
3. 能否同时使用索引器和迭代器?
完全可以。例如List既实现了索引器(支持list[0]访问),又实现了迭代器(支持foreach遍历),二者互补,分别解决随机访问和顺序遍历的需求。
五、面试总结
索引器和迭代器的核心区别可概括为:索引器管"访问",迭代器管"遍历"。索引器让对象像数组一样被随机访问,迭代器让集合像List一样被顺序遍历,二者语法、底层、场景均不同,切勿混淆。
面试中若被问及,可先点明核心差异,再结合代码案例说明用法,最后补充底层实现(索引器是属性语法糖,迭代器是状态机语法糖),即可展现对知识点的深度理解。