JavaRush /Курси /Swift SELF /Корисні операції з Dictionary

Корисні операції з Dictionary

Swift SELF
Рівень 13 , Лекція 5
Відкрита

1. Коли потрібні mapValues, merge і grouping

Іноді здається, що цикл — це універсальна викрутка: ним можна закрутити будь-який шуруп — і стіл, і стілець, а іноді й власну психіку. Але щойно ви 3–4 рази поспіль пишете «створити новий словник, пройтися старим, щось перетворити», мозок починає підозрювати, що це не творчість, а копіпаста у ввічливій формі.

Стандартна бібліотека Swift містить саме ті методи, які зводять типові шаблони роботи зі словником до зручного API. Наприклад, mapValues уміє: «перетвори значення, зберігши ключі», merging/merge — «злий два словники за правилом розв’язання конфліктів ключів», а Dictionary(grouping:by:) — «візьми масив і згрупуй елементи за обчислюваним ключем». Ці операції з’явилися як частина вдосконалення Dictionary і стали окремими важливими можливостями словника.

Щоб не загубитися, давайте одразу зафіксуємо мінітаблицю «що до чого»:

Операція Звідки → куди Основна ідея Що робить блок { ... }
mapValues
Dictionary → новий Dictionary ключі ті самі, значення змінюються «як перетворити Value на інше значення»
merging
Dictionary + Dictionary → новий Dictionary об’єднуємо, а конфлікт ключів розв’язуємо правилом «що робити, якщо є old і new»
merge
Dictionary + Dictionary → той самий Dictionary те саме, але змінюємо поточний словник «що робити за конфлікту»
Dictionary(grouping:by:)
ArrayDictionary<Key, [Element]> будуємо групи за ключем «яким буде ключ групи для елемента»

Блок { ... } як правило, а не як «страшна магія»

З цього місця важливо зняти напруження. Повноцінні замикання (closures), їхній синтаксис, скорочення та стиль ми розбиратимемо пізніше — окремо. Сьогодні наша мета скромніша й практичніша: вміти читати такі API і розуміти їхній зміст, навіть якщо ви поки не почуваєтеся впевнено в темі замикань.

Думайте про блок у фігурних дужках як про маленьке правило, яке метод застосовує всередині себе багато разів. Метод ніби каже: «Я сам розберуся з обходом даних, але дайте мені правило: що робити з кожним елементом». Тому сьогодні ми писатимемо блоки максимально простими — у 1–2 рядки — і завжди зі зрозумілими іменами параметрів (жодних $0, жодних «здогадайтеся, що я мав на увазі»).

Є три форми, які вам достатньо впевнено впізнавати на око:

let a = 10
let s = String(a) // тут правило міститься в String(...)
print(s)          // 10

Це приклад без блоку — просто щоб нагадати: «правило перетворення» може бути і функцією, і конструктором, і чим завгодно.

А ось «правило в блоці» для одного параметра:

let numbers = [1, 2, 3]
let strings = numbers.map { number in
    "n=\(number)"	// нове значення: number → "n=\(number)"
}
print(strings) // ["n=1", "n=2", "n=3"]

І «правило для конфлікту», де параметрів два:

let old = 5
let new = 2
let combined = old + new
print(combined) // 7

У merging і merge ви буквально описуєте, яким буде combined, коли зустрічаються old і new.

2. mapValues: змінюємо значення, ключі залишаються незмінними

Коли ви вперше чуєте mapValues, хочеться спитати: «Це що, map, але для словника?» Майже. Важливий нюанс у тому, що ключі залишаються тими самими, а перетворюються лише значення. У результаті ви отримуєте новий словник із тим самим набором ключів.

Чому це взагалі винесли в окремий метод? Тому що така операція дуже часта: у вас є словник «ключ → дані», і ви хочете «ключ → інша форма даних». Наприклад, «слово → кількість» можна перетворити на «слово → зручний підпис для друку», а «категорія → список» — на «категорія → розмір списку». У Swift це саме mapValues.

Мініприклад: робимо «ярлики» для друку

Уявімо, що в нас є частотна карта слів:

let counts: [String: Int] = ["swift": 3, "code": 1]

let labels = counts.mapValues { count in
    "x\(count)"
}

print(labels) // ["swift": "x3", "code": "x1"]

Ключі "swift" і "code" не змінювалися, а ось значення Int перетворилися на String.

Типовий випадок: «список → кількість»

Дуже поширене завдання: є словник «група → елементи групи», а ви хочете «група → скільки елементів». Це саме те, що в статтях про вдосконалення Dictionary показують як приклад mapValues: групуємо, а потім перетворюємо значення (масиви) на їхні розміри.

let byFirstLetter: [Character: [String]] = [
    "s": ["swift", "stack"],
    "c": ["code"]
]

let sizes = byFirstLetter.mapValues { words in
    words.count
}

print(sizes) // ["s": 2, "c": 1]

Так, це можна зробити циклом. Але mapValues робить намір очевиднішим: «я не перебудовую словник, я перетворюю значення».

Важливо: результат — новий словник

mapValues повертає абсолютно новий Dictionary. Тобто вихідний лишається таким, яким був. Це особливо зручно, коли ви хочете спершу отримати «сирий словник», а потім кілька різних представлень для різних задач: друку, статистики, перевірок.

3. merging і merge: об’єднуємо словники та розв’язуємо конфлікти

Об’єднання словників — це як об’єднання двох списків покупок від різних членів сім’ї. Якщо обидва написали «молоко», треба вирішити: беремо більше молока, залишаємо старе, беремо нове чи взагалі сваримося й ідемо в магазин окремо.

У Swift для цього є пара методів: merging (не змінює вихідний словник, а повертає новий) і merge (змінює поточний словник «на місці»). Обидва вимагають правила для конфліктних ключів через параметр uniquingKeysWith.

merging: «дай мені новий словник»

Ось класичний приклад для частотних карт: хочемо скласти лічильники.

let a: [String: Int] = ["swift": 2, "java": 1]
let b: [String: Int] = ["swift": 3, "kotlin": 4]

let total = a.merging(b, uniquingKeysWith: { old, new in
    old + new
})

print(total) // ["swift": 5, "java": 1, "kotlin": 4]

Правило читається буквально: «якщо ключ збігся, у мене є значення old (те, що було в першому словнику) і значення new (те, що прийшло з другого), склади їх». Саме така ідея — «правило об’єднання за збігу ключів» — і є суттю API merging/merge.

merge: «зміни поточний словник»

Іноді вам зручно зберігати один «накопичувальний» словник і поступово вливати в нього нові дані. Тоді потрібен merge:

var base: [String: Int] = ["a": 1, "b": 2]
let incoming: [String: Int] = ["b": 10, "c": 3]

base.merge(incoming, uniquingKeysWith: { old, new in // значення з обох словників
    old
})

print(base) // ["a": 1, "b": 2, "c": 3]

Ми вибрали правило «залишити старе значення» (old). Це теж частий варіант: наприклад, «налаштування за замовчуванням» вливаємо в «налаштування користувача», але користувацькі не чіпаємо.

Різниця між merge і merging

Тут корисно один раз вивчити не на зубок, а «на сенс»:

Метод Чи змінює вихідний словник? Чи повертає значення? Коли зручно
merging
ні новий словник коли потрібен результат як окреме значення, без побічних ефектів
merge
так (mutating) нічого важливого (по суті Void) коли у вас «накопичувач» і ви оновлюєте його під час роботи застосунку

4. Dictionary(grouping:by:): перетворюємо масив на словник груп

Якщо Dictionary — це «телефонна книга» (ключ → дані), то grouping — це «розкласти людей по кімнатах за якоюсь ознакою». У вас є масив елементів, ви кажете: «Ключ групи — ось такий», і отримуєте словник, де значення — масив елементів цієї групи.

У Swift це ініціалізатор Dictionary(grouping:by:): він бере послідовність значень і функцію або правило, яке для кожного елемента обчислює ключ групи. У результаті отримуємо Dictionary<Key, [Element]>.

Приклад: групуємо слова за довжиною

let words = ["swift", "go", "rust", "js", "java"]

let byLength = Dictionary(grouping: words, by: { word in
    word.count // згрупувати слова за кількістю букв
})

print(byLength) // наприклад: [2: ["go", "js"], 4: ["rust", "java"], 5: ["swift"]]

Порядок ключів під час друку словника не гарантується, тож конкретний порядок у виводі може відрізнятися. Але зміст лишається незмінним: ключ — довжина, значення — масив слів із такою довжиною.

Приклад: групуємо за першою літерою

Перша літера рядка — річ трохи підступна, бо рядок може бути порожнім. Нам потрібен безпечний підхід. Для простоти візьмемо рядки, які точно не порожні, або додамо символ за замовчуванням:

let names = ["Ann", "Bob", "Alice", ""]

let byFirst = Dictionary(grouping: names, by: { name in
    name.first ?? "?"
})

print(byFirst) // наприклад: ["A": ["Ann", "Alice"], "B": ["Bob"], "?": [""]]

Зверніть увагу на стиль: ми не використовуємо !, бо порожній рядок — цілком реальна ситуація під час введення користувача.

Зв’язка grouping + mapValues

Ось тут починається справжня «потужність без ускладнення»: спершу ми групуємо, а потім перетворюємо групи на лічильники через mapValues. Це саме той двоетапний прийом, який робить код читабельним: спочатку «розклали», потім «порахували».

let words = ["swift", "go", "rust", "js", "java"]

let grouped = Dictionary(grouping: words, by: { word in word.count })
let counts = grouped.mapValues { group in group.count }

print(counts) // наприклад: [2: 2, 4: 2, 5: 1]

Такий підхід розглядають як одну з мотивацій появи цих API: замість ручного словника і перевірок «є ключ чи ні» ви формулюєте задачу більш декларативно.

5. Мініпроєкт: «TextStats»

Тепер зберемо все в один маленький практичний сценарій, який можна запускати навіть у Web IDE: застосунок читає два рядки тексту, будує частотну карту для кожного рядка, потім об’єднує їх, а наприкінці формує кілька «звітних» представлень через mapValues і grouping.

Важливо: ми вже знаємо split, lowercased(), trimmingCharacters(in:), Dictionary і частотні карти. Новими сьогодні є лише три операції: mapValues, merging/merge і grouping.

Крок 1: нормалізація токенів

Нам хочеться, щоб Swift, swift і swift вважалися одним словом. Для цього заведемо маленьку функцію. Так, функції в нас були раніше в курсі — скористаємося ними, щоб код не перетворювався на простирадло.

import Foundation

func normalize(_ token: Substring) -> String {
    String(token)
        .trimmingCharacters(in: .whitespacesAndNewlines)
        .lowercased()
}

Крок 2: рахуємо частоти в одному рядку

Використовуємо вже знайомий патерн freq[word, default: 0] += 1.

import Foundation

func countWords(in line: String) -> [String: Int] {
    var freq: [String: Int] = [:]
    for raw in line.split(separator: " ") {
        let word = normalize(raw)
        if !word.isEmpty { freq[word, default: 0] += 1 }
    }
    return freq
}

Крок 3: об’єднуємо дві частотні карти через merging

Ось тут з’являється наша сьогоднішня зірка: merging.

let a = ["swift": 2, "code": 1]
let b = ["swift": 1, "cli": 3]

let total = a.merging(b, uniquingKeysWith: { old, new in
    old + new
})

print(total) // ["swift": 3, "code": 1, "cli": 3]

Сенс «скласти лічильники за збігу ключів» — один із найчастіших випадків для merging.

Крок 4: робимо «гарний друк» через mapValues

Іноді вам хочеться не просто числа, а підказки для користувача.

let counts: [String: Int] = ["swift": 3, "cli": 1]

let pretty = counts.mapValues { count in
    "зустрічається \(count) раз(ів)"
}

print(pretty["swift"] ?? "—") // зустрічається 3 раз(ів)

Це той випадок, коли цикл for теж можливий, але mapValues робить намір прямим: «ключі не чіпаю, змінюю значення».

Крок 5: групуємо слова за довжиною і рахуємо розміри груп

Ми вже знаємо, як групувати. Далі застосовуємо mapValues, щоб отримати лічильник розмірів груп.

let words = ["swift", "go", "rust", "java", "js"]

let groups = Dictionary(grouping: words, by: { word in word.count })
let lengths = groups.mapValues { group in group.count }

print(lengths) // наприклад: [2: 2, 4: 1, 5: 1, 6: 1]

Це саме той «комбінований прийом», заради якого й зручно знати обидві операції.

Схема потоку даних

Щоб закріпити в голові не рядки коду, а потік даних, корисно побачити це як просту блок-схему:

flowchart TD
    A[Вхід: два рядки тексту] --> B[split + normalize]
    B --> C1[Частотна карта №1]
    B --> C2[Частотна карта №2]
    C1 --> D[merging + правило old + new]
    C2 --> D
    D --> E[Підсумкові частоти]
    E --> F["mapValues: красиві підписи"]
    E --> G[grouping: за довжиною або першою літерою]
    G --> H[mapValues: розмір груп]

6. Типові помилки

Помилка №1: плутати mapValues з перетворенням ключів.
Часто рука тягнеться «поміняти ключі місцями» або «зробити ключі lowercase». mapValues так не вміє за означенням: він зберігає ключі й змінює лише значення. Якщо вам потрібно змінювати ключі, це інша задача і зазвичай розв’язується ручним збиранням нового словника.

Помилка №2: писати занадто довге правило в { ... } і втрачати читабельність.
Коли блок розростається до 15 рядків із вкладеними if і print("debug"), API перестає бути «красивим скороченням» і перетворюється на шифр. Сьогоднішнє правило доброго тону просте: один блок — одна думка. Якщо думок більше, краще винести їх у функцію (наприклад, func combine(old:new:) -> Int) і викликати її всередині блоку.

Помилка №3: неправильно розуміти, хто такий old, а хто такий new у merging/merge.
У конфлікті ключів old — це значення, яке вже було в словнику (у першому словнику або в base при merge), а new — те, що прийшло з «доданого» словника. Якщо їх переплутати, можна отримати дуже дивні наслідки, особливо коли правило — «залишити старе» або «перезаписати новим».

Помилка №4: чекати від Dictionary(grouping:by:) готових лічильників.
grouping не рахує. Він групує і повертає масиви. Якщо вам потрібен лічильник, зробіть другий крок mapValues { group in group.count }. Це нормально: «спочатку розкласти по коробках, потім порахувати предмети в кожній коробці».

Помилка №5: використовувати ! під час обчислення ключа групи для потенційно порожніх рядків.
Спокуса написати name.first! велика, бо це «коротше». Але порожній рядок у введенні користувача — це не фантастика, а статистично неминуча реальність. Набагато спокійніше використовувати name.first ?? "?" і потім вирішувати, що робити з групою "?".

1
Опитування
Словники Swift, рівень 13, лекція 5
Недоступний
Словники Swift
Основи роботи з Dictionary
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ