索引器
本教程共 100 篇 · 第 36 篇 · 更新于 2026-07-31 · 约 11 分钟阅读
36. 索引器
本节目标:学完你能给自己的类加上
obj[i]这样的下标访问,甚至用字符串做键。
数组能用 names[0] 取下标,挺方便。C# 允许你给自己的类也加上这种能力,这叫索引器(indexer)。它让对象表现得像一个「虚拟数组」。
基础语法:整数索引器
索引器用 this[类型 参数] 声明,同样有 get/set。下面这个类内部用 List<string> 存名字,对外提供 names[0] 这样的访问。
namespace CSharpDemo;
class NameList
{
private List<string> names = [];
public string this[int index]
{
get => names[index];
set => names[index] = value;
}
public void Add(string name) => names.Add(name);
}
var list = new NameList();
list.Add("小明");
list.Add("小红");
Console.WriteLine(list[0]); // 小明
list[1] = "小刚";
Console.WriteLine(list[1]); // 小刚
Note索引器和属性很像,但属性有名字、索引器没有名字,只写
this[...]。它把「按位置取一段数据」这件事包装起来。
只读 / 只写索引器
和属性一样,索引器也能只留 get 或只留 set。只读索引器常用于「按条件查出来、不让外部乱改」的场景。
namespace CSharpDemo;
class ReadOnlyList
{
private int[] numbers = [10, 20, 30];
public int this[int i] => numbers[i]; // 只有 get
}
var r = new ReadOnlyList();
Console.WriteLine(r[2]); // 30
字符串索引器:按名字查
索引器的参数不一定是整数。最实用的变体是字符串索引器,用一个键直接取值,类似字典。下面这个类按学生姓名查分数。
namespace CSharpDemo;
class ScoreBook
{
private Dictionary<string, int> scores = new();
public int this[string name]
{
get => scores.TryGetValue(name, out var v) ? v : -1;
set => scores[name] = value;
}
}
var book = new ScoreBook();
book["小明"] = 95;
book["小红"] = 88;
Console.WriteLine(book["小明"]); // 95
Console.WriteLine(book["小刚"]); // -1,没找到
Tip字符串索引器比
Dictionary更「面向对象」:你能在get里藏默认值和容错逻辑,调用方写起来却像查数组一样自然。
索引器可以重载
同一个类能提供多个索引器,只要参数类型或个数不同。比如同时支持「按学号 int」和「按姓名 string」两种查法。
namespace CSharpDemo;
class StudentStore
{
private Dictionary<int, string> byId = new();
private Dictionary<string, int> byName = new();
public string this[int id]
{
get => byId.TryGetValue(id, out var n) ? n : "未知";
set => byId[id] = value;
}
public int this[string name]
{
get => byName.TryGetValue(name, out var i) ? i : -1;
set => byName[name] = value;
}
}
var store = new StudentStore();
store[1] = "小明";
store["小明"] = 1;
Console.WriteLine(store[1]); // 小明
Console.WriteLine(store["小明"]); // 1
Note重载索引器和重载方法同理:编译器按传入下标时的类型挑选匹配的版本。
什么时候用索引器
- 你的类本质是一个「集合的包装」,希望像数组那样被遍历式访问。
- 想用有意义的键(字符串、枚举)代替裸整数下标。
- 访问时要附带边界检查、默认值、日志等逻辑。
如果只是单纯存数据,直接用 List<T>、Dictionary<K,V> 更省事;索引器适合「包装一层语义」。
索引器与属性到底差在哪
两者都是 get/set 的封装,关键区别:属性有名字(public string Name),索引器没有名字、用 this[...]。属性回答「这个对象的某个性」,索引器回答「按某个键取对象里的一段数据」。所以索引器更像数组访问,而不是普通字段。
索引器和真实数组的不同
背后的存储不一定是数组。索引器可以映射到任意数据源:数据库、字典、甚至现场计算。它还能在 get/set 里加边界检查和日志,而裸数组做不到。
namespace CSharpDemo;
class SafeList
{
private List<int> data = [1, 2, 3];
public int this[int i]
{
get
{
if (i < 0 || i >= data.Count) return 0; // 越界返回 0
return data[i];
}
set
{
while (data.Count <= i) data.Add(0);
data[i] = value;
}
}
}
var s = new SafeList();
Console.WriteLine(s[5]); // 0,不会抛异常
s[2] = 9;
Console.WriteLine(s[2]); // 9
Note真实数组下标越界会直接抛
IndexOutOfRangeException;索引器由你决定越界时怎么办,更可控。
多维索引器
索引器参数不止一个,最常见是二维「行、列」,用来包装矩阵类。
namespace CSharpDemo;
class Matrix
{
private int[,] grid = new int[3, 3];
public int this[int r, int c]
{
get => grid[r, c];
set => grid[r, c] = value;
}
}
var m = new Matrix();
m[0, 0] = 5;
Console.WriteLine(m[0, 0]); // 5
让索引器带日志
因为 get/set 是你写的,可以顺便记日志、统计访问次数,这是裸集合没有的能力。
namespace CSharpDemo;
class LoggedList
{
private List<string> items = [];
public int Hits { get; private set; }
public string this[int i]
{
get { Hits++; return items[i]; }
set { Hits++; items[i] = value; }
}
public void Add(string s) => items.Add(s);
}
var l = new LoggedList();
l.Add("a"); l[0] = "b";
Console.WriteLine($"{l[0]} 被访问 {l.Hits} 次");
Tip常见坑:别在索引器的
get里放耗时操作,因为调用方往往以为它和数组一样快。重活改成显式方法更诚实。
框架里真实的索引器
你其实早就在用索引器了。例如 System.Text.StringBuilder 支持 sb[0] 取下标字符,System.Collections.BitArray 支持 bits[3] 取某一位。它们都不是数组,却因实现了索引器而能用方括号访问。
namespace CSharpDemo;
var sb = new System.Text.StringBuilder("码上学");
Console.WriteLine(sb[0]); // 码,StringBuilder 的索引器
让类同时可被 foreach 遍历
索引器让对象「像数组一样取下标」,若再实现 GetEnumerator,它还能被 foreach 直接遍历,体验接近真正的集合。
namespace CSharpDemo;
class NumList
{
private int[] data = [1, 2, 3];
public int this[int i] => data[i];
public int Count => data.Length;
public IEnumerator<int> GetEnumerator()
{
foreach (var x in data) yield return x;
}
}
foreach (var n in new NumList()) Console.WriteLine(n); // 1 2 3
Note实现
GetEnumerator返回IEnumerator<T>,这类类型就能出现在foreach右侧。索引器负责「按位取」,枚举器负责「整体走」,两者常配套。
多维索引器配合校验
二维索引器很适合包装矩阵,并在 set 里做越界保护,比裸二维数组安全。
namespace CSharpDemo;
class Grid
{
private int[,] cells = new int[3, 3];
public int this[int r, int c]
{
get => cells[r, c];
set
{
if (r < 0 || c < 0) return;
cells[r, c] = value;
}
}
}
var g = new Grid();
g[1, 1] = 9;
Console.WriteLine(g[1, 1]); // 9
Tip常见坑:把索引器当成「免费的集合」——它背后仍是你写的逻辑。若
get每次都重新计算或读外部资源,性能可能远不如真数组。
索引器的返回类型不必是元素
索引器的 get 可以返回任何类型,不必和「元素」直接对应。下面用字符串下标,返回一整条记录对象,而非单个值。
namespace CSharpDemo;
class Registry
{
private Dictionary<string, (int Age, string City)> data = new();
public (int Age, string City) this[string name]
{
get => data.TryGetValue(name, out var v) ? v : (0, "未知");
set => data[name] = value;
}
}
var r = new Registry();
r["小明"] = (18, "北京");
Console.WriteLine($"{r["小明"].Age} 岁 来自 {r["小明"].City}");
Note索引器返回元组、记录甚至自定义对象都没问题。它本质是「按键取一段数据」的语法糖,返回什么由你定。
一个类同时有索引器和普通属性
索引器和属性互不冲突:属性按名字访问,索引器按下标访问,两者可以并存于同一类,各管一摊。
namespace CSharpDemo;
class Shelf
{
private List<string> books = [];
public int Count => books.Count; // 普通属性
public string this[int i] // 索引器
{
get => books[i];
set => books[i] = value;
}
public void Add(string b) => books.Add(b);
}
var s = new Shelf();
s.Add("C# 入门");
Console.WriteLine($"{s.Count} 本,第一本:{s[0]}");
索引器 vs 字典:怎么选
单纯「按键存取值」,直接用 Dictionary<K,V> 更轻更标准。当你需要给访问过程附加语义——比如越界给默认值、访问记日志、读出来的是实时计算的结果、或对象本身「就是一个集合」——才值得包一层索引器。
| 需求 | 直接用 Dictionary | 自定义索引器 |
|---|---|---|
| 只是存查键值 | 适合 | 多余 |
| 访问要带逻辑 | 不方便 | 适合 |
| 对象本身就是集合 | 不太贴合 | 语义更自然 |
Tip判断标准很简单:索引器是为「你的类型」披上集合的外衣;若没有这层语义,裸
Dictionary永远是更省事的选择。
为什么需要索引器(设计初衷)
数组之所以能用 a[i],是因为下标访问是语言内建语法。但自定义类型默认没有这能力。索引器本质上是一种语法糖:它把「按某个键取一段数据」封装成方括号形式,让对象对外表现得像集合,对内却可以接任意存储(数组、字典、数据库、甚至实时计算)。没有它,你就得暴露一堆 GetByIndex、SetByName 之类的方法,调用起来既啰嗦又失去「像一个数组」的直觉。
Note索引器与「属性」是亲兄弟:都靠
get/set封装。差别只在属性有名字、索引器用this[...]按下标。可以把索引器理解为「没有名字、靠键访问的属性」。
最佳实践与常见坑汇总
- 越界怎么处理由你定:真实数组越界会抛
IndexOutOfRangeException;索引器可以由你返回默认值或自行决定,但务必在文档或get里写清约定,别让调用方猜。 get里别藏重活:调用方默认它和数组访问一样快。若每次取值都要读数据库或做复杂计算,宁可改成显式方法(如Find(name)),语义更诚实。- 只存查就用
Dictionary:索引器适合「给类型披上集合外衣并附加语义」;没有这层语义时,裸Dictionary<K,V>永远更轻。 - 能只读就只读:若外部不该修改内部数据,只留
get,避免被随意写入。
Tip判断要不要索引器的简单标准:你的类型「本身就是一个集合」或「访问时必须带逻辑」(边界检查、默认值、日志、实时计算)才上索引器;否则
List<T>/Dictionary<K,V>更省心。
小结
索引器 this[...] 让自定义对象支持 obj[下标] 式访问,语法和属性几乎一样,只是没有名字、用 this。整数索引器最常见,字符串索引器能用键直接取值、很适合查表场景,且返回类型任意(可返回元组或对象)。索引器可只读、可只写、可重载,还能带多维参数;配合 GetEnumerator 还能被 foreach 遍历。它本质是给「集合型」类套一层像数组一样的易用外壳,并在访问时附带边界检查、日志等逻辑。单纯存查键值用 Dictionary 更轻,只有需要给访问附加语义时才自定义索引器。StringBuild、BitArray 等框架类型正是索引器的现实范例。