【C#學習筆記】14《理解Thread 執行緒與 Stack / Heap 記憶體配置》
【C#學習筆記】16《System.Collections.Generic 常用集合》
使用C#時,很常看到的語法之一就是 List<int>、List<string> 這種帶有 <> 的寫法。其實這就是泛型(Generics),也是C#中非常重要的概念。
解釋泛型(Generics)前,必須先了解什麼是List。
List<T>是.NET提供的泛型集合,可以視為可動態調整大小的陣列(Dynamic Array)。
說明上應該解釋成T型別的List,而不是List T,例如範例:
List<int> scores = new List<int>();//建立一個整數的列表
List<string> fruits = new List<string>();//建立一個字串的列表
不像陣列大小固定,List可以隨時新增或刪除元素。
List用法範例
// 建立一個只能存放 int 的 List
var list = new List<int>();
// 新增單一元素
list.Add(1);
// 一次新增多個元素
list.AddRange(new[] { 2, 3, 4 });
// Count 代表目前 List 中的元素數量
Console.WriteLine($"List Count: {list.Count}");
Console.WriteLine();
// 反轉 List 中所有元素的順序
list.Reverse();
Console.WriteLine("List after Reverse:");
Output(list);
// 透過索引存取元素(索引從 0 開始)
var item = list[2];
Console.WriteLine($"Item at index 2: {item}");
Console.WriteLine();
// 取得最後一個元素
var last = list[list.Count - 1];
Console.WriteLine($"Last item: {last}");
Console.WriteLine();
// 尋找值為 3 的元素索引
// 找不到時會回傳 -1
var index = list.FindIndex(x => x == 3);
Console.WriteLine($"Index of item 3: {index}");
Console.WriteLine();
// 判斷 List 是否包含指定元素
var contains = list.Contains(3);
Console.WriteLine($"List contains 3: {contains}");
Console.WriteLine();
// 在指定索引插入元素
// 將數字 5 插入到索引 2 的位置
list.Insert(2, 5);
Console.WriteLine("List after Insert:");
Output(list);
// 移除第一個值為 2 的元素
list.Remove(2);
Console.WriteLine("List after Remove(2):");
Output(list);
// 移除指定索引的元素
// 這裡移除索引 1 的元素
list.RemoveAt(1);
Console.WriteLine("List after RemoveAt(1):");
Output(list);
/// 輸出 List 中所有元素
void Output(List<int> list)
{
foreach (int item in list)
{
// 加上空格方便閱讀,例如:4 3 2 1
Console.Write($"{item} ");
}
Console.WriteLine();
Console.WriteLine();
}
| 方法 / 屬性 | 功能 |
|---|---|
Add() |
新增單一元素到尾端 |
AddRange() |
一次新增多個元素 |
Count |
取得目前元素數量 |
Reverse() |
將整個 List 反轉 |
list[index] |
依索引取得或修改元素 |
FindIndex() |
尋找符合條件元素的索引 |
Contains() |
檢查是否包含指定元素 |
Insert(index, value) |
在指定位置插入元素 |
Remove(value) |
移除第一個符合指定值的元素 |
RemoveAt(index) |
移除指定索引位置的元素 |
總結來說,如果用遊戲開發的角度來理解:
List<Enemy>:管理所有敵人。
List<Bullet>:管理場上的所有子彈。
List<Item>:管理玩家背包中的物品。
List<GameObject>:管理場景中的遊戲物件。
有了List的概念後,接著繼續來解釋泛型(Generics)。
泛型(Generic)可以理解成:將「型別(Type)」當成參數傳入類別或方法。
也就是說,只有在使用的時候,把型別T指定進去,才能確定它的最終型別,所以List<T>是一個泛稱,因此稱為泛型。
List<int> numbers = new List<int>();
List<string> names = new List<string>();
雖然兩者都使用 List,但因為指定了不同的型別,所以:
List<int>:只能存放 int
List<string>:只能存放 string
可以把List<T>想像成一個模板,而T則是告訴它要存放什麼資料型別。
假設沒有泛型,可能需要使用object來存放不同型別的資料:
object value = 100;
object text = "Hello";
使用時還需要自行轉型:
int number = (int)value;
//如果轉型錯誤,就可能在執行時發生例外。
使用泛型後:
List<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
// numbers.Add("ABC"); // ❌ 編譯器直接報錯
編譯器會在編譯階段就幫你檢查型別,而不是等程式執行後才出問題。
型別安全(Type Safety)
減少型別轉換(Casting)
編譯時即可發現錯誤
一份程式碼可重複利用於不同型別
提升實現設計模式(Design Pattern)的效率
Dictionary<TKey, TValue>
適合建立「Key → Value」的對應關係,例如玩家背包中的物品數量:
Dictionary<string, int> inventory = new Dictionary<string, int>();
inventory["Potion"] = 5;
inventory["Arrow"] = 100;
Console.WriteLine(inventory["Potion"]);
Queue<T>(先進先出)
適合NPC排隊、技能排程等情境。
Queue<string> spawnQueue = new Queue<string>();
spawnQueue.Enqueue("Slime");
spawnQueue.Enqueue("Goblin");
Console.WriteLine(spawnQueue.Dequeue());
Stack<T>(後進先出)
適合撤銷操作(Undo)、返回上一個畫面等功能。
Stack<string> menuHistory = new Stack<string>();
menuHistory.Push("Main Menu");
menuHistory.Push("Settings");
menuHistory.Push("Audio");
Console.WriteLine(menuHistory.Pop());
T的命名只是C#使用者之間的潛規則?或是默契?並不是C#強制規定。
class Repository<TEntity>
{
}
class Cache<TKey, TValue>
{
}
class Pair<TFirst, TSecond>
{
}
常用的命名
| 名稱 | 用途 |
|---|---|
T |
任意型別 |
TKey |
Key 的型別 |
TValue |
Value 的型別 |
TEntity |
實體資料型別 |
TResult |
回傳值型別 |
我自己是這樣判斷的:
如果程式碼邏輯完全一樣,只是處理的資料型別不同,那就很適合使用泛型。
情境 1:不需要泛型
假設你在做一個敵人系統:
public class Enemy
{
public string Name;
public int HP;
}
管理敵人的清單:
List<Enemy> enemies = new List<Enemy>();
這裡的List<Enemy>已經是泛型的使用了,但你不用自己設計新的泛型類別。
因為你的系統就是專門處理Enemy,沒有必要讓它支援其他型別。
這種情境下,我會把它當作一種動態資料陣列的使用而非視為泛型。
情境 2:適合使用泛型
假設你想寫一個方法,只負責輸出清單內容。
如果不用泛型,你可能會寫:
void PrintEnemies(List<Enemy> enemies) { }
void PrintItems(List<Item> items) { }
void PrintPlayers(List<Player> players) { }
其實三個方法做的事情都一樣,只是型別不同。
這時可以改成泛型:
void PrintList<T>(List<T> list)
{
foreach (var item in list)
{
Console.WriteLine(item);
}
}
PrintList(enemies);
PrintList(items);
PrintList(players);
一個方法就能處理所有型別!
情境 3:遊戲物件池(Object Pool)
物件池的概念是遊戲中很常用的效能優化方式,目的在於避免大量生成的物件,在生成與銷毀占用太多記憶體。
假設你的遊戲需要物件池來管理子彈、特效、怪物。
如果不用泛型:
public class BulletPool
{
// ...
}
public class EnemyPool
{
// ...
}
public class EffectPool
{
// ...
}
三個類別的邏輯可能有近90%相同。
這時就可以設計成:
public class ObjectPool<T>
{
private List<T> objects = new List<T>();
public void Add(T obj)
{
objects.Add(obj);
}
}
ObjectPool<Bullet> bulletPool = new ObjectPool<Bullet>();
ObjectPool<Enemy> enemyPool = new ObjectPool<Enemy>();
ObjectPool<ParticleSystem> effectPool = new ObjectPool<ParticleSystem>();
建立一個自製的MyStack
public class MyStack<T>
{
private List<T> _list = new();
//將元素添加到列表的末尾。
public void Push(T item) => _list.Add(item);
//彈出元素後,該元素將從列表中移除,回傳該元素的值。
public T Pop()
{
CheckNotEmpty();
int v = _list.Count - 1;
var lastItem = _list[v];
_list.RemoveAt(v);
return lastItem;
}
//查看列表頂部的元素,但不從列表中移除它。回傳該元素的值。
public T Peek()
{
CheckNotEmpty();
return _list[_list.Count - 1];
}
public bool IsEmpty() => _list.Count == 0;//判斷列表是否為空。
public void Clear() => _list.Clear();//清空列表中的所有元素。
public int Count
{
get { return _items.Count; }
}
public override string ToString() => $"Stack with {Count()} items.";
//檢查堆疊是否為空,如果是空的,則拋出InvalidOperationException。
private void CheckNotEmpty()
{
if (IsEmpty())
{
throw new InvalidOperationException("Stack is empty.");
}
}
}
using GameSample;
var stack = new MyStack<int>();
stack.Push(1);
stack.Push(2);
stack.Push(3);
Console.WriteLine($"Peek: {stack.Peek()}");
Console.WriteLine($"Pop: {stack.Pop()}");
Console.WriteLine($"Pop: {stack.Pop()}");
Console.WriteLine($"Count: {stack.Count()}");
如果說泛型(Generics)是「讓任何型別都可以使用」,那泛型約束就是限制哪些型別可以使用這個泛型。
為什麼需要泛型約束?
假設我們想寫一個通用的治療方法:
public static void Heal<T>(T target)
{
target.HP += 100; // ❌ 編譯錯誤
}
編譯器不知道T是否真的有HP屬性,因此無法通過編譯。
這時就可以加入約束:
public static void Heal<T>(T target) where T : Character
{
target.HP += 100;
}
常見的泛型約束
1. where T : BaseClass
限制必須繼承某個類別。
public class Character
{
public int HP;
}
public class Player : Character { }
public void Heal<T>(T target) where T : Character
{
target.HP += 100;
}
2. where T : IInterface
限制必須實作某個介面。
public interface IDamageable
{
void TakeDamage(int damage);
}
public void Attack<T>(T target) where T : IDamageable
{
target.TakeDamage(10);
}
3. where T : new()
限制必須有無參數建構子。
public class Factory<T> where T : new()
{
public T Create()
{
return new T();
}
}
4. where T : class
限制必須是參考型別(Reference Type)。
public class Cache<T> where T : class
{
}
可以使用:
Cache<string> cache = new Cache<string>();
Cache<Player> playerCache = new Cache<Player>();
不能使用:
// Cache<int> ❌
5. where T : struct
限制必須是實值型別(Value Type)。
public class NumberStorage<T> where T : struct
{
}
可以使用:
NumberStorage<int> numbers = new NumberStorage<int>();
NumberStorage<float> floats = new NumberStorage<float>();
更多的細節可以參考官方文件
Dictionary<TKey, TValue>是C#中非常常用的集合類別,可以用來儲存「鍵(Key)→ 值(Value)」的對應關係。官方文件
你可以把它想成現實中的字典:
| Key(單字) | Value(解釋) |
|---|---|
"Apple" |
"蘋果" |
"Dog" |
"狗" |
"Cat" |
"貓" |
假設用List儲存玩家資料:
List<string> players = new List<string>
{
"Alice",
"Bob",
"Charlie"
};
如果想找"Bob",通常需要遍歷整個List。
Dictionary<int, string> players = new Dictionary<int, string>();
players.Add(1001, "Alice");
players.Add(1002, "Bob");
players.Add(1003, "Charlie");
Console.WriteLine(players[1002]);
但使用Dictionary可以直接透過Key取得資料,適合用於快速查找資料。
Dictionary<int, string> players = new Dictionary<int, string>();
int:Key(例如玩家 ID)
string:Value(例如玩家名稱)
Dictionary<int, string> players = new();
// 新增
players.Add(1, "Knight");
// 新增或修改
players[2] = "Mage";
// 取得
string name = players[2];
// 判斷 Key 是否存在
players.ContainsKey(1);
// 安全取得
players.TryGetValue(2, out string result);
// 刪除
players.Remove(1);
// 數量
Console.WriteLine(players.Count);
// 清空
players.Clear();
泛型是C#中非常重要的設計工具,它讓程式碼更安全、更具重複利用性。從List<T>到Dictionary<TKey, TValue>,再到自訂泛型類別與方法,泛型的應用幾乎無所不在。掌握泛型不僅能提升程式設計效率,也能幫助你更好地理解設計模式與架構。