1. 關於 collection 的幾句話
到目前為止我們都在用陣列——這是存放同型別多個元素最簡單的方式。但 C# 其實有超多更方便、更強大的 collection,可以解決各種需求。
Collection 就是可以存一堆其他物件的物件。跟陣列不一樣,collection 通常可以動態改變大小,還有很多方便的資料操作方法,針對不同情境也有最佳化。
主要的 collection 類型(快速瀏覽):
List<T> —— 動態陣列,可以自動增減長度:
List<string> names = new List<string>();
names.Add("阿列克斯");
names.Add("瑪莉亞");
Console.WriteLine(names[0]); // 阿列克斯
Console.WriteLine(names.Count); // 2
Dictionary<TKey, TValue> —— "key-value" 配對的 collection,每個 key 對應一個 value:
Dictionary<string, int> ages = new Dictionary<string, int>();
ages["阿列克斯"] = 25;
ages["瑪莉亞"] = 30;
Console.WriteLine(ages["阿列克斯"]); // 25
注意:上面例子我們用中括號 [] 來存取 collection 的元素——就跟陣列一樣!這就是 indexer 的功勞,今天我們就要聊這個。
這只是 collection 的初步認識——詳細的功能、差異和應用我們之後會再深入。現在你只要知道,collection 可以用中括號存取元素,這不是魔法,是語言的特殊機制。
2. Indexer
一般的 C# 物件都是用屬性和方法來操作。但如果你的物件本身就是個 mini-collection 呢?比如:
- 你寫了一個 Week class,要根據數字回傳星期幾的名字:week[0] → "星期一"。
- 或是 Library class,可以用編號找書:library[3] → "向斯萬之路"。
這樣用起來是不是超直覺?每次都寫 library.GetBookByIndex(3) 太囉嗦了,大家都想像用陣列一樣存取!
這時候 indexer 就派上用場啦。
Indexer —— 是 class 的一種特殊成員,讓你可以用中括號語法存取物件,就像陣列一樣: obj[0]、 obj["key"],等等。indexer 外觀看起來像屬性,但沒有名字,參數寫在中括號裡。就像你平常寫 .Name,但這裡是 [i]。
3. 有 indexer 的簡單 collection
來做個小 class,存你最愛的顏色。用字串陣列來存。如果沒有 indexer,就得寫個 GetColor(int i) 方法。但有 indexer 就很帥:
using System;
public class FavoriteColors
{
// 私有欄位存顏色
private string[] colors = new string[5];
// Indexer:
public string this[int index]
{
get
{
// 陣列邊界檢查(封裝!)
if (index < 0 || index >= colors.Length)
throw new IndexOutOfRangeException("顏色索引錯誤!");
return colors[index];
}
set
{
if (index < 0 || index >= colors.Length)
throw new IndexOutOfRangeException("顏色索引錯誤!");
colors[index] = value ?? throw new ArgumentNullException(nameof(value));
}
}
}
class Program
{
static void Main()
{
FavoriteColors favorites = new FavoriteColors();
favorites[0] = "綠色";
favorites[1] = "藍色";
favorites[2] = "紅色";
favorites[10] = "紫色"; // 會丟出例外!
Console.WriteLine(favorites[1]); // 藍色
}
}
這裡發生了什麼?
- 我們做了一個私有陣列,外部不能直接亂動。
- indexer 寫成 public string this[int index],this 表示這是物件本身的 indexer。
- 在 get 和 set 裡做邊界檢查,不讓你越界或寫 null。
- 最後就可以像陣列一樣用 favorites[0] 存取。
4. Indexer 語法細節
語法跟屬性很像,但名字(像 Age)換成 this 加參數:
// Indexer 的簽名(通用範本)
[修飾詞] 回傳型別 this[索引型別 索引名稱]
{
get { ... }
set { ... }
}
範例:經典寫法
public class MyCollection
{
private int[] data = new int[10];
// 可讀寫的 indexer
public int this[int index]
{
get { return data[index]; }
set { data[index] = value; }
}
}
Indexer 不只可以用 int
重點:indexer 不一定只能用 int。你可以用任何型別(只要 key 有意義):
public string this[string colorName]
{
get { /* ... */ }
set { /* ... */ }
}
比如電話簿 class,直接用名字查:
public class PhoneBook
{
private Dictionary<string, int> entries = new Dictionary<string, int>();
public int this[string name]
{
get
{
if (entries.ContainsKey(name))
return entries[name];
return null;
}
set
{
entries[name] = value;
}
}
}
關於 collection 跟 Dictionary<string, string> 怎麼運作,我之後會再講 :P
5. 實戰範例:文字裡的單字計數器
繼續進化我們的小程式。假設現在有個 class,可以計算每個單字在文字中出現幾次。很方便的是,使用者可以直接用中括號查單字出現次數:
using System.Collections.Generic;
public class WordCounter
{
private Dictionary<string, int> counter = new Dictionary<string, int>();
// 用字串(單字)當 indexer
public int this[string word]
{
get
{
if (counter.ContainsKey(word))
return counter[word];
return 0; // 沒這個單字就回傳 0。
}
set
{
counter[word] = value;
}
}
// 從字串加進單字計數
public void AddWords(string text)
{
foreach (var word in text.Split(' ', System.StringSplitOptions.RemoveEmptyEntries))
{
if (counter.ContainsKey(word))
counter[word]++;
else
counter[word] = 1;
}
}
}
// 在 Main:
var wc = new WordCounter();
wc.AddWords("媽媽 洗 桌子 洗 媽媽 爸爸");
Console.WriteLine($"'媽媽' 出現 {wc["媽媽"]} 次");
Console.WriteLine($"'桌子' 出現 {wc["桌子"]} 次");
Console.WriteLine($"'貓' 出現 {wc["貓"]} 次"); // 0
這有什麼實用價值? 這種寫法常用來做自訂 collection、記憶體庫、mapping(像 dictionary 跟 index),甚至自製 DSL(C# 裡的小語言)。
6. 限制與細節
indexer 很強大,但還是有幾個規則跟小陷阱(哪有那麼完美 XD)。
indexer 沒有名字
跟屬性不一樣,indexer 沒有名字,只有 this[參數型別] 這種簽名。如果你寫 public int MyIndexer[int i],編譯器會傻眼。只能用 this。
不能有 static indexer
indexer 只能用在 class 實體,不能用在 static 成員。也就是說不能宣告 static int this[int i],因為 this 只指向物件本身。
可以用不同型別/參數數量 overload
你可以在同一個 class 裡寫多個 indexer,只要參數型別或數量不同。例如:
public string this[int i] { get { ... } set { ... } }
public string this[string key] { get { ... } set { ... } }
這是合法的,編譯器不會搞混——如果參數重複才會報錯。
一定要有 get 或 set
indexer 一定要有 get 或 set。如果只想讀,就拿掉 set,只想寫就拿掉 get。通常兩個都會寫。
7. 實用價值與為什麼要學這個
- indexer 在 資料 collection 裡超常用。很多 .NET class 都有:像 List<T>、Dictionary<TKey,TValue>。你寫 list[2] 就是在用 indexer!
- indexer 可以隱藏內部實作(封裝!),但給你一個直覺又熟悉的介面。用你 class 的人不用管你怎麼存資料,只要用 [index] 就好。
- 你的 code 會變得簡潔又好懂——面試官(還有未來同事)都會很愛這種寫法。
屬性 vs Indexer:比較
| 屬性 | Indexer | |
|---|---|---|
| 名字 | 有(例如 Name) | 沒有(用 this[參數] 取代) |
| 存取方式 | 用名字 | 用 index(或其他 key) |
| static | 可以是 static | 只能用在實體 |
| 一個 class 幾個 | 可以很多 | 可以很多,但參數簽名要不同 |
| 用途 | 存/取資料 | mini-collection、關聯資料 |
8. 常見錯誤與小建議
錯誤 1: 想寫 static indexer。
不行啦——this[...] 只能用在物件上。
錯誤 2: 忘記檢查 index。
如果 get 或 set 沒檢查邊界,程式可能直接爆掉。
錯誤 3: 參數型別搞混。
如果寫兩個參數一樣的 indexer,編譯器會報錯。
錯誤 4: 忘了寫 get 或 set。
如果要能讀又能寫,兩個都要有。
建議: 如果你的 class 包裝陣列——直接把存取都丟給 indexer。這樣又快又直覺。
9. 為什麼要學這個
indexer 讓物件的介面變得簡單、好懂又好用。你可以隱藏內部實作,但給開發者一個超方便的資料存取方式。
內建的 collection 就是這樣做的:string、List<T>、Dictionary<TKey, TValue>、Span<T> 等等。你寫 array[2] 或 text[0],其實就是在用 indexer。
最重要的是——你現在也能寫出 自己的 class,像這些 collection 一樣靈活又簡潔。這就是專業又好讀的 code 的第一步啦!
GO TO FULL VERSION