JavaRush /Курси /Swift SELF /Створення Dictionary, типи ключів, літерали

Створення Dictionary, типи ключів, літерали

Swift SELF
Рівень 13 , Лекція 1
Відкрита

1. Навіщо вам Dictionary

Коли ви лише знайомитеся з колекціями, масив здається універсальним рішенням: розклали елементи один за одним — і живете спокійно. Але в реальному коді швидко постає запитання: «Як мені швидко знайти значення за певною ознакою?». Якщо щоразу переглядати весь масив, код починає нагадувати людину, яка шукає ключі в квартирі й послідовно перевіряє кожну полицю.

Уявіть задачу: у нас є список книг і кількість сторінок, які ми прочитали. У масиві це могло б виглядати як два масиви: назви окремо, сторінки окремо. Або як масив пар. І ось вам потрібно дізнатися, скільки сторінок у книжки "Swift" — доведеться шукати. А Dictionary дає змогу зберігати дані так: ключ → значення, тобто "Swift"120. Тоді пошук зводиться до простого запиту: «дай мені значення за ключем».

Щоб закріпити, порівняймо ідеї в таблиці:

Колекція Що зберігає Як звертаємося Коли зручно
Array
елементи підряд за індексом 0,1,2... коли важливий порядок, потрібні «перші 10» або перебір за порядком
Dictionary
пари ключзначення за ключем (наприклад, рядком) коли потрібен швидкий доступ за імʼям, ID або кодом

Невеликий історичний штрих: Dictionary — один із базових типів стандартної бібліотеки Swift. Згодом навколо нього зʼявилися нові зручні операції: злиття, групування тощо. І це не випадково: у більшості застосунків він справді працює як надійна робоча конячка.

Ментальна модель «ключ → значення»

Перед тим як писати код, корисно домовитися про картинку в голові. Dictionary — це як гардероб у театрі: у вас є номерок (ключ), і за ним ви отримуєте пальто (значення). Вам не важливо, у якому порядку висять пальта і скільки людей було перед вами: номерок → пальто, і крапка.

Найважливіша відмінність від масиву тут у тому, що ключ не обовʼязково має бути числом і взагалі не обовʼязково має бути «позицією». Ключем може бути рядок ("Ann"), число (42), булеве значення (true/false) і ще деякі типи. Але логіка завжди одна: за ключем ми хочемо швидко знайти значення.

Можна навіть намалювати це схемою:

flowchart LR
    A["Ключ: 'swift'"] --> B["Значення: 120"]
    C["Ключ: 'cli'"] --> D["Значення: 30"]
    E["Ключ: 'algorithms'"] --> F["Значення: 10"]

У коді це означатиме, що ми створюємо словник, який уміє відповідати на запитання: «скільки сторінок у книжки з такою назвою?».

Тип словника: [Key: Value]

Зараз буде важливий момент про типізацію. Swift — вимоглива мова: вона любить заздалегідь розуміти, що саме ви зберігаєте. Тому тип словника записується так:

[Key: Value]

Тобто це словник, де ключі мають тип Key, а значення — тип Value.

Наприклад, якщо ми хочемо зберігати вік людини за імʼям:

  • ключ: імʼя (String)
  • значення: вік (Int)

Тоді тип виглядатиме так: [String: Int].

Невеликий приклад, який можна запустити як код верхнього рівня:

import Foundation

var ages: [String: Int] = ["Ann": 24, "Bob": 30]
print(ages) // Порядок може відрізнятися: ["Bob": 30, "Ann": 24]

Зверніть увагу на дві речі. По-перше, ми явно написали тип [String: Int], і тепер словник не дасть випадково записати туди «вік рядком». По-друге, під час print порядок пар може бути не таким, як ви очікували. До цього ми ще повернемося.

Літерал словника: ["a": 1, "b": 2]

Коли ви створюєте масив, ви використовуєте літерал [1, 2, 3]. Зі словником усе схоже, але всередині кожного запису є дві частини: ключ і значення, розділені двокрапкою.

Запис має такий вигляд:

["ключ": значення, "ключ2": значення2]

Ось невеликий приклад каталогу цін:

import Foundation

var prices: [String: Int] = ["coffee": 250, "tea": 180]
print(prices) // Наприклад: ["tea": 180, "coffee": 250]

Важливо звикнути: двокрапка : всередині літерала — це не «тип», а саме роздільник «ключ: значення». Тип ми пишемо зовні як [String: Int].

Порожній словник: [:]

Порожній масив — це []. Логічно було б очікувати, що порожній словник — теж [], але ні: це порожній масив. Для порожнього словника в Swift є спеціальна форма:

[:]

І тут новачків підловлює класична ситуація: якщо написати просто var d = [:], компілятор скаже щось на кшталт «я не розумію, які тут типи ключів і значень». Оскільки [:] не містить жодної пари, вгадувати ні з чого.

Тому порожній словник майже завжди створюють так:

import Foundation

var counters: [String: Int] = [:]
print(counters) // [:]

Альтернативний спосіб — через повну форму типу. Він інколи трапляється в коді й буває корисним, коли ви читаєте чужі проєкти:

import Foundation

var scores = Dictionary<String, Int>()
print(scores) // [:]

Цей варіант довший, але інколи здається наочнішим тим, хто любить, щоб код сам себе пояснював.

var і let: словник можна «заморозити»

Словник — це колекція. А колекції в Swift підкоряються правилам var/let, які ви вже знаєте: якщо колекцію оголошено через let, вона стає незмінною. Це корисно, коли ви хочете захистити дані від випадкових правок, у тому числі й від себе через дві години, коли забудете, що робили.

Порівняймо:

import Foundation

let fixedAges: [String: Int] = ["Ann": 24]
print(fixedAges["Ann"] as Any) // Optional(24)

І змінюваний варіант:

import Foundation

var ages: [String: Int] = [:]
ages["Ann"] = 24
ages["Bob"] = 30
print(ages)

Тут ми використали запис ages["Ann"] = 24. Поки що сприймайте це як «покласти пару ключ → значення». Тонкощі читання, зокрема чому там Optional, ми розберемо в наступній лекції. Тож зараз не лякайтеся виводу Optional(24).

2. Ключі та поведінка словника

Типи ключів і Hashable

Зараз буде момент, де я трохи привідкрию завісу над тим, як це працює в Swift під капотом. Щоб словник міг швидко знаходити значення за ключем, ключ повинен бути спеціального типу: він має підходити для хешування. У Swift це виражається вимогою: ключ має бути Hashable.

Простими словами, ключ має бути таким, щоб Swift умів швидко й стабільно використовувати його як ідентифікатор.

Добра новина: найпотрібніші типи вже Hashable:

  • String
  • Int
  • Bool

Тож ключами спокійно можуть бути рядки й числа.

Приклад: словник «місяць → кількість днів» (поки без календарних тонкощів):

import Foundation

let daysInMonth: [Int: Int] = [1: 31, 2: 28, 3: 31]
print(daysInMonth) // Наприклад: [2: 28, 1: 31, 3: 31]

А ось приклад того, що не може бути ключем (покажу рядком-коментарем, щоб ваш код усе одно компілювався):

import Foundation

// ❌ Масив не може бути ключем, бо він не Hashable
var bad: [[Int]: String] = [:] // error: Type '[Int]' does not conform to protocol 'Hashable'

print("Ключ словника має бути Hashable") // Ключ словника має бути Hashable

І тут важливий практичний висновок: якщо ви намагаєтеся зробити ключем щось складне, компілятор може заборонити це — і найчастіше це на краще. Словник любить прості ключі: рядки, числа, стабільні ідентифікатори.

Ключі унікальні: перезапис значення

Словник відрізняється від масиву ще й тим, що ключі в ньому унікальні. Не можна покласти два значення з одним і тим самим ключем так, щоб вони «жили поруч». Якщо ви записуєте значення за вже наявним ключем, ви замінюєте старе значення.

Це зручно: ми можемо оновлювати запис одним рядком.

import Foundation

var prices: [String: Int] = ["tea": 180]
prices["tea"] = 200

print(prices) // ["tea": 200]

Цю поведінку важливо памʼятати, тому що новачок інколи очікує, що словник зберігає історію. Ні, словник зберігає останнє значення для ключа. Історію ви зберігатимете окремо — зазвичай у масиві, якщо вона взагалі потрібна.

Чому не можна покладатися на порядок

Тут буде одна з найнеприємніших пасток для початківців. Масив за своєю природою впорядкований: елемент 0 іде перед елементом 1. А словник — це структура для пошуку, а не для збереження красивого порядку.

Тому порядок елементів:

  • під час print(dictionary)
  • під час перебору for (k, v) in dictionary

не варто сприймати як надійну гарантію, на яку можна спиратися в логіці коду.

Словник може друкуватися по-різному залежно від версії, середовища, даних і внутрішніх оптимізацій. Іноді він виглядатиме стабільно, і саме це найгірше: ви звикаєте, а потім одного дня він раптом змінюється.

Невеликий приклад — зверніть увагу на коментар: порядок не гарантований.

import Foundation

let dict: [String: Int] = ["a": 1, "b": 2, "c": 3]
print(dict) // Порядок ключів не є контрактом

3. Приклад: книга → прочитані сторінки

Щоб не залишатися на рівні абстракції, продовжимо наш навчальний консольний мініпроєкт: зберігатимемо, скільки сторінок ви прочитали в кожній книжці. На попередніх заняттях у нас уже були рядки, числа, split, Optional і базові функції — цього достатньо, щоб акуратно побудувати введення.

Зробімо найпростіший парсер рядка формату:

Swift 120

Тобто одне слово-назва і число сторінок.

import Foundation

func parseBookLine(_ line: String) -> (title: String, pages: Int)? {
    let parts = line.split(separator: " ")
    guard parts.count == 2, let pages = Int(parts[1]) else { return nil }
    return (String(parts[0]), pages)
}

Тепер створімо словник і спробуймо додати один запис. Зверніть увагу: ми не займаємося справжнім читанням за ключем — це наступна лекція. Тут наша мета — створити словник і навчитися додавати до нього пари.

import Foundation

var pagesByTitle: [String: Int] = [:]

if let line = readLine(), let book = parseBookLine(line) {
    pagesByTitle[book.title] = book.pages
}

print(pagesByTitle) // Наприклад: ["Swift": 120]

Якщо ввести:

Swift 120

то словник отримає пару "Swift"120.

Тепер покажемо важливу особливість словника: якщо ви введете книжку з тією самою назвою вдруге, значення заміниться, тому що ключ той самий.

import Foundation

var pagesByTitle: [String: Int] = ["Swift": 120]
pagesByTitle["Swift"] = 150

print(pagesByTitle) // ["Swift": 150]

З погляду нашого мініпроєкту це навіть логічно: ви дочитали ще 30 сторінок, і «поточна кількість» стала 150. Так, поки що ми не використовуємо += і не накопичуємо значення акуратно — це буде трохи пізніше, коли познайомимося зі зручними способами оновлення значень. Поки що просто запамʼятаймо: присвоєння за ключем працює і як «додати», і як «оновити».

Шпаргалка за синтаксисом

Коли ви вперше починаєте писати словники, мозок зазвичай плутає квадратні дужки масиву й квадратні дужки словника. Щоб заспокоїти внутрішнього панікера, корисно тримати одну таблицю перед очима — хоча б кілька днів.

Що хочемо зробити Як пишемо
Тип словника
[String: Int]
Літерал словника
["a": 1, "b": 2]
Порожній словник
var d: [String: Int] = [:]
Додати/оновити пару
d["a"] = 10
Отримати кількість пар
d.count

І ще: count є і в масиві, і в словнику, але сенс різний. У масиву count — це кількість елементів, а у словника count — кількість унікальних ключів, тобто пар ключ → значення.

4. Типові помилки під час створення Dictionary

Помилка №1: намагатися створити порожній словник як var d = [:] і дивуватися помилці компілятора.
Порожній літерал [:] не містить жодного ключа й жодного значення, тому Swift не може здогадатися про типи. Вирішується просто: або явно задайте тип var d: [String: Int] = [:], або використовуйте Dictionary<String, Int>(). У навчальних завданнях найчастіше обирають перший варіант, тому що він коротший і відразу видно формат [Key: Value].

Помилка №2: очікувати, що словник «памʼятає порядок», і будувати логіку на тому, що «першим надрукується ключ Ann».
Словник призначений для швидкого доступу за ключем, а не для красивого порядку. Іноді порядок виводу здається стабільним, і це створює хибне відчуття безпеки. Але покладатися на це не можна: одного разу ви зміните вхідні дані, версію компілятора або просто запустите все в іншому середовищі — і «першим» стане інший ключ.

Помилка №3: переплутати «додавання другого запису» з «перезаписом першого».
Якщо ви двічі записуєте значення за одним і тим самим ключем, другий раз не «додає ще один запис», а замінює наявний. Це нормально й часто корисно, але якщо ви очікували «історію змін», то словник — не той інструмент: вам потрібен інший формат зберігання, наприклад масив подій.

Помилка №4: обрати невідповідний тип ключа й упертися в Hashable.
Новачки іноді намагаються зробити ключем масив, словник або щось складне без розуміння обмежень. Swift тут доволі суворий: ключ повинен бути Hashable. На старті курсу сприймайте це як правило дорожнього руху: «ключ має бути простим і стабільним». Рядки та числа — найкращий вибір.

Помилка №5: оголосити словник через let, а потім намагатися додавати пари.
Це класика жанру: «я ж просто хочу дописати один рядок». Але let означає незмінність. Якщо словник має зростати або оновлюватися — це var. let залишайте для випадків, коли словник створено й далі використовують лише для читання, наприклад для таблиці констант на кшталт «код → повідомлення».

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ