この記事の要点
- Swift 4.0 では、
DictionaryとSetに多くのメソッドやイニシャライザが追加され、グルーピング・フィルタリング・値の変換といったよくある処理を 1 ステップで書けるようになりました。 - 値をキーごとにまとめる
Dictionary(grouping:by:)、値だけを変換するmapValues(_:)、キーと値のペアから辞書を作る 2 種類のイニシャライザ、辞書を返すfilter(_:)、デフォルト値付きの subscript、辞書同士を統合するmerge/mergingなどが代表的な追加です。 Setにも、配列ではなく同じ型のSetを返すfilter(_:)が追加されました。SetとDictionaryの両方に、現在のcapacityの参照とreserveCapacity(_:)も加わりました。- これらの多く(独自の
keys/valuesコレクションを除く)は Swift 3.2 でも利用できます。
以下では、買い物データを題材に各機能を見ていきます。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コレクションを持つようになりました。keysは高速なキー検索を保ったまま、ミュータブルなvaluesは値をその場で変更できます。 Setにも、配列ではなく同じ型のSetを返すfilter(_:)が追加されました。SetとDictionaryの両方に、現在のcapacityの参照とreserveCapacity(_:)が追加され、内部ストレージのサイズを確認・制御できるようになりました。
独自の keys / values コレクションを除き、これらの変更は Swift 3.2 でも利用できます。Swift 4.0 に切り替えていなくても活用できます。
関連リンク
- SE-0154(辞書のkeys/valuesコレクション) — 独自の
keys/valuesコレクションを導入する Proposal - SE-0165(辞書とセットの機能強化) — グルーピング・
mapValues・デフォルト値付き subscript・mergeなどを導入する Proposal - Swift 4.0 リリース — これらの強化を含む Swift 4.0 の公式リリース告知