1. Коли потрібні mapValues, merge і grouping
Іноді здається, що цикл — це універсальна викрутка: ним можна закрутити будь-який шуруп — і стіл, і стілець, а іноді й власну психіку. Але щойно ви 3–4 рази поспіль пишете «створити новий словник, пройтися старим, щось перетворити», мозок починає підозрювати, що це не творчість, а копіпаста у ввічливій формі.
Стандартна бібліотека Swift містить саме ті методи, які зводять типові шаблони роботи зі словником до зручного API. Наприклад, mapValues уміє: «перетвори значення, зберігши ключі», merging/merge — «злий два словники за правилом розв’язання конфліктів ключів», а Dictionary(grouping:by:) — «візьми масив і згрупуй елементи за обчислюваним ключем». Ці операції з’явилися як частина вдосконалення Dictionary і стали окремими важливими можливостями словника.
Щоб не загубитися, давайте одразу зафіксуємо мінітаблицю «що до чого»:
| Операція | Звідки → куди | Основна ідея | Що робить блок { ... } |
|---|---|---|---|
|
Dictionary → новий Dictionary | ключі ті самі, значення змінюються | «як перетворити Value на інше значення» |
|
Dictionary + Dictionary → новий Dictionary | об’єднуємо, а конфлікт ключів розв’язуємо правилом | «що робити, якщо є old і new» |
|
Dictionary + Dictionary → той самий Dictionary | те саме, але змінюємо поточний словник | «що робити за конфлікту» |
|
Array → Dictionary<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
Тут корисно один раз вивчити не на зубок, а «на сенс»:
| Метод | Чи змінює вихідний словник? | Чи повертає значення? | Коли зручно |
|---|---|---|---|
|
ні | новий словник | коли потрібен результат як окреме значення, без побічних ефектів |
|
так (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 ?? "?" і потім вирішувати, що робити з групою "?".
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ