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' 就是新值
}
}
}
}
注意 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;
}
}
}
}
熟悉的元素:
- this 放在第一個參數前 —— 這是 Extension Members 的規定(要擴充的物件)。
- 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。
GO TO FULL VERSION