1. 入门
想象一下,我们有一个最爱的类 DogShelter,它存着一堆狗的集合。在前面的课里我们已经给它加过索引器,可以通过编号拿到狗狗:Dog firstDog = myShelter[0];。这挺酷的!
但如果用户想按名字拿狗呢?或者按品种?甚至按多个条件组合?当然,我们可以加类似 GetDogByName("Buddy") 或 GetDogByBreedAndAge("Labrador", 5) 这种方法。这种做法完全没毛病。
不过有时候,真的很想让访问方式更像数组,更直观。比如直接写:Dog buddy = myShelter["Buddy"]; 或 Dog oldLab = myShelter["Labrador", 8];。
如果 DogShelter 是我们自己的类,直接加新索引器就行。但如果 DogShelter 是第三方库的类,咱们改不了?或者我们只想加个很特殊的访问方式,不想“污染”主类?
这时候,扩展索引器(Extension Indexers)就闪亮登场啦!
2. “方括号”加在外面
还记得上节课我们给 Dog 加了扩展属性 DisplayName 吗?索引器的玩法也差不多!
扩展索引器——就是在静态类里定义的静态索引器,让你能用 obj[索引] 这种语法访问已有类型的对象,就算这些类型本来没这个索引器,或者你想加个参数类型不一样的索引器。
就像你买了个冰箱,后来想出个办法,敲一下某个地方它就能弹出一瓶可乐。冰箱还是原来的冰箱,但功能“外挂”上去了!
扩展索引器的语法
public static class MyExtensionClass
{
extension(ObjectType 实例)
{
public static ReturnType this[索引类型 index ]
{
get
{
// 读取逻辑,使用 实例 和 index
return ...;
}
set
{
// 用 实例、index 和关键字 'value'
// 'value' 就是新值
}
}
}
}
注意 this 扩展对象类型 实例。这个写法和扩展方法、扩展属性一模一样。实例 就是我们在 get 和 set 里要扩展的对象的名字。
3. 怎么声明 Extension Indexer(别把脑子烧坏)?
语法和上节课讲的 Extension Properties 很像,只不过多了索引参数。来看个最小例子:
public static class DogShelterExtensions
{
extension(DogShelter shelter)
{
public static Dog this[string name]
{
get
{
foreach (var dog in shelter)
{
if (dog.Name == name)
return dog;
}
return null;
}
}
}
}
熟悉的元素:
- this 放在第一个参数前——这是扩展成员(扩展对象)的要求。
- 类名后面跟着参数列表,这些参数会在方括号里用到。
实战:给 DogShelter 按名字加索引器
来改造下我们的练习项目。假设我们有个狗狗收容所,每只狗的名字都不一样:
DogShelter 类(库/外部代码)
public class Dog
{
public string Name { get; set; }
public int Age { get; set; }
}
public class DogShelter : IEnumerable<Dog>
{
private List<Dog> dogs = new List<Dog>();
public void AddDog(Dog dog) => dogs.Add(dog);
// 旧的按编号索引器
public Dog this[int index]
{
get => dogs[index];
set => dogs[index] = value;
}
public IEnumerator<Dog> GetEnumerator() => dogs.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
我们想要:shelter["Busya"]
以前——只能用方法:
// C# 14 之前:
public static Dog? FindByName(this DogShelter shelter, string name) { ... }
现在——直接用扩展索引器:
public static class DogShelterExtensions
{
extension(DogShelter shelter)
{
public static Dog? this[string name]
{
get
{
foreach (var dog in shelter)
if (dog.Name == name)
return dog;
return null;
}
set
{
for (int i = 0; i < shelter.Count; i++)
{
if (shelter[i].Name == name)
{
shelter[i] = value!;
return;
}
}
throw new ArgumentException("Dog not found");
}
}
}
}
现在主代码看起来舒服多了:
var shelter = new DogShelter();
shelter.AddDog(new Dog { Name = "Busya", Age = 3 });
shelter.AddDog(new Dog { Name = "Terry", Age = 5 });
// 用 extension 索引器!
Dog busya = shelter["Busya"]!;
Console.WriteLine(busya.Age);
shelter["Busya"] = new Dog { Name = "Busya", Age = 4 };
可视化:发生了什么?
| 操作 | 以前怎么做 | 用 Extension Indexer |
|---|---|---|
| 按名字查找 | shelter.FindByName("X") | shelter["X"] |
| 按名字修改狗 | shelter.UpdateName("X", ..) | shelter["X"] = ... |
4. Extension Indexers 的细节和特点
编译器和作用域
- Extension Indexer 必须声明在静态 public 类里(和普通 extension methods 一样)。
- 别忘了加上需要的 using。忘了的话——编译器不会报错,但代码编译不了。
- 如果基类已经有同样的索引器——不能扩展(签名必须不一样)。
set 访问器的实现
你可以只写 get(那就是只读索引器)。也可以加上 set(上面例子有)——这样就能读也能写。
值传递和引用传递
Extension Indexer 是针对你加扩展的对象实例(第一个参数前有 this)。如果对象是引用类型,你改的是它的状态。
一个类里多个索引器
没问题——你可以声明多个 extension 索引器,参数组合不一样就行!比如按年龄查:shelter[5](老的),shelter["Busya"](新的),shelter[age: 3](还可以再加)。
例子:给 DogShelter 加两个索引器
public static class DogShelterExtensions
{
extension(DogShelter shelter)
{
// 按名字
public static Dog? this[string name]
{
get => shelter.FirstOrDefault(d => d.Name == name);
set
{
for (int i = 0; i < shelter.Count; i++)
if (shelter[i].Name == name)
shelter[i] = value!;
}
}
// 按年龄——返回第一个符合年龄的狗
public static Dog? this[int age]
{
get => shelter.FirstOrDefault(d => d.Age == age);
}
}
}
现在可以这样写:
var youngDog = shelter[1]; // 按年龄
var tony = shelter["Tony"]; // 按名字
shelter["Terry"] = new Dog { Name = "Terry", Age = 9 };
实际场景
- 外部库:你想给第三方类加更多索引方式,不动源码。比如操作订单集合,按编号、日期、状态查,不用写一堆包装方法。
- “适配器模式”:把老的“笨”API集合变成现代、简洁、更 C# 风格的,不破坏兼容性。
- 迁移老代码:给现有类型加新功能,不碰原有代码和测试。
- 测试方便:可以临时加索引器(比如按测试用的特殊条件查),不用污染主类。
5. 用 Extension Indexers 时的常见错误和坑
如果基类已经有完全一样签名的索引器,extension 索引器不会被调用——优先用基类的。
Extension 索引器本质还是 extension member,没有对应的 using(命名空间引用)就用不了。
还有个常见坑——返回 null 却没提醒用户。如果有人误查了不存在的元素,extension 索引器返回 null,可能会在别的地方引发 NullReferenceException。建议提前想好:是抛异常、返回特殊占位对象,还是直接 null。
如果你有多个 extension 索引器,要保证参数类型和数量唯一。不能有两个签名一样的索引器——编译器会报错。
Extension 索引器只能用在对象实例上,不能给静态类型加。
GO TO FULL VERSION