Skip to content

C# - 索引器

索引器(Indexer)是一种特殊的成员,它允许对象像数组一样通过索引进行访问。当你为一个类定义索引器时,可以使用熟悉的数组访问运算符([])来访问该类的实例。这为访问自定义集合或数据结构中的元素提供了一种更直观、更富有表现力的语法。

一维索引器的语法与属性(property)类似,但它使用 this 关键字作为名称,并在方括号中包含一个索引参数。

public T this[int index]
{
// get 访问器用于读取值。
get
{
// 返回指定索引处值的逻辑。
}
// set 访问器用于赋值。
set
{
// 设置指定索引处值的逻辑。
}
}

与属性一样,索引器可以包含 get 和/或 set 访问器。你还可以使用表达式主体语法(expression-bodied syntax)来实现更简洁的代码,这在现代 C# 中很常见。

属性(property)用于访问特定的、命名的成员变量,而索引器(indexer)则用于通过键(可以是整数、字符串或其他类型)访问集合状对象中的元素。这有效地抽象了类的内部存储机制,使其可以像虚拟数组或字典一样行为。

让我们创建一个 Sentence 类,用于存储单词的集合。我们将添加一个索引器,以便通过位置访问单个单词。此示例使用了现代 C# 特性,例如表达式主体成员(expression-bodied members)和适当的异常处理(exception handling)。

using System;
public class Sentence
{
private readonly string[] _words;
public Sentence(string text)
{
_words = text.Split(' ');
}
// 用于通过整数位置访问单词的索引器
public string this[int index]
{
get
{
if (index < 0 || index >= _words.Length)
{
throw new IndexOutOfRangeException($"Index {index} is out of range.");
}
return _words[index];
}
set
{
if (index < 0 || index >= _words.Length)
{
throw new IndexOutOfRangeException($"Index {index} is out of range.");
}
_words[index] = value;
}
}
public int Length => _words.Length;
}
public class Program
{
public static void Main(string[] args)
{
var sentence = new Sentence("The quick brown fox jumps over the lazy dog");
// Using the 'get' accessor of the indexer
Console.WriteLine($"The fourth word is: {sentence[3]}"); // 输出: fox
// Using the 'set' accessor of the indexer
sentence[1] = "slow";
Console.WriteLine("The modified sentence is:");
for (int i = 0; i < sentence.Length; i++)
{
Console.Write($"{sentence[i]} ");
}
// 输出: The slow brown fox jumps over the lazy dog
}
}

在前面的示例中,我们检查了索引,如果索引无效则抛出 IndexOutOfRangeException。这是标准做法,与内置 .NET 集合的行为方式一致。返回 null 或空字符串可能会隐藏错误,并导致代码后面出现意料之外的 NullReferenceException。

索引器不限于整数键。你可以使用任何类型,例如 string,来创建类似字典的行为。这通常通过使用 Dictionary<TKey, TValue> 作为内部数据存储来实现。

下面是一个简单的配置管理器示例,它使用字符串索引器来获取和设置配置值。

using System;
using System.Collections.Generic;
public class AppSettings
{
private readonly Dictionary<string, string> _settings = new();
// 字符串索引器
public string? this[string key]
{
get
{
// TryGetValue 比先检查 ContainsKey 再访问键更高效
_settings.TryGetValue(key, out var value);
return value; // 如果键未找到,则返回 null
}
set
{
if (value is null)
{
_settings.Remove(key);
}
else
{
_settings[key] = value;
}
}
}
}
public class Program
{
public static void Main()
{
var settings = new AppSettings();
// 使用 set 访问器
settings["ApiUrl"] = "https://api.example.com";
settings["Timeout"] = "30000";
// 使用 get 访问器
Console.WriteLine($"API URL: {settings["ApiUrl"]}");
Console.WriteLine($"Non-existent Key: {settings["ApiKey"] ?? "Not Found"}");
}
}

一个类可以有多个索引器,这一特性称为索引器重载(indexer overloading)。每个重载都必须具有不同数量或类型的参数。这允许以灵活的方式访问数据。

代表 2D 网格的类是索引器重载的完美候选。我们可以提供一个索引器来访问特定的单元格 (row, col),并提供另一个来访问整个行。

using System;
public class Grid<T>
{
private readonly T[,] _matrix;
public Grid(int rows, int cols)
{
if (rows <= 0) throw new ArgumentOutOfRangeException(nameof(rows));
if (cols <= 0) throw new ArgumentOutOfRangeException(nameof(cols));
_matrix = new T[rows, cols];
}
public int Rows => _matrix.GetLength(0);
public int Cols => _matrix.GetLength(1);
// 带有两个参数的索引器,用于单元格访问
public T this[int row, int col]
{
get => _matrix[row, col];
set => _matrix[row, col] = value;
}
// 重载的索引器,用于获取整个行(只读)
public T[] this[int row]
{
get
{
var rowData = new T[Cols];
for (int i = 0; i < Cols; i++)
{
rowData[i] = _matrix[row, i];
}
return rowData;
}
}
}
public class Program
{
public static void Main()
{
var grid = new Grid<int>(3, 4);
grid[1, 2] = 99; // 设置特定单元格
Console.WriteLine($"Value at (1, 2): {grid[1, 2]}");
var secondRow = grid[1]; // 获取整个第二行
Console.WriteLine($"Value from the extracted row at index 2: {secondRow[2]}");
}
}
  • IndexOutOfRangeException:这是最常见的错误。当你尝试访问超出底层数据结构有效边界的索引时会发生此错误。始终在你的索引器实现中添加边界检查。
  • KeyNotFoundException:在使用基于字典的索引器时,当你尝试访问不存在的键时会发生此错误。使用 TryGetValue 来优雅地处理这种情况。
  • Ambiguous Overloads:如果你定义了多个索引器,请确保它们的签名(signature)是唯一的。如果对索引器的调用存在歧义,编译器将报告错误。

索引器是一个强大的功能,可以为你的类创建自定义且直观的 API。为了加深理解,可以考虑探索:

  • 在索引器旁实现 IEnumerable<T> 等标准接口,以在你的自定义类型上启用 LINQ 查询和 foreach 循环。
  • 在 C# 8.0 的索引和范围(indices and ranges)功能背景下探索索引器,它提供了更简洁的语法来从末尾访问元素或切片集合(如 myCollection[^1])。
  • 查阅内置 .NET 集合(如 List<T> 和 Dictionary<TKey, TValue>)的源代码,了解它们是如何实现的。