📚 陣列、集合與泛型
📌 什麼是陣列?
陣列就像一排置物櫃,每個櫃子都有編號(索引),你可以透過編號快速找到裡面的東西。
一維陣列
// 宣告一個可以放 5 個整數的陣列(就像準備 5 個櫃子)
int[] numbers = new int[5];
// 把數字放進第一個櫃子(索引從 0 開始)
numbers[0] = 10;
// 把數字放進第二個櫃子
numbers[1] = 20;
// 也可以在宣告時直接放入資料
int[] scores = { 90, 85, 78, 92, 88 };
// 用 for 迴圈走訪每個櫃子
for (int i = 0; i < scores.Length; i++) // Length 回傳陣列的長度
{
// 印出每個櫃子裡的分數
Console.WriteLine($"第 {i} 個分數:{scores[i]}");
}
二維陣列
// 宣告一個 3x3 的二維陣列(像一張表格,3 列 3 行)
int[,] matrix = new int[3, 3];
// 在第 0 列、第 1 行放入數字 5
matrix[0, 1] = 5;
// 也可以直接初始化整張表格
int[,] grid = {
{ 1, 2, 3 }, // 第 0 列的三個值
{ 4, 5, 6 }, // 第 1 列的三個值
{ 7, 8, 9 } // 第 2 列的三個值
};
// 用雙層迴圈走訪整張表格
for (int row = 0; row < grid.GetLength(0); row++) // GetLength(0) 取得列數
{
for (int col = 0; col < grid.GetLength(1); col++) // GetLength(1) 取得行數
{
// 印出每個格子的值,用 tab 分隔
Console.Write($"{grid[row, col]}\t");
}
// 每一列印完後換行
Console.WriteLine();
}
不規則陣列(Jagged Array)
// 不規則陣列像是每一列長度可以不同的表格(像階梯)
int[][] jagged = new int[3][]; // 宣告有 3 列
// 第 0 列有 2 個元素
jagged[0] = new int[] { 1, 2 };
// 第 1 列有 4 個元素
jagged[1] = new int[] { 3, 4, 5, 6 };
// 第 2 列有 1 個元素
jagged[2] = new int[] { 7 };
// 走訪不規則陣列
for (int i = 0; i < jagged.Length; i++) // 外層走每一列
{
for (int j = 0; j < jagged[i].Length; j++) // 內層走該列的每個元素
{
// 印出每個元素
Console.Write($"{jagged[i][j]} ");
}
// 每一列印完換行
Console.WriteLine();
}
📌 常用集合類別
List<T> — 動態陣列
// List 就像一個可以自動伸縮的書架
List<string> fruits = new List<string>(); // 建立一個空書架
// 加入水果(往書架放書)
fruits.Add("蘋果"); // 加入第一個元素
fruits.Add("香蕉"); // 加入第二個元素
fruits.Add("橘子"); // 加入第三個元素
// 移除指定的水果
fruits.Remove("香蕉"); // 把香蕉從書架拿走
// 檢查是否包含某水果
bool hasApple = fruits.Contains("蘋果"); // 回傳 true
// 取得元素數量
int count = fruits.Count; // 回傳 2(因為香蕉已移除)
// 用 foreach 走訪所有水果
foreach (string fruit in fruits) // 逐一取出每個水果
{
// 印出水果名稱
Console.WriteLine(fruit);
}
Dictionary<TKey, TValue> — 字典
// Dictionary 就像一本電話簿,用名字(Key)查電話(Value)
Dictionary<string, int> phoneBook = new Dictionary<string, int>();
// 加入聯絡人和電話號碼
phoneBook["小明"] = 12345678; // 小明的電話
phoneBook["小華"] = 87654321; // 小華的電話
// 用名字查電話
if (phoneBook.ContainsKey("小明")) // 先確認電話簿裡有沒有小明
{
// 印出小明的電話號碼
Console.WriteLine($"小明的電話:{phoneBook["小明"]}");
}
// 安全地取值(推薦用法)
if (phoneBook.TryGetValue("小華", out int phone)) // 嘗試取值,成功則放入 phone
{
// 取值成功,印出電話
Console.WriteLine($"小華的電話:{phone}");
}
// 走訪所有鍵值對
foreach (var pair in phoneBook) // pair 包含 Key 和 Value
{
// 印出每個人的名字和電話
Console.WriteLine($"{pair.Key}: {pair.Value}");
}
HashSet<T>、Queue<T>、Stack<T>
// HashSet 像是一個「不允許重複」的集合(像一組不重複的印章)
HashSet<int> uniqueNumbers = new HashSet<int>();
uniqueNumbers.Add(1); // 加入 1,成功
uniqueNumbers.Add(2); // 加入 2,成功
uniqueNumbers.Add(1); // 再加入 1,會被忽略(已存在)
// Queue 是先進先出(像排隊買票,先到先服務)
Queue<string> line = new Queue<string>();
line.Enqueue("第一位客人"); // 排入隊伍
line.Enqueue("第二位客人"); // 排入隊伍
string first = line.Dequeue(); // 取出最前面的人(第一位客人)
// Stack 是後進先出(像疊盤子,最上面的先拿)
Stack<string> plates = new Stack<string>();
plates.Push("盤子A"); // 放入最底層
plates.Push("盤子B"); // 放在 A 上面
string top = plates.Pop(); // 拿走最上面的(盤子B)
📌 泛型(Generics)
泛型就像是一個「萬用模具」,你可以決定要用它來做什麼形狀的餅乾。
泛型方法
// T 是型別參數,呼叫時才決定實際型別
static T GetMax<T>(T a, T b) where T : IComparable<T> // 約束 T 必須可比較
{
// 如果 a 大於 b,回傳 a;否則回傳 b
return a.CompareTo(b) > 0 ? a : b;
}
// 使用泛型方法
int maxInt = GetMax(10, 20); // T 被推斷為 int,回傳 20
string maxStr = GetMax("abc", "xyz"); // T 被推斷為 string,回傳 "xyz"
泛型類別
// 泛型類別:一個可以裝任何型別的箱子
public class Box<T> // T 代表箱子裡要裝什麼型別的東西
{
// 箱子裡的物品
public T Item { get; set; }
// 建構函式,建立箱子時放入物品
public Box(T item)
{
Item = item; // 把物品放進箱子
}
// 顯示箱子裡的物品
public void ShowItem()
{
// 印出物品的內容
Console.WriteLine($"箱子裡有:{Item}");
}
}
// 建立一個裝 int 的箱子
Box<int> intBox = new Box<int>(42);
// 建立一個裝 string 的箱子
Box<string> strBox = new Box<string>("Hello");
泛型約束
// where T : class → T 必須是參考型別(類別)
public class Repository<T> where T : class
{
private List<T> _items = new List<T>(); // 儲存資料的清單
public void Add(T item) // 加入一筆資料
{
_items.Add(item); // 把資料加入清單
}
}
// where T : struct → T 必須是值型別(int, bool 等)
// where T : new() → T 必須有無參數建構函式
// where T : IComparable → T 必須實作 IComparable 介面
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:索引超出範圍
// ❌ 錯誤寫法
int[] arr = { 1, 2, 3 }; // 陣列只有索引 0, 1, 2
Console.WriteLine(arr[3]); // 💥 IndexOutOfRangeException!索引 3 不存在
// ✅ 正確寫法
int[] arr = { 1, 2, 3 }; // 陣列只有索引 0, 1, 2
if (arr.Length > 3) // 先檢查長度是否足夠
{
Console.WriteLine(arr[3]); // 安全存取
}
// 記住:陣列索引從 0 開始,最大索引是 Length - 1
解釋: 陣列的索引從 0 開始,長度為 3 的陣列最大索引是 2。存取索引 3 就像去找第 4 個櫃子,但你只有 3 個。
❌ 錯誤 2:在 foreach 中修改集合
// ❌ 錯誤寫法
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
foreach (int n in numbers) // 正在走訪集合
{
if (n % 2 == 0) // 如果是偶數
{
numbers.Remove(n); // 💥 InvalidOperationException!不能在走訪時修改
}
}
// ✅ 正確寫法:用 RemoveAll 或反向 for 迴圈
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
numbers.RemoveAll(n => n % 2 == 0); // 一次移除所有偶數,安全又簡潔
解釋: foreach 走訪時,集合的結構不能被改變。就像你在數書架上的書時,別人不能同時抽走其中一本。
❌ 錯誤 3:泛型約束錯誤
// ❌ 錯誤寫法
public class Cache<T> where T : struct // 約束 T 為值型別
{
public T Data { get; set; }
}
Cache<string> cache = new Cache<string>(); // 💥 編譯錯誤!string 是參考型別,不符合 struct 約束
// ✅ 正確寫法:根據需求選擇正確的約束
public class Cache<T> where T : class // 改為 class 約束,允許參考型別
{
public T Data { get; set; } // 現在可以用 string 了
}
Cache<string> cache = new Cache<string>(); // ✅ 編譯通過
解釋: struct 約束只允許值型別(int, bool, struct 等),而 string 是參考型別。選錯約束就像把方形積木塞進圓形洞裡。