JavaRush /Курсы /Swift SELF /Полезные операции Dictionary

Полезные операции Dictionary

Swift SELF
13 уровень , 5 лекция
Открыта

1. Когда нужны mapValues, merge и grouping

Иногда кажется, что цикл — это универсальная отвёртка: им можно закрутить любой шуруп (и стол, и стул, и иногда собственную психику). Но как только вы 3–4 раза подряд пишете «создать новый словарь, пройти по старому, что-то преобразовать», мозг начинает подозревать, что это не творчество, а копипаста вежливой формы.

Стандартная библиотека Swift как раз содержит методы, которые упаковывают типовые «паттерны» работы со словарём. Например, 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 + 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 ?? "?" и потом уже решать, что делать с группой "?".

1
Задача
Swift SELF, 13 уровень, 5 лекция
Недоступна
Ценник витрины
Ценник витрины
1
Задача
Swift SELF, 13 уровень, 5 лекция
Недоступна
Итог матчей
Итог матчей
1
Задача
Swift SELF, 13 уровень, 5 лекция
Недоступна
Длины слов
Длины слов
1
Задача
Swift SELF, 13 уровень, 5 лекция
Недоступна
Два дня трат
Два дня трат
1
Опрос
Словари Swift, 13 уровень, 5 лекция
Недоступен
Словари Swift
Основы работы с Dictionary
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ