首页 / C# 入门教程 / 索引器

C# 入门教程

索引器

本教程共 100 篇 · 第 36 篇 · 更新于 2026-07-31 · 约 11 分钟阅读

C#C# 入门教程索引器indexer面向对象

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],是因为下标访问是语言内建语法。但自定义类型默认没有这能力。索引器本质上是一种语法糖:它把「按某个键取一段数据」封装成方括号形式,让对象对外表现得像集合,对内却可以接任意存储(数组、字典、数据库、甚至实时计算)。没有它,你就得暴露一堆 GetByIndexSetByName 之类的方法,调用起来既啰嗦又失去「像一个数组」的直觉。

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 更轻,只有需要给访问附加语义时才自定义索引器。StringBuildBitArray 等框架类型正是索引器的现实范例。