CodeGym /课程 /C# SELF /扩展成员:索引器

扩展成员:索引器

C# SELF
第 18 级, 课程 4
可用

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' 就是新值
            }
        }
    }        
}
扩展索引器(Extension Indexer)语法

注意 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;
            }
        }
    }        
}
给 DogShelter 按名字扩展索引器

熟悉的元素:

  • 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 索引器只能用在对象实例上,不能给静态类型加。

1
调查/小测验
认识索引器第 18 级,课程 4
不可用
认识索引器
索引器和扩展成员
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION