iT邦幫忙

0

【C#學習筆記】15《泛型(Generics)》

  • 分享至 

  • xImage
  •  

【C#學習筆記】14《理解Thread 執行緒與 Stack / Heap 記憶體配置》
【C#學習筆記】16《System.Collections.Generic 常用集合》


使用C#時,很常看到的語法之一就是 List<int>、List<string> 這種帶有 <> 的寫法。其實這就是泛型(Generics),也是C#中非常重要的概念。

解釋泛型(Generics)前,必須先了解什麼是List。

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)

泛型(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嗎?

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 >類別

Dictionary<TKey, TValue>C#中非常常用的集合類別,可以用來儲存「鍵(Key)→ 值(Value)」的對應關係。官方文件
你可以把它想成現實中的字典

Key(單字) Value(解釋)
"Apple" "蘋果"
"Dog" "狗"
"Cat" "貓"

為什麼不用 List?

假設用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(例如玩家名稱)

常用API一覽

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>,再到自訂泛型類別與方法,泛型的應用幾乎無所不在。掌握泛型不僅能提升程式設計效率,也能幫助你更好地理解設計模式與架構。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言