CodeGym /課程 /C# SELF /Extension Members:索引子(Indexers)

Extension Members:索引子(Indexers)

C# SELF
等級 18, 課堂 4
開放

1. 前言

想像一下,我們有我們最愛的 class DogShelter,它存著一堆狗狗的 collection。在前面的課程我們已經加過 indexer,可以用編號拿到狗狗:Dog firstDog = myShelter[0];。這超方便的!

但如果我們的使用者想要不是用編號,而是用名字來找狗狗呢?或是用品種?甚至是用一堆條件組合?當然啦,我們可以加像 GetDogByName("Buddy") 或 GetDogByBreedAndAge("Labrador", 5) 這種 method。這樣做完全沒問題。

不過有時候真的很想要更像 array 一樣、直覺一點的存取方式。像這樣寫:Dog buddy = myShelter["Buddy"]; 或 Dog oldLab = myShelter["Labrador", 8];。

如果 DogShelter 是我們自己寫的 class,直接加新的 indexer 就好。但如果 DogShelter 是外部 library 的 class,不能改?或是我們只想加一個很特殊的存取方式,不想把主 class 弄得很亂?

這時候就輪到 擴充索引子(Extension Indexers) 上場啦!

2. 「中括號」也能加在外面

還記得我們上次課程加過 DisplayName 這個 extension property 給 Dog 嗎?indexer 其實也差不多!

擴充索引子 就是寫在 static class 裡的 static indexer,可以讓你對已經存在的型別用 obj[索引] 這種語法,就算原本 class 沒有這種 indexer,或你想加不同型別的參數都可以。

這就像你買了一台冰箱,然後自己發明一個方法,只要敲某個地方它就會自動掉出一瓶可樂。冰箱本體沒變,但功能「外掛」上去了!

擴充索引子的語法


public static class MyExtensionClass
{
    extension(ObjectType 實例)
    {    
        public static ReturnType this[索引型別 index ]
        {
            get
            {
                // 讀取邏輯,使用 實例 和 index
                return ...;
            }
            set
            {
                // 用 實例、index 和 'value' 關鍵字
                // 'value' 就是新值
            }
        }
    }        
}
擴充索引子的語法(Extension Indexer)

注意 this 擴充物件型別 實例 這段。這跟我們之前 extension method 跟 property 的寫法一樣。實例 就是我們在 get/set accessor 裡要用來代表被擴充的物件。

3. 怎麼宣告 Extension Indexer(腦袋不會炸)?

語法跟我們上次講的 Extension Properties 很像,只是多了 index 參數。來看個最小範例:


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 用名字查找的 Extension Indexer

熟悉的元素:

  • this 放在第一個參數前 —— 這是 Extension Members 的規定(要擴充的物件)。
  • class 名字後面就是你要在中括號裡用的參數。
這跟一般 indexer 幾乎一樣,只是你不用去改原本的 class!

實作練習:幫 DogShelter 加上用名字查找的 indexer

來改一下我們的練習專案。假設我們有一個狗狗收容所,每隻狗的名字都不一樣:

DogShelter class(library/外部程式碼)

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);

    // 舊的 indexer,用編號
    public Dog this[int index]
    {
        get => dogs[index];
        set => dogs[index] = value;
    }

    public IEnumerator<Dog> GetEnumerator() => dogs.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

我們想要:shelter["Буся"]

以前只能用 method:

// C# 14 以前:
public static Dog? FindByName(this DogShelter shelter, string name) { ... }

現在可以用 Extension Indexer:


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 = "Буся", Age = 3 });
shelter.AddDog(new Dog { Name = "Тузик", Age = 5 });

// 用 extension-indexer!
Dog busya = shelter["Буся"]!;
Console.WriteLine(busya.Age);

shelter["Буся"] = new Dog { Name = "Буся", Age = 4 };

視覺化:到底發生什麼事?

操作 以前怎麼做 有 Extension Indexer 之後
用名字找狗 shelter.FindByName("X") shelter["X"]
用名字改狗狗資料 shelter.UpdateName("X", ..) shelter["X"] = ...

4. Extension Indexers 的細節與特性

編譯器跟作用範圍

  • Extension Indexer 一定要寫在 public static class 裡(跟 extension methods 一樣)。
  • 記得要加對的 using。沒加的話,編譯器不會報錯,但你的 code 會編不過。
  • 如果原本的 class 已經有一樣的 indexer,就不能擴充(簽名要不一樣)。

set accessor 的實作

你可以只寫 get(這樣 indexer 只能讀),也可以加上 set(像上面例子)—— 這樣就能用 indexer 讀寫資料。

傳值還是傳參考

Extension Indexer 是針對你要擴充的物件實例(this 放第一個參數)。如果物件是 reference type,你就是直接改它的狀態。

一個 class 可以有多個 indexer

沒問題!你可以宣告多個 extension-indexer,只要參數型別不一樣就好。例如用年齡找:shelter[5](舊的)、shelter["Буся"](新的)、shelter[age: 3](你想加也可以)。

範例:幫 DogShelter 加兩個 indexer


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["Тони"];          // 用名字
shelter["Тузик"] = new Dog { Name = "Тузик", Age = 9 };

實際應用場景

  • 外部 library: 你想幫外部 class 加更多 indexer,不用動原始碼。例如操作訂單 collection,可以用編號、日期、狀態等方式查找,不用寫一堆包裝 method。
  • 「Adapter pattern」: 你把舊的 collection API 變成現代、簡潔、更 C# 風格的寫法,還不會破壞相容性。
  • Legacy code 遷移: 幫已經寫好的型別加新功能,不用動到舊 code 跟測試。
  • 測試方便: 你可以加臨時的 indexer 來做特殊查找(例如只在測試時用的條件),不用把主 class 弄髒。

5. Extension Indexers 常見錯誤與陷阱

如果原本的 class 已經有一模一樣簽名的 indexer,extension-indexer 不會被呼叫 —— 會優先用原本的 indexer。

Extension-indexer 也是 extension member,如果沒加對的 using(namespace),擴充功能就不會出現。

另一個常見錯誤是回傳 null 卻沒提醒使用者。如果有人不小心查不存在的元素,extension-indexer 回傳 null,可能會在別的地方爆 NullReferenceException。建議你要想清楚要怎麼處理:丟例外、回傳特殊物件還是就直接回 null。

如果你有多個 extension-indexer,要確保它們的參數型別跟數量都不一樣。不能有兩個簽名一樣的 indexer —— 編譯器會報錯。

Extension-indexer 只能用在物件實例,不能用在 static type。

2
任務
C# SELF, 等級 18, 課堂 4
上鎖
為數字列表添加擴充索引子
為數字列表添加擴充索引子
1
問卷/小測驗
認識索引子,等級 18,課堂 4
未開放
認識索引子
索引子跟 Extension Members
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION