Зачем нужен словарь
Представьте телефонную книгу: вы не перебираете все записи подряд, а сразу ищете человека по имени и получаете его номер. Словарь в 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] и объединяет их в один: если ключ встречается только в одном из словарей, его значение переходит в результат как есть; если ключ есть в обоих — значения складываются.
Используйте сабскрипт словаря со значением по умолчанию, чтобы не проверять наличие ключа вручную.