Swift Digest
Blog | Swift.org Blog

Swift 4.0 における Dictionary と Set の改善

Dictionary and Set Improvements in Swift 4.0

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

この記事の要点

以下では、買い物データを題材に各機能を見ていきます。GroceryItem は名前と売り場(Department)を持つ型で、商品の配列 groceries を起点にします。

struct GroceryItem: Hashable {
    var name: String
    var department: Department

    enum Department {
        case bakery, produce, seafood
    }
}

let apple = GroceryItem(name: "Apples", department: .produce)
let banana = GroceryItem(name: "Bananas", department: .produce)
let croissant = GroceryItem(name: "Croissants", department: .bakery)
let salmon = GroceryItem(name: "Salmon", department: .seafood)

let groceries = [apple, banana, croissant, salmon]

キーごとに値をまとめる

Dictionary(grouping:by:) イニシャライザを使うと、シーケンスの各要素から計算したキーごとに値をまとめた辞書を作れます。

Swift 3.1 以前は、空の辞書から手作業で組み立てる必要がありました。型注釈、ループ、キーが既に存在するかのチェックが必要です。

// Swift <= 3.1
var grouped: [GroceryItem.Department: [GroceryItem]] = [:]
for item in groceries {
    if grouped[item.department] != nil {
        grouped[item.department]!.append(item)
    } else {
        grouped[item.department] = [item]
    }
}

Swift 4.0 では、各要素のキーを返すクロージャを渡すだけで、同じ辞書を 1 行で作れます。

// Swift 4.0
let groceriesByDepartment = Dictionary(grouping: groceries,
                                       by: { item in item.department })
// groceriesByDepartment[.bakery] == [croissant]

各キーの値は、その売り場に属する商品の配列で、元のリストと同じ順序になります。

辞書の値を変換する

mapValues(_:) メソッドを使うと、キーはそのままに値だけを変換できます。次のコードは、各売り場の商品配列を「商品数」に変換します。

let departmentCounts = groceriesByDepartment.mapValues { items in items.count }
// departmentCounts[.bakery] == 1

キーが変わらないため、内部レイアウトを再利用でき、ハッシュ値を計算し直す必要もありません。そのため辞書をゼロから作り直すより高速です。

キーと値のペアから辞書を作る

キーと値のペアのシーケンスから辞書を作るイニシャライザが 2 種類追加されました。キーが一意な場合と、キーが重複しうる場合に対応します。

キーのシーケンスと値のシーケンスがある場合は、zip(_:_:) でペアのシーケンスにまとめられます。キーが一意だと確信できる場合は、Dictionary(uniqueKeysWithValues:) を使います。

let zippedNames = zip(groceries.map { $0.name }, groceries)
var groceriesByName = Dictionary(uniqueKeysWithValues: zippedNames)
// groceriesByName["Apples"] == apple
// groceriesByName["Kumquats"] == nil

Dictionary(uniqueKeysWithValues:) はキーが一意な場合にのみ使ってください。シーケンスにキーの重複があると、実行時エラーになります。

キーが重複しうる場合は、Dictionary(_:uniquingKeysWith:) を使います。このイニシャライザは、キーが重複したときに呼ばれるクロージャを受け取ります。クロージャは同じキーを共有する 1 つ目と 2 つ目の値を引数に取り、既存の値・新しい値・両者を組み合わせた値のいずれかを返せます。

let pairs = [("dog", "🐕"), ("cat", "🐱"), ("dog", "🐶"), ("bunny", "🐰")]
let petmoji = Dictionary(pairs,
                         uniquingKeysWith: { (old, new) in new })
// petmoji["cat"] == "🐱"
// petmoji["dog"] == "🐶"

ここではクロージャが常に 2 つ目の引数を返すため、"dog" の値は後から来た "🐶" になります。

条件に合うエントリを選ぶ

辞書の filter(_:) メソッドは、以前のバージョンのように配列ではなく 辞書 を返すようになりました。キーと値のペアを引数に取り、結果に含めたいペアで true を返すクロージャを渡します。

func isOutOfStock(_ item: GroceryItem) -> Bool {
    // 在庫を確認する
}

let outOfStock = groceriesByName.filter { (_, item) in isOutOfStock(item) }
// outOfStock["Croissants"] == croissant
// outOfStock["Apples"] == nil

デフォルト値を使う

辞書に、キーと default パラメータを取る subscript が追加されました。辞書の値はキーで引くと optional になりますが、この subscript ではキーが見つからない場合に指定したデフォルト値が返ります。

// banana は前掲の定義を再利用し、カートで使う商品を追加で定義する
let shrimp = GroceryItem(name: "Shrimp", department: .seafood)
let bread = GroceryItem(name: "Bread", department: .bakery)
let grape = GroceryItem(name: "Grapes", department: .produce)

var cart = [banana: 1]

cart[banana]              // Optional(1)
cart[shrimp]              // nil

cart[banana, default: 0]  // 1
cart[shrimp, default: 0]  // 0

この subscript を通して値を変更することもでき、カートに商品を追加するコードが簡潔になります。

for item in [banana, banana, bread] {
    cart[item, default: 0] += 1
}

banana の処理では現在の値が取り出されて 1 増え、辞書に書き戻されます。bread のようにキーが見つからない場合は、デフォルト値 0 が返り、それを増やした値で新しいキーと値のペアが追加されます。ループ終了後、cart[banana: 3, bread: 1] になります。

2 つの辞書を 1 つに統合する

辞書をまとめて変更する手段として、ある辞書を別の辞書に統合するメソッドが追加されました。

cart の内容に別の辞書を統合するには、mutating な merge(_:uniquingKeysWith:) メソッドを使います。渡す uniquing クロージャは Dictionary(_:uniquingKeysWith:) と同じ働きで、同じキーに 2 つの値があるときに呼ばれ、いずれか、または組み合わせた値を返します。

let otherCart = [banana: 2, grape: 3]
cart.merge(otherCart, uniquingKeysWith: +)
// cart == [banana: 5, grape: 3, bread: 1]

ここでは uniquingKeysWith に加算演算子 + を渡しているため、同じキーの個数どうしが合算されます。統合した結果を新しい辞書として得たい場合は、非 mutating な merging(_:uniquingKeysWith:) メソッドを使います。

その他の追加

独自の keys / values コレクションを除き、これらの変更は Swift 3.2 でも利用できます。Swift 4.0 に切り替えていなくても活用できます。

関連リンク