快轉到主要內容

[從C#到Swift] 04. Collection Types

集合類型的可變性 (Mutability of Collections)
#

1. 核心觀念
#

  • 概念解說:在 Swift 中,集合(Arrays, Sets, Dictionaries)的可變性完全取決於它們是被宣告為變數(var)還是常數(let)。這與集合本身的類型定義無關,而是由宣告方式決定。
  • 關鍵語法var (Mutable), let (Immutable)
  • Note

建立不可變的集合(Immutable collections)是一個好的實踐習慣。這不僅讓程式邏輯更清晰,Swift 編譯器也能針對不可變集合進行效能最佳化。

2. 範例
#

// 如果宣告為 var,集合可以在創建後被修改(新增、刪除、變更項目)
var mutableArray = [1, 2, 3]
mutableArray.append(4) 

// 如果宣告為 let,集合的大小和內容都不能改變
let immutableArray = [1, 2, 3]
// immutableArray.append(4) // 這行會報錯

邏輯解說:這段程式碼展示了 var 賦予了集合修改內容的能力,而 let 則完全鎖定了集合的內容與長度。

3. C#
#

概念對應:C# 的 readonly 關鍵字與 Swift 的 let 行為有本質上的不同。

C# 範例

public class CollectionExample {
    // 即使是 readonly,List 的內容依然可以被修改
    public readonly List<int> numbers = new List<int> { 1, 2, 3 };

    public void Modify() {
        numbers.Add(4); // 在 C# 這是合法的!readonly 只保護 reference 不被重新指派
    }
}

關鍵差異分析

  • 語法面:Swift 的 let 宣告集合時,是真正意義上的「完全不可變」(Deep Immutability)。
  • 行為面:這是因為 Swift 的集合類型是 Struct (Value Type),而 C# 的集合是 Class (Reference Type)。在 Swift 中,當你把 Array 指派給 let,代表這個 Value Type 的實體完全不能被更動;而在 C# 中,readonly List 僅代表你不能將變數指向另一個 List 物件,但原本 List 內部的資料是可以被修改的。若要在 C# 達到 Swift let 的效果,通常需要使用 ImmutableList<T>ReadOnlyCollection<T>

陣列 (Arrays)
#

1. 核心觀念
#

  • 概念解說:Array 是有序的清單,允許儲存重複的值。Swift 的 Array 是一個泛型集合。
  • 關鍵語法Array<Element>, [Element], [], .count, .isEmpty, .append(), +=, subscript[], [Range], .insert(), .remove(at:)
  • Note

Swift 的 Array 類型與 Foundation 的 NSArray 類別有橋接(Bridged)關係。

2. 範例
#

// 1. 初始化與實字 (Literals)
var someInts: [Int] = [] // 空陣列
var threeDoubles = Array(repeating: 0.0, count: 3) // [0.0, 0.0, 0.0]
var shoppingList = ["Eggs", "Milk"] // 型別推斷為 [String]

// 2. 陣列合併與新增
var anotherThreeDoubles = Array(repeating: 2.5, count: 3)
var sixDoubles = threeDoubles + anotherThreeDoubles // 使用 + 串接

shoppingList.append("Flour")
shoppingList += ["Baking Powder", "Chocolate Spread", "Cheese", "Butter"]

// 3. 存取與強大的區間修改 (Range Subscript)
var firstItem = shoppingList[0]
shoppingList[0] = "Six eggs"

// 替換 index 4...6 的三個項目為兩個項目 (會改變陣列總長度)
shoppingList[4...6] = ["Bananas", "Apples"]

// 4. 插入與移除
shoppingList.insert("Maple Syrup", at: 0)
let removedItem = shoppingList.remove(at: 0) // 回傳被移除的項目

邏輯解說: Swift 透過 [] 讓空陣列與定義變得非常精簡。特別值得注意的是 + 與 += 運算子,它們讓集合的串接像數值運算一樣直觀。此外,區間修改 (Range Subscript),允許開發者用不同數量的元素替換指定區間,系統會自動處理陣列長度的縮放。

3. C#
#

概念對應

  • Swift 的 [T] 對應 C# 的 List
  • Swift 的 Array(repeating:count:) 類似 C# 的 Enumerable.Repeat().ToList()。
  • Swift 的陣列 […] 對應 C# 的 Collection Initializer {…}

C# 範例

// 1. 初始化與重複預設值
var someInts = new List<int>();
var threeDoubles = Enumerable.Repeat(0.0, 3).ToList(); 
var shoppingList = new List<string> { "Eggs", "Milk" };

// 2. 列表合併與新增
var anotherThreeDoubles = Enumerable.Repeat(2.5, 3).ToList(); // 補上此對照
var sixDoubles = new List<double>(threeDoubles);
sixDoubles.AddRange(anotherThreeDoubles);

shoppingList.Add("Flour");
// 注意:必須補上這三個項目,後續 RemoveRange(4, 3) 才不會報錯
shoppingList.AddRange(new[] { "Baking Powder", "Chocolate Spread", "Cheese", "Butter" });

// 3. 區間修改 (C# 需組合 RemoveRange 與 InsertRange)
shoppingList.RemoveRange(4, 3); 
shoppingList.InsertRange(4, new[] { "Bananas", "Apples" });

// 4. 移除
var removedItem = shoppingList[0]; 
shoppingList.RemoveAt(0); // C# RemoveAt 不會回傳元素,需手動先取值

關鍵差異分析

  • 語法面:Swift 的 += 用於陣列串接比 C# 的 AddRange 更簡潔。Swift 的 Range Subscript ([4...6] = ...) 是 C# 開發者會非常羨慕的功能。
  • 行為面:Swift 的 Array 是 Value Type,C# 的 List<T>Reference Type
    • 在 Swift:var a = [1]; var b = a; b.append(2); -> a 還是 [1],只有 b 變了(Copy-on-Write 機制)。
    • 在 C#:var a = new List<int>{1}; var b = a; b.Add(2); -> a 也會變成 [1, 2],因為它們指向同一個物件。
  • 設計 : Swift 的 remove(at:) 會回傳被移除的元素,方便直接使用;C# 則必須在 RemoveAt 之前先手動透過索引讀取值。

集合 (Sets)
#

1. 核心觀念
#

  • 概念解說
    • Set 是無序且元素唯一的集合。存入 Set 的類型必須遵循 Hashable 協定,以便計算雜湊值來確認唯一性。透過屬性與方法來操作集合。
    • Swift 提供了圖形化的集合運算方法(參考下圖):
      • intersection (交集):兩者都有的。(左上)
      • symmetricDifference (對稱差集):兩者其中之一有的,但不是兩者都有的 (A ∪ B - A ∩ B)。(右上)
      • union (聯集):兩者所有的。(左下)
      • subtracting (差集):A 有但 B 沒有的。(右下)
  • 關鍵語法Set<Element>, insert(_:), intersection(_:), symmetricDifference(_:), union(_:), subtracting(_:)

2. 範例
#

let oddDigits: Set<Int> = [1, 3, 5, 7, 9]
let evenDigits: Set<Int> = [0, 2, 4, 6, 8]
let singleDigitPrimeNumbers: Set<Int> = [2, 3, 5, 7]

oddDigits.union(evenDigits).sorted()
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

oddDigits.intersection(evenDigits).sorted()
// []

oddDigits.subtracting(singleDigitPrimeNumbers).sorted()
// [1, 9] (奇數中扣掉質數 3, 5, 7,剩 1, 9)

oddDigits.symmetricDifference(singleDigitPrimeNumbers).sorted()
// [1, 2, 9] (奇數與質數的聯集,扣掉共同擁有的 3, 5, 7)

邏輯解說: 這些方法都會回傳一個新的 Set,而不會修改原始的 Set,而sorted() 會把它轉為 Array[Element]。

3. C#
#

概念對應: C# 的 HashSet 提供了 IntersectWith, UnionWith, ExceptWith, SymmetricExceptWith

C# 範例

var oddDigits = new HashSet<int> { 1, 3, 5, 7, 9 };
var evenDigits = new HashSet<int> { 0, 2, 4, 6, 8 };

// C# 的 HashSet 方法通常是 "原地修改 (In-place modification)"
var tempSet = new HashSet<int>(oddDigits); // 先複製一份以免改到原資料
tempSet.UnionWith(evenDigits); 
// tempSet 現在變成聯集結果

// 若要像 Swift 一樣回傳新集合,通常需依賴 LINQ
var unionResult = oddDigits.Union(evenDigits).ToHashSet();

關鍵差異分析

  • 行為面:Swift 範例中的 unionsubtracting 等方法是 Functional 的,它們回傳新集合,不改原集合。C# 的 UnionWith 等方法是 Imperative 的,直接修改呼叫該方法的集合。
  • 語法提示:若要在 Swift 中執行類似 C# 的原地修改,需使用 formUnion, formIntersection 等帶有 form 前綴的方法。

集合成員關係與相等性 (Membership and Equality)
#

1. 核心觀念
#

  • 概念解說:判斷集合之間的關係。
    • ==:內容完全相同。
    • isSubset(of:):是否為子集 (被包含)。
    • isSuperset(of:):是否為超集 (包含對方)。
    • isStrictSubset(of:) / isStrictSuperset(of:):真子集/真超集 (包含但不相等)。
    • isDisjoint(with:):是否完全沒有交集。
  • 關鍵語法isSubset(of:), isSuperset(of:), isDisjoint(with:)

2. 範例
#

let houseAnimals: Set<String> = ["🐶", "🐱"]
let farmAnimals: Set<String> = ["🐮", "🐔", "🐑", "🐶", "🐱"]
let cityAnimals: Set<String> = ["🐦", "🐭"]

houseAnimals.isSubset(of: farmAnimals) // true
farmAnimals.isSuperset(of: houseAnimals) // true
farmAnimals.isDisjoint(with: cityAnimals) // true

邏輯解說: 這些方法直觀地對應了數學集合論中的定義。Emoji 在 Swift 中也是合法的 Character/String,可以直接操作。

3. C#
#

概念對應SetEquals, IsSubsetOf, IsSupersetOf, Overlaps

C# 範例

var houseAnimals = new HashSet<string> { "🐶", "🐱" };
var farmAnimals = new HashSet<string> { "🐮", "🐔", "🐑", "🐶", "🐱" };
var cityAnimals = new HashSet<string> { "🐦", "🐭" };

houseAnimals.IsSubsetOf(farmAnimals); // true
farmAnimals.IsSupersetOf(houseAnimals); // true

// C# 檢查是否 "有交集" 用 Overlaps,所以 "無交集" (Disjoint) 要取反
!farmAnimals.Overlaps(cityAnimals); // true

關鍵差異分析

  • 語法面:C# 沒有直接名為 IsDisjoint 的方法,通常使用 !Overlaps 來判斷是否分離。
  • 行為面:兩者的邏輯定義基本一致。值得一提的是 Swift 的 == 運算子直接比較內容值,而 C# 的 HashSet 若使用 == 預設是比較參考 (Reference Equality),必須使用 SetEquals 方法來比較內容物是否相同。

字典 (Dictionaries)
#

1. 核心觀念
#

  • 概念解說:儲存 Key-Value 對應關係的無序集合。Key 必須是 Hashable
  • 關鍵語法[Key: Value], updateValue, removeValue, Subscript key access
  • Note

使用 Subscript 語法存取字典時,回傳的是一個 Optional 值,因為該 Key 可能不存在。

2. 範例
#

var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]

// 新增或修改
airports["LHR"] = "London"

// 使用 updateValue 可以取得舊值
if let oldValue = airports.updateValue("Dublin Airport", forKey: "DUB") {
    print("Old value was \(oldValue)")
}

// 存取值(回傳 Optional)
if let airportName = airports["DUB"] {
    print("Airport is \(airportName)")
} else {
    print("Not found")
}

// 移除
airports["APL"] = nil // 設為 nil 即移除

邏輯解說

  1. [Key: Value] 是標準簡寫。
  2. updateValue 很有用,它在更新同時會回傳「更新前的值」,適合用來做 Log 或邏輯判斷。
  3. 將某個 Key 的值設為 nil,等同於從字典中刪除該 Key。

3. C#
#

概念對應:對應 C# 的 Dictionary<TKey, TValue>

C# 範例

// C#
var airports = new Dictionary<string, string> {
    { "YYZ", "Toronto Pearson" },
    { "DUB", "Dublin" }
};

// 存取值 - C# 的索引器若 Key 不存在會拋出 Exception
// string name = airports["INVALID"]; // Throws KeyNotFoundException

// 安全存取方式
if (airports.TryGetValue("DUB", out string airportName)) {
    Console.WriteLine($"Airport is {airportName}");
}

// 移除
airports.Remove("APL"); // 不能賦值 null 來移除

關鍵差異分析

  • 語法面
    • Swift 的索引存取 dict[key] 永遠回傳 Optional,這迫使開發者處理 Key 不存在的情況(更安全)。
    • C# 的索引存取 dict[key] 假設 Key 存在,否則拋出例外。C# 開發者必須習慣使用 TryGetValue 或是 Swift 的 Optional Binding (if let)。
  • 行為面
    • 在 Swift 中,dict["key"] = nil 是刪除操作。
    • 在 C# 中,如果 Value Type 是 Reference Type,dict["key"] = null 只是把該 Value 設為 null,Key 依然存在於字典中。這是非常容易混淆的點。
    • 迭代 (Iteration):Swift 迭代字典時拿到的是 Tuple (key, value),非常方便解構;C# 拿到的是 KeyValuePair<TKey, TValue> 物件,需存取 .Key.Value 屬性。