若要讓合成的 Swift 值保持唯一而不改變其插入順序,請掃描輸入陣列一次、用集合追蹤已接受的鍵,並只將首次出現的值附加到輸出陣列。本指南會建立一個可在 Mac 上執行的小型本機計算。

這項任務會處理一份有順序的合成形狀值清單、移除重複值,並根據保留的項目計算總和。所有操作都在記憶體中進行;範例不會讀取外部輸入,也不會儲存計算結果。

選擇集合前,先定義「唯一」的意思

Swift 陣列會保留順序,也允許重複值。集合只包含唯一值,但沒有明確的順序。因此,將陣列轉成集合再轉回陣列,無法保留首次出現的順序。之後排序會建立排序順序,這可能與插入順序不同。Swift 的集合型別文件說明了這些集合行為。

在此範例中,每個合成值都有一個形狀名稱和大小。預設規則是,只有兩個屬性都相同時,兩個項目才算重複。因此,大小為 12 的圓形和大小為 20 的圓形仍是不同的值。

規則問題 此計算採用的規則
什麼決定輸入順序? 在提供的陣列中的位置
什麼情況下兩個值算重複? 形狀相同且大小相同
保留哪一個項目? 首次出現的項目
後續的重複項目會讓保留值移動嗎? 不會
輸入為空時會怎樣? 傳回空陣列和總和零
哪些值會計入總和? 只有保留的值

這項規則可避免一個常見錯誤:當計算需要區分不同的形狀與大小組合時,卻只按形狀名稱移除項目。範例也會計算只按形狀判定的結果,讓你看出改變規則會造成什麼影響。

在 Mac 上建立完整的本機 Swift 範例

你需要安裝 Swift 工具鏈。在 Terminal 中執行 swift --version,確認命令可用。Swift 官方的建立命令列工具教學說明了這項檢查、可執行套件的建立方式,以及如何執行套件。

為範例建立新目錄,並初始化可執行套件:

mkdir OrderDemo
cd OrderDemo
swift package init --name OrderDemo --type executable

將 Sources 下產生的可執行程式碼檔案替換成以下完整程式。產生的檔名和目錄配置可能因工具鏈而異;請替換現有的進入點,而不是再新增一個。此程式使用 @main 進入點,因此請將它放在名為 OrderDemo.swift 的檔案中;如有需要,請將產生的 main.swift 重新命名。Swift 的命令列教學說明了選用 main.swift 檔案或 @main 進入點的方式。

不需要匯入任何模組或套件相依項目。程式使用標準函式庫中的型別和函式。

struct Tile: Hashable {
    let shape: String
    let size: Int
}

func uniqueFirst<Value, Key: Hashable>(
    _ values: [Value],
    by key: (Value) -> Key
) -> [Value] {
    var seen: Set<Key> = []
    var result: [Value] = []

    for value in values {
        if seen.insert(key(value)).inserted {
            result.append(value)
        }
    }

    return result
}

@main
struct OrderDemo {
    static func main() {
        let input = [
            Tile(shape: "circle", size: 12),
            Tile(shape: "square", size: 8),
            Tile(shape: "circle", size: 12),
            Tile(shape: "circle", size: 20),
            Tile(shape: "square", size: 8)
        ]

        let retained = uniqueFirst(input, by: { $0 })
        let total = retained.reduce(0) { $0 + $1.size }
        let skipped = input.count - retained.count

        let expected = [
            Tile(shape: "circle", size: 12),
            Tile(shape: "square", size: 8),
            Tile(shape: "circle", size: 20)
        ]
        precondition(retained == expected)
        precondition(total == 40)
        precondition(skipped == 2)

        let empty: [Tile] = []
        precondition(uniqueFirst(empty, by: { $0 }).isEmpty)
        precondition(
            uniqueFirst([input[0], input[0]], by: { $0 })
                == [input[0]]
        )

        let byShape = uniqueFirst(input, by: { $0.shape })
        precondition(byShape == [input[0], input[1]])
        let shapeTotal = byShape.reduce(0) { $0 + $1.size }
        precondition(shapeTotal == 20)

        for tile in retained {
            print("\(tile.shape): \(tile.size)")
        }
        print("Total: \(total)")
        print("Skipped duplicates: \(skipped)")
        print("Shape-only total: \(shapeTotal)")
    }
}

在套件目錄中執行:

swift run OrderDemo

預期的程式輸出如下,前面可能會先顯示建置訊息。本研究環境沒有 Swift 執行檔,因此本文只依據取得的 API 文件檢視程式,並未在此執行。

circle: 12
square: 8
circle: 20
Total: 40
Skipped duplicates: 2
Shape-only total: 20

陣列加集合的方法為何能保留順序?

陣列是輸出順序的唯一來源。迴圈會依序訪問每個輸入位置,而 append(_:) 會將已接受的值附加到結果末尾。集合只負責判斷選取的鍵是否已出現;程式不會走訪集合。

關鍵運算式是 seen.insert(key(value)).inserted。Swift 文件指出,對新成員呼叫 insert(_:) 時會傳回 inserted 作為 true;如果已有相等的成員,則會傳回 false。集合中原有的成員會維持不變。這個傳回值可讓單一操作決定是否附加原始值。請參閱 Apple 的集合插入文件。

對於提供的輸入,第一個圓形和正方形會被接受。第二個完全相同的圓形會被跳過。較大的圓形因完整值不同而被接受,重複的正方形則會被跳過。因此,保留的大小是 12 + 8 + 20 = 40。

此實作維持一項實用的不變條件:每次迭代後,結果都包含 seen 中每個鍵最早遇到的值,且依遇到的順序排列。新鍵會同時擴充兩個集合;重複鍵則不會改變任何一個集合。這項推理能說明順序保證,不受集合儲存成員的方式影響。

自訂 Swift 值應如何表示相等性?

宣告 struct Tile: Hashable 表示要求合成遵循。兩個儲存屬性 String 和 Int 都可雜湊,因此 Swift 能合成所需行為。Apple 在 Hashable 文件中說明了這些合成條件。

以 { $0 } 作為鍵時,整個 Tile 值都會決定唯一性。使用 { $0.shape } 則會將規則改成每種形狀只保留一個項目。在範例中,這會移除後續那個大小為 20 的圓形,讓總和降至 20。這是在比較兩種不同規則,而不是用第二種方式實作同一項規則。

在計算期間,請讓鍵的選取方式保持確定。應從正在處理的值推導鍵,而不是使用計數器或會變動的外部變數。否則,相等的輸入可能會得到不同的鍵,並以不同項目的形式保留下來。

如果你手動實作相等性和雜湊,兩者應使用相同的必要屬性。Apple 要求相等的實例以相同順序將相同值提供給雜湊器。避免直接使用 hashValue 作為唯一性鍵:雜湊值不能取代值本身的相等性規則。保留實際可雜湊的鍵,集合才能套用該規則。請參閱 Hashable 的遵循要求。

何時應改用有序集合?

對於這項單次計算,陣列搭配本機集合能讓行為一目了然。如果應用程式會反覆新增、移除、重新排序並查詢唯一成員,專用的有序集合能讓這些操作更容易維護。

選擇 適用情況 相關限制
陣列加集合 單次走訪合成 Swift 值,也可能使用選定的鍵 函式會定義首次出現項目的處理方式
Swift Collections OrderedSet 持續使用只包含可雜湊唯一值的有序集合 需要另外加入 Swift Collections 套件
Foundation NSOrderedSet API 已使用 Foundation 的有序物件集合 表示靜態有序集合的類別;可變操作使用 NSMutableOrderedSet

Swift Collections 官方文件將 OrderedSet 說明為只含唯一元素的有序集合,透過 OrderedCollections 匯入。它提供 append(_:) 和 insert(_:at:) 等可依位置操作的 API。這些都是套件 API,因此只加入 import OrderedCollections 並不會設定好原本不含相依項目的專案。請參閱 OrderedSet。

Apple 的 Foundation 文件將 NSOrderedSet 說明為由不同物件構成的靜態有序集合,並指出 NSMutableOrderedSet 是其可變版本。對於上述原生 Swift 值計算,泛型函式能直接表達所選的相等性鍵,無須引入該物件集合。請參閱 NSOrderedSet。

計算結果要供桌面介面使用時,會有哪些變化?

完整範例是同步執行的,並在函式內管理暫時集合。對於小型合成清單,這能提供清楚的處理順序,而不必引入工作項目。

在 macOS 介面中,請適當隔離面向 UI 的狀態和更新。Apple 將 MainActor 說明為全域 actor,其執行器等同於主派送佇列;Swift 並行處理指南說明了如何對存取介面狀態的函式和型別套用 @MainActor。請參閱 MainActor 和並行處理。

並行處理也需要明確定義順序。Swift 不保證任務群組中的子任務依何種順序完成。因此,任務完成時才附加結果,保留的是完成順序,可能不同於原始輸入順序。官方並行處理指南說明了這項限制。

如果未來版本會同時執行彼此獨立的合成計算,請讓每個輸入位置隨結果一併傳遞、收集後按輸入位置恢復順序,再執行唯一性處理。這是根據文件說明的完成順序限制推導出的設計。除非有額外工作值得進行更多協調,否則請讓目前的小型範例維持同步執行。

重複使用函式前,先檢查規則

程式包含精確保留順序、空輸入、完全相同的重複值,以及只按形狀判定的替代規則等檢查。這些檢查針對最可能改變計算意義的決定。

調整範例時,請檢查三項條件:輸入本身必須有意義的順序,鍵必須符合你預期的相等性規則,保留首次出現項目的做法也必須適用。此函式會刻意跳過之後符合條件的項目;它不會合併這些項目的大小,也不會取代保留的值。回報的跳過數量能讓這項效果清楚可見。

對於提供的合成形狀清單,完成後會得到依首次出現順序排列的三個值,總和為 40。讓陣列負責順序、集合負責成員判定,並由選定的鍵決定「唯一」的意思。

來源