$ sudo teach IT

Зачем нужен словарь

Представьте телефонную книгу: вы не перебираете все записи подряд, а сразу ищете человека по имени и получаете его номер. Словарь в Swift работает так же — это набор пар «ключ-значение», где по ключу можно почти мгновенно найти связанное с ним значение, не перебирая всю коллекцию циклом, как пришлось бы делать с массивом.

Официально такая коллекция называется Dictionary. Полная запись типа выглядит как Dictionary<Key, Value>, но на практике почти всегда используют короткую форму [Key: Value] — например, [String: Int] для словаря, где ключи это строки, а значения целые числа.

Создание словаря

Словарь можно создать литералом — списком пар «ключ: значение» в квадратных скобках:

var ages: [String: Int] = ["Аня": 20, "Борис": 25]
print(ages)

Если значения в литерале уже расставляют тип однозначно, аннотацию [String: Int] можно не писать — компилятор выведет тип сам. А вот для пустого словаря вывести тип не из чего, поэтому аннотация обязательна:

var scores: [String: Int] = [:]
// без аннотации компилятор не поймёт, какого типа будут ключи и значения

Обратите внимание на [:] — это литерал именно пустого словаря, а не пустого массива ([]): двоеточие внутри скобок отличает одно от другого.

Доступ по ключу — это опционал

Здесь у словаря есть особенность, из-за которой новички часто спотыкаются. Когда вы обращаетесь к словарю по ключу через сабскрипт словарь[ключ], в ответ приходит не сам Value, а Value? — опционал:

let ages: [String: Int] = ["Аня": 20, "Борис": 25]
let age = ages["Аня"]
print(age)
// напечатает Optional(20)

let missing = ages["Виктор"]
print(missing)
// напечатает nil — такого ключа в словаре нет

Это не неудобство компилятора, а осознанная защита: словарь физически не может знать заранее, есть ли у него нужный ключ, пока вы его не спросите. Если бы обращение по ключу сразу отдавало Value, программе пришлось бы либо падать при отсутствующем ключе, либо втихую подставлять какое-то случайное значение — оба варианта хуже честного nil. Опционал заставляет вас явно решить, что делать, если ключа не окажется, — точно так же, как вы уже поступали с опционалами раньше.

Значение по умолчанию через ??

Самый частый способ обработать отсутствующий ключ — оператор ??, который вы уже знаете: если слева nil, берётся значение справа.

let ages: [String: Int] = ["Аня": 20]
let age = ages["Виктор"] ?? 0
print(age)
// напечатает 0, потому что ключа "Виктор" нет

Сабскрипт со значением по умолчанию

Есть более короткая форма того же самого — сабскрипт с параметром default:

let ages: [String: Int] = ["Аня": 20]
let age = ages["Виктор", default: 0]
print(age)
// напечатает 0, точно так же, как и с ??

Разница раскрывается, когда такой сабскрипт стоит слева от присваивания с изменяющим оператором: словарь[ключ, default: 0] += 1 одной строкой и увеличивает существующее значение, и заводит новую запись с единицы, если ключа ещё не было. Ниже вы увидите, почему это особенно удобно.

Добавление и изменение значений

Присваивание по ключу работает и для новой записи, и для замены существующей — словарь сам разбирается, что нужно:

var ages: [String: Int] = ["Аня": 20]
ages["Борис"] = 25
// добавили новую пару, ключа "Борис" раньше не было
ages["Аня"] = 21
// заменили значение по существующему ключу
print(ages)

Есть и метод updateValue(_:forKey:), который делает то же самое, но дополнительно возвращает старое значение — тоже в виде опционала, потому что если ключа не было, возвращать нечего:

var ages: [String: Int] = ["Аня": 20]
let old = ages.updateValue(21, forKey: "Аня")
print(old)
// напечатает Optional(20) — прежнее значение до обновления

let oldMissing = ages.updateValue(30, forKey: "Виктор")
print(oldMissing)
// напечатает nil — ключа раньше не было, обновлять было нечего

Удаление записи

Удалить пару можно методом removeValue(forKey:) — он тоже возвращает удалённое значение как опционал, или присваиванием nil по ключу, что делает то же самое короче:

var ages: [String: Int] = ["Аня": 20, "Борис": 25]
let removed = ages.removeValue(forKey: "Борис")
print(removed)
// напечатает Optional(25)

ages["Аня"] = nil
// то же самое действие другой записью: удалили ключ "Аня"
print(ages)
// напечатает [:]

Размер, ключи и значения

У словаря, как и у других коллекций, есть count и isEmpty. Отдельно можно получить все ключи через keys и все значения через values:

let ages: [String: Int] = ["Аня": 20, "Борис": 25]
print(ages.count)
// 2
print(ages.isEmpty)
// false
for name in ages.keys {
    print(name)
}
// Аня и Борис, но в каком порядке — заранее не известно

Здесь важный момент: порядок пар в словаре не определён. Он не совпадает ни с порядком добавления, ни с алфавитным порядком, и может отличаться при каждом запуске программы. Если результат нужно предъявить в конкретном порядке — например, отсортированным по алфавиту, — ключи или значения нужно явно сортировать, но это уже тема одного из следующих уроков.

Обход словаря

Перебрать все пары сразу удобнее всего циклом for-in, где каждая пара приходит как кортеж «ключ, значение» — а кортежи вы уже умеете деструктурировать:

let ages: [String: Int] = ["Аня": 20, "Борис": 25]
for (name, age) in ages {
    print("\(name): \(age)")
}
// Аня: 20 и Борис: 25 — но порядок пар не гарантирован

Требование Hashable к ключу

Ключом словаря может быть не любой тип, а только тот, что соответствует протоколу Hashable — словарь внутри устроен так, что ему нужно быстро вычислить по значению ключа число (хеш) и по нему найти нужную ячейку. String, Int, Character и Bool уже соответствуют Hashable «из коробки», так что на практике в качестве ключа вы почти всегда используете что-то из стандартных типов и просто не задумываетесь об этом требовании.

Приём: подсчёт частоты через словарь

Одна из самых частых задач на словари — посчитать, сколько раз встречается каждый элемент в коллекции: слова в тексте, символы в строке, покупки в списке. Идея простая: заводим пустой словарь [Элемент: Int], идём циклом по коллекции и на каждом элементе увеличиваем счётчик — как раз тот случай, где сабскрипт с default экономит строку кода:

let letters = "пример"
var frequency: [Character: Int] = [:]
for letter in letters {
    frequency[letter, default: 0] += 1
}
print(frequency["р"] ?? 0)
// 2 — буква "р" встречается в слове дважды

Без default: 0 пришлось бы отдельно проверять, есть ли уже такой ключ, и заводить его вручную при первой встрече — тот же результат, но заметно длиннее.

Частые ошибки

Забыть, что словарь[ключ] — это опционал, и попытаться использовать результат напрямую как число или строку. Компилятор такого не пропустит и потребует явно развернуть значение или подставить значение по умолчанию.

Перепутать пустой словарь и пустой массив: [:] — словарь, [] — массив. Без аннотации типа компилятор не всегда способен догадаться, что вы имели в виду именно словарь.

Полагаться на порядок пар при переборе или при печати словаря — он не гарантирован и может меняться от запуска к запуску, даже если содержимое словаря не менялось.

Считать, что updateValue(_:forKey:) возвращает новое значение — на самом деле он возвращает старое, то, что было в словаре до вызова.

Итоги

  • Словарь [Key: Value] хранит пары «ключ-значение» и находит значение по ключу быстро, без перебора
  • Обращение по ключу через сабскрипт всегда возвращает Value?, потому что ключа может не быть — это защита, а не неудобство
  • ?? значение и сабскрипт с default: дают удобное значение по умолчанию вместо разворачивания опционала
  • Присваивание по ключу добавляет новую пару или заменяет существующую; updateValue(_:forKey:) делает то же самое и возвращает старое значение
  • removeValue(forKey:) и присваивание nil по ключу удаляют запись
  • count, isEmpty, keys и values дают размер и отдельные ключи или значения; порядок пар не определён
  • Обход for (ключ, значение) in словарь перебирает все пары как кортежи
  • Ключом может быть только тип, соответствующий Hashable — у стандартных типов вроде String и Int оно уже есть
  • Словарь со значением по умолчанию через сабскрипт — удобный инструмент для подсчёта частоты элементов

Проверьте себя

4 вопроса

Частота слов

Напишите функцию wordFrequency(_:), которая принимает строку text и возвращает словарь [String: Int] — сколько раз встречается каждое слово.

Слова в строке разделены одиночным пробелом. Разбейте строку на слова методом split(separator:), а для подсчёта используйте сабскрипт словаря со значением по умолчанию: result[слово, default: 0] += 1. Для пустой строки верните пустой словарь.

Ключ с максимальным значением

Напишите функцию keyWithMaxValue(_:), которая принимает словарь [String: Int] и возвращает ключ с наибольшим значением. Так как словарь может оказаться пустым, а найти максимум тогда не из чего, функция возвращает String?: для пустого словаря верните nil.

Гарантируется, что в тестах наибольшее значение всегда единственное, без повторов — вам не нужно решать, какой ключ выбрать при равенстве.

Объединение словарей

Напишите функцию mergeCounts(_:_:), которая принимает два словаря [String: Int] и объединяет их в один: если ключ встречается только в одном из словарей, его значение переходит в результат как есть; если ключ есть в обоих — значения складываются.

Используйте сабскрипт словаря со значением по умолчанию, чтобы не проверять наличие ключа вручную.