List<Product> products =
new List<Product>();
List<T> 適合:
管理一組有順序的資料
→ 可以新增、刪除
→ 可以使用 Index
但今天來看兩個不同的需求。
例如:
P001 → Keyboard
P002 → Mouse
P003 → Monitor
我們希望:
P002
↓
找到 Mouse
這時可以使用:
Dictionary<TKey, TValue>
例如已使用的優惠碼:
WELCOME
SUMMER
VIP
如果再次加入:
WELCOME
我們希望:
WELCOME
→ 只保存一次
這時可以使用:
HashSet<T>
所以今天最重要的兩個 Mental Model:
Dictionary<TKey, TValue>
→ Key → Value
HashSet<T>
→ 不重複
Dictionary<TKey, TValue> 是什麼?Dictionary 可以先理解成:
透過一個 Key,找到對應的 Value。
例如:
P001 → Keyboard
P002 → Mouse
P003 → Monitor
其中:
P001
P002
P003
→ Key
而:
Keyboard
Mouse
Monitor
→ Value
最重要的關係:
Key
↓
找到
↓
Value
例如:
P002
↓
Mouse
這就是 Dictionary 的核心。
TKey 和 TValue 是什麼?先看:
Dictionary<string, string>
第一個 Type:
string
→ TKey
第二個 Type:
string
→ TValue
代表:
Key
→ string
Value
→ string
例如:
"P001" → "Keyboard"
"P002" → "Mouse"
也可以:
Dictionary<int, string>
代表:
Key
→ int
Value
→ string
例如:
1 → Amy
2 → Bob
3 → Kevin
所以:
Dictionary<TKey, TValue>
TKey
→ Key 的 Type
TValue
→ Value 的 Type
Add()建立:
Dictionary<string, string> products =
new Dictionary<string, string>();
代表:
products
Key
→ string
Value
→ string
一開始:
Count
→ 0
接著加入資料:
products.Add(
"P001",
"Keyboard"
);
products.Add(
"P002",
"Mouse"
);
products.Add(
"P003",
"Monitor"
);
現在:
P001 → Keyboard
P002 → Mouse
P003 → Monitor
Add() 需要:
Key
+
Value
例如:
products.Add(
"P001",
"Keyboard"
);
就是:
P001
→ Key
Keyboard
→ Value
Dictionary 有一個重要規則:
同一個 Dictionary 裡,Key 不能重複。
假設已經有:
products.Add(
"P001",
"Keyboard"
);
又執行:
products.Add(
"P001",
"Mouse"
);
就會發生錯誤。
因為:
P001 → Keyboard
P001 → Mouse
↑
相同 Key
如果 Key 可以重複,就無法確定:
P001
→ 到底要找到哪一筆?
所以:
Dictionary
→ Key 必須唯一
但是 Value 可以重複。
例如:
001 → Amy
002 → Amy
沒有問題。
Day 16 的 List 使用:
names[0]
透過 Index 取得資料。
Dictionary 則是:
products["P001"]
透過 Key 取得 Value。
例如:
string productName =
products["P001"];
Console.WriteLine(productName);
結果:
Keyboard
所以:
List<T>
→ [index]
Dictionary<TKey, TValue>
→ [key]
原本:
P001 → Keyboard
可以:
products["P001"] =
"Mechanical Keyboard";
變成:
P001
→ Mechanical Keyboard
所以:
dictionary[key]
→ 取得 Value
dictionary[key] = value
→ 修改 Value
例如:
products["P004"] =
"Webcam";
如果 P004 原本不存在,這行也會新增:
P004 → Webcam
TryGetValue():Key 不存在怎麼辦?假設 Dictionary 只有:
P001
P002
P003
如果直接:
products["P999"]
會發生錯誤。
因為:
P999
→ 不存在
如果不確定 Key 是否存在,可以使用:
TryGetValue()
例如:
if (
products.TryGetValue(
"P002",
out var productName
)
)
{
Console.WriteLine(productName);
}
結果:
Mouse
可以理解成:
TryGetValue("P002", ...)
↓
嘗試用 P002 找 Value
↓
找到了?
如果找到:
true
+
Value 放進 productName
如果找不到:
false
所以:
TryGetValue()
找到
→ true
→ 取得 Value
找不到
→ false
out var productName 是什麼?這裡:
out var productName
可以先理解成:
如果找到 Value,就把找到的資料放進
productName。
例如:
P002
↓
Mouse
↓
productName
這篇先知道用途即可,out 的完整用法之後再深入。
ContainsKey()如果只想知道:
這個 Key 存不存在?
可以:
bool exists =
products.ContainsKey("P001");
整理:
ContainsKey()
→ 只確認 Key 是否存在
TryGetValue()
→ 確認是否存在
→ 同時取得 Value
如果本來就需要 Value,通常直接使用 TryGetValue() 會更方便。
Remove() 與 foreachRemove():根據 Key 刪除假設:
P001 → Keyboard
P002 → Mouse
P003 → Monitor
執行:
products.Remove("P002");
結果:
P001 → Keyboard
P003 → Monitor
所以:
Dictionary.Remove(key)
→ 根據 Key 刪除資料
foreach:走訪 DictionaryDictionary 一樣可以使用 foreach:
foreach (var item in products)
{
Console.WriteLine(
$"{item.Key} - {item.Value}"
);
}
結果:
P001 - Keyboard
P002 - Mouse
P003 - Monitor
每一筆資料都有:
item.Key
item.Value
可以想成:
item
├── Key
└── Value
所以:
foreach
→ 一筆一筆取得 Key / Value
Dictionary<string, Product>:透過 ID 找 Object實務上,Value 不一定只是 string。
也可以是一個 Object。
例如:
class Product
{
public string Name { get; private set; }
public decimal Price { get; private set; }
public Product(
string name,
decimal price)
{
Name = name;
Price = price;
}
}
建立:
Dictionary<string, Product> products =
new Dictionary<string, Product>();
代表:
Key
→ string
Value
→ Product
加入:
products.Add(
"P001",
new Product(
"Keyboard",
1500
)
);
products.Add(
"P002",
new Product(
"Mouse",
800
)
);
products.Add(
"P003",
new Product(
"Monitor",
6000
)
);
可以想成:
P001
↓
Product
├── Name = Keyboard
└── Price = 1500
P002
↓
Product
├── Name = Mouse
└── Price = 800
P003
↓
Product
├── Name = Monitor
└── Price = 6000
假設:
string productId = "P002";
使用:
if (
products.TryGetValue(
productId,
out var product
)
)
{
Console.WriteLine(
$"{product.Name} - ${product.Price}"
);
}
else
{
Console.WriteLine("找不到商品");
}
結果:
Mouse - $800
完整流程:
P002
↓
Dictionary
↓
TryGetValue()
↓
找到 Product Object
↓
使用 Name / Price
這就是 Dictionary 很常見的使用方式:
ProductId → Product
UserId → User
AccountId → Account
HashSet<T>:管理不重複資料接著看另一個需求。
假設我們要保存已使用的優惠碼:
WELCOME
SUMMER
VIP
如果再次加入:
WELCOME
需求是:
WELCOME
→ 只保存一次
這時可以使用:
HashSet<string>
HashSet<T> 可以先理解成:
用來管理不重複元素的 Collection。
但這裡要注意:
HashSet 並不是只能保存
string。
HashSet<T> 裡面的:
T
和 List<T> 一樣,代表:
要保存的元素 Type
所以可以:
HashSet<string>
HashSet<int>
HashSet<Product>
HashSet<Student>
例如:
HashSet<string>
→ 不重複的 string
HashSet<int>
→ 不重複的 int
HashSet<Product>
→ 不重複的 Product Object
先來比較三種 Collection。
List<Product>
[0] Product
[1] Product
[2] Product
List 的重點:
順序
+
Index
Dictionary<string, Product>
P001 → Product
P002 → Product
P003 → Product
Dictionary 的重點:
Key
↓
Value
HashSet<Product>
Product
Product
Product
HashSet 沒有:
Index
也沒有:
Key → Value
每一筆就是一個元素。
可以想成:
HashSet<T>
│
├── T
├── T
└── T
它真正關心的是:
這個元素
→ 是否已經存在?
所以 HashSet 的核心不是:
它排在哪裡?
也不是:
它的 Key 是什麼?
而是:
這筆資料是否重複?
HashSet<string>:保存不重複的字串建立:
HashSet<string> codes =
new HashSet<string>();
加入:
codes.Add("WELCOME");
codes.Add("SUMMER");
codes.Add("VIP");
可以想成:
HashSet<string>
│
├── WELCOME
├── SUMMER
└── VIP
如果再次:
codes.Add("WELCOME");
不會變成:
WELCOME
WELCOME
最後仍然只有一份:
WELCOME
所以:
HashSet<string>
→ 每個 string 不重複
int例如:
HashSet<int> numbers =
new HashSet<int>();
numbers.Add(10);
numbers.Add(20);
numbers.Add(10);
numbers.Add(30);
概念上最後是:
HashSet<int>
10
20
30
第二個:
10
因為已經存在,所以不會再加入。
所以:
HashSet<int>
→ 每個 int 只保存一次
同樣也可以:
HashSet<Product> products =
new HashSet<Product>();
例如:
Product keyboard =
new Product(
"Keyboard",
1500
);
Product mouse =
new Product(
"Mouse",
800
);
products.Add(keyboard);
products.Add(mouse);
概念上:
HashSet<Product>
│
├── Product
│ ├── Name = Keyboard
│ └── Price = 1500
│
└── Product
├── Name = Mouse
└── Price = 800
所以:
HashSet 不只可以保存基本資料,也可以保存自己建立的 Object。
這裡有一個很重要的觀念。
假設:
Product product1 =
new Product(
"Keyboard",
1500
);
Product product2 =
new Product(
"Keyboard",
1500
);
從資料內容來看:
product1
Name = Keyboard
Price = 1500
和:
product2
Name = Keyboard
Price = 1500
看起來完全一樣。
但它們是分別建立出來的兩個 Object:
product1
→ Object A
product2
→ Object B
如果:
HashSet<Product> products =
new HashSet<Product>();
products.Add(product1);
products.Add(product2);
初學者可能會想:
Name 一樣
Price 一樣
→ 應該算重複?
但事情沒有這麼單純。
HashSet 必須先知道:
什麼情況下,兩個 Product 才算「相同」?
例如到底是:
Name 一樣
→ 就算相同?
還是:
Name + Price 都一樣
→ 才算相同?
或者:
ProductId 一樣
→ 才算同一個 Product?
這其實是:
Equality
→ 如何判斷兩個 Object 是否相同
的問題。
因此這篇先使用:
HashSet<string>
HashSet<int>
來學習 HashSet。
因為:
"HELLO" 和 "HELLO"
→ 是否相同很直覺
10 和 10
→ 是否相同也很直覺
而:
Product Object
和
Product Object
要怎麼判斷相同,需要額外定義規則。
這部分之後再深入。
HashSet 名字裡有:
Hash
所以它並不是每次都單純從第一筆開始一筆一筆比較。
初學階段可以先用這個簡化模型理解:
加入資料
↓
計算 Hash
↓
找到資料可能所在的位置
↓
確認是否已有相同資料
↓
不存在
→ 加入
已存在
→ 不加入
例如:
codes.Add("WELCOME");
第一次:
WELCOME
↓
Hash
↓
檢查
↓
不存在
↓
加入成功
再次:
codes.Add("WELCOME");
可以理解成:
WELCOME
↓
Hash
↓
找到可能位置
↓
確認已存在
↓
不加入
這也是為什麼:
HashSet<T>
非常適合:
判斷資料是否已存在
+
避免保存重複元素
這篇先理解到這裡即可。
Hash、Collision、GetHashCode() 等底層細節之後再介紹。
Add() 會回傳 boolHashSet 的 Add() 還有一個很好用的特性。
它會告訴你:
這次是否真的加入成功。
例如:
HashSet<string> codes =
new HashSet<string>();
bool added =
codes.Add("WELCOME");
第一次加入:
added
→ true
如果再次:
bool addedAgain =
codes.Add("WELCOME");
結果:
addedAgain
→ false
因為:
WELCOME
→ 已經存在
所以:
HashSet.Add()
true
→ 原本不存在
→ 成功加入
false
→ 原本已存在
→ 沒有再次加入
Contains()、Remove()、foreachContains()判斷資料是否存在:
bool exists =
codes.Contains("WELCOME");
所以:
Contains()
→ 這個元素存在嗎?
Remove()刪除:
codes.Remove("WELCOME");
所以:
Remove()
→ 刪除指定元素
foreach走訪:
foreach (string code in codes)
{
Console.WriteLine(code);
}
但有一個重要觀念:
不要依賴
HashSet<T>的元素順序。
HashSet 的主要目的不是:
維持第 1 筆
第 2 筆
第 3 筆
而是:
確保元素不重複
因此:
需要順序
→ List<T>
需要不重複
→ HashSet<T>
List、Dictionary、HashSet 怎麼選?現在已經學過三種 Collection。
| Collection | 適合的需求 | 核心概念 |
|---|---|---|
List<T> |
管理有順序的一組資料 | 順序 / Index |
Dictionary<TKey, TValue> |
透過 Key 找資料 | Key → Value |
HashSet<T> |
管理不重複資料 | Unique |
最簡單的判斷:
需要一組有順序的資料?
→ List<T>
需要透過 Key 找資料?
→ Dictionary<TKey, TValue>
需要資料不能重複?
→ HashSet<T>
例如:
商品清單
→ List<Product>
商品 ID → Product
→ Dictionary<string, Product>
已使用優惠碼
→ HashSet<string>
也可以:
不重複的數字
→ HashSet<int>
甚至:
不重複的 Product
→ HashSet<Product>
只是自訂 Object 要先決定:
什麼情況下
→ 兩個 Object 算相同?
重點不是哪一個 Collection 比較好。
而是:
資料需求不同,選擇的 Collection 也不同。
今天最重要的是三個 Mental Model:
List<T>
→ 有順序的一組資料
Dictionary<TKey, TValue>
→ Key → Value
HashSet<T>
→ 一組不重複的 T
而:
T
→ 不只能是 string
例如:
HashSet<string>
→ 不重複的 string
HashSet<int>
→ 不重複的 int
HashSet<Product>
→ 不重複的 Product
只是使用自訂 Object 時,還需要進一步回答:
兩個 Object
→ 什麼情況算相同?
這就是之後會碰到的 Equality。
最後記住:
需要順序
→ List<T>
需要 Key 找資料
→ Dictionary<TKey, TValue>
需要避免重複
→ HashSet<T>
先看資料需求,再選擇適合的 Collection。