1. Когда нужны mapValues, merge и grouping
Иногда кажется, что цикл — это универсальная отвёртка: им можно закрутить любой шуруп (и стол, и стул, и иногда собственную психику). Но как только вы 3–4 раза подряд пишете «создать новый словарь, пройти по старому, что-то преобразовать», мозг начинает подозревать, что это не творчество, а копипаста вежливой формы.
Стандартная библиотека Swift как раз содержит методы, которые упаковывают типовые «паттерны» работы со словарём. Например, 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 + rule 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 ?? "?" и потом уже решать, что делать с группой "?".
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ