Link Search Menu Expand Document

Перечислення

Перечислення визначають спільний тип для групи пов’язаних значень, та дозволяють працювати з цими значеннями в коді у типобезпечний спосіб.

Якщо ви знайомі з мовою C, вам могло бути відомо, що перечислення в C присвоюють пов’язані імена набору цілочисельних значень. Перечислення у Swift є набагато більш гнучкі, а їх елементи не обов’язково повинні мати конкретні значення. Однак елементи перечислення можуть мати значення (відомі, як “сирі” значення), і ці значення не обов’язково повинні бути цілочисельного типу: це можуть бути рядки, символи, цілі чи числа з рухомою комою.

Елементи перечислень також можуть визначати асоційовані значення будь-якого типу, що будуть зберігатись разом із кожним елементом, подібно до юніонів чи варіантів в інших мовах програмування. Можна визначити спільний набір елементів всередині одного перечислення, кожен з яких матиме різний набір асоційованих із ним значень певних типів.

Перечислення у Swift є самі по собі першокласними типами. Вони підтримують багато можливостей, які раніше традиційно підтримувалась лише класами, такі як властивості, що обчислюються (щоб надавати додаткову інформацію про поточне значення перечислення), та методи екземплярів (щоб надавати функціональність, пов’язану зі значенням, котре представляє перечислення). Перечислення можуть також визначати ініціалізатори для задавання початкового значення елемента; для перечислень можна створювати розширення, щоб виводити їх функціональність за межі початкової реалізації; перечислення можуть підпорядковуватись до протоколів, щоб надавати стандартну функціональність.

Детальніше з цими можливостями можна ознайомитись у розділах Властивості, Методи, Ініціалізація, Розширення, та Протоколи.

Синтаксис перечислень

Щоб створити перечислення, слід використати ключове слово enum та помістити все оголошення в пару фігурних дужок:

enum <ЯкесьПеречислення> {
    // тут йде визначення перечислення
}

Ось приклад перечислення для чотирьох основних точок компасу:

enum CompassPoint {
    case north
    case south
    case east
    case west
}

Значення, оголошені в перечисленні (такі як north, south, east, та west) є його елементами перечислення. Для оголошення нового елементу перечислення використовують ключове слово case.

Примітка

Навідміну від C та Objective-C, елементам перечислень Swift при створенні не присвоюється цілочисельне значення за замовчанням. У прикладі вище, елементи перечислення CompassPointnorth, south, east, та west – не дорівнюватимуть неявно 0, 1, 2 та 3, як це було б в C. Замість цього, елементи перечислення є повноцінними значеннями на власних правах, із явно визначеним типом – CompassPoint.

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

enum Planet {
    case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}

Кожне оголошення перечислення визначає новий тип. Як і інші типи у Swift, їх назви (такі як CompassPoint та Planet) заведено писати з великої літери. Бажано давати перечисленням назви в однині, а не у множині, щоб вони читались самоочевидним чином:

var directionToHead = CompassPoint.west
// дослівдно: var напрямокРуху = ТочкаКомпасу.захід

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

directionToHead = .east

Тип змінної directionToHead вже відомий, і тому можна пропустити ім’я типу при присвоєнні нового значення. Це робить код набагато легшим для читання при роботі з явно типізованими значеннями перечислень.

Визначення елементу перечислення за допомогою інструкції Switch

Визначити елементи перечислення можна за допомогою інструкції switch наступним чином:

directionToHead = .south
switch directionToHead {
case .north:
    print("На багатьох планетах є північ")
case .south:
    print("Стережіться пінгвінів")
case .east:
    print("Тут встає сонце")
case .west:
    print("Тут небо синє")
}
// Надрукує "Стережіться пінгвінів"

Цей код можна прочитати так:

“Розглядаємо значення змінної directionToHead. Якщо вона дорівнює .north, друкуємо "На багатьох планетах є північ". Якщо вона дорівнює .south, друкуємо "Стережіться пінгвінів".”
…і так далі.

Як описано в розділі Потік керування, інструкція switch повинна бути вичерпною при розгляді елементів перечислення. Якщо пропустити випадок для елементу .west, код не скомпілюється, бо він не розглядатиме повного списку елементів CompassPoint. Вимога вичерпності гарантує, що елементи перечислення не будуть пропущені випадково.

Якщо в даному випадку недоречно створювати окремий випадок для кожного можливого елементу перечислення, можна створити випадок за замовчанням default для покриття всіх елементів, які не оброблено явно:

let somePlanet = Planet.earth        // Планета.земля
switch somePlanet {
case .earth:
    print("Здебільшого, нешкідлива")
default:
    print("Небезпечне місце для людей")
}
// Надрукує "Здебільшого, нешкідлива"

Ітерування по елементах перечислення

Для деяких перечислень буває зручно мати всі їхні елементи у колекції. Щоб дозволити це, слід написати : CaseIterable після назви перечислення. У такому випадку Swift видасть усі елементи перечислення у властивості allCases типу перечислення. Ось приклад:

enum Beverage: CaseIterable {
    case coffee, tea, juice
}
let numberOfChoices = Beverage.allCases.count
print("Доступно напоїв: \(numberOfChoices)")
// Надрукує "Доступно напоїв: 3"

У прикладі вище, для доступу до колекції, що містить усі елементи перечислення Beverage, пишемо Beverage.allCases. Колекцію allCases можна використовувати як будь-яку іншу колекцію: елементами цієї колекції є екземпляри типу перечислення, тому у даному випадку вони є значеннями типу Beverage. У прикладі вище підраховується їхня кількість, а у прикладі нижче – відбувається ітерування по ним у циклі for-in:

for beverage in Beverage.allCases {
    print(beverage)
}
// coffee
// tea
// juice

Використаний у прикладах вище синтаксис позначає перечислення підпорядкованим до протоколу CaseIterable. Детальніше про протоколи можна дізнатись у розділі Протоколи.

Асоційовані значення

У прикладах в попередньому підрозділі показано, як елементи перечислення визначено (і типізовано) як повноправне значення. Константі чи змінній можна присвоїти значення Planet.earth, і звірити це значення пізніше. Однак, іноді буває корисно мати можливість зберігати асоційовані значення інших типів разом з елементами перечислень. Це дозволяє зберігати власну додаткову інформацію разом з елементом перечислення, і дозволяє цій інформації змінюватись при кожному використанні цього елементу перечислення у коді.

У Swift можна оголошувати перечислення для зберігання асоційованих значень будь-якого заданого типу, і типи цих значень можуть за потреби бути різними для кожного елементу перечислення. Перечислення такого виду відомі як розмічені об’єднання, тип-сума або варіант в інших мовах програмування.

Наприклад, припустимо, що система інвентаризації магазину потребує, щоб товари відслідковувались за допомогою одного з двох видів штрих-кодів. Одні товари маркуються одномірним штрих-кодом у форматі UPC, що використовує цифри від 0 до 9. Кожен штрих-код містить цифру, що кодує “систему числення”, після цього йде п’ять цифр “коду виробника” та п’ять цифр “коду товару”. Останньою є “перевірочна” цифра, котра потрібна для підтвердження правильності зчитування коду:


Інші товари маркуються двовимірним штрих-кодом у форматі QR-коду, що може використовувати будь-які символи в кодуванні ISO 8859-1, та може кодувати рядок довжиною до 2953 символів.

Для системи інвентаризації було б зручно мати можливість зберігати штрих-коди UPC як кортеж із чотирьох цілих чисел, а QR-коди - як рядки довільної довжини.

У Swift, можна створити перечислення, котре може зберігати штрих-коди обох типів:

enum Barcode {
    case upc(Int, Int, Int, Int)
    case qrCode(String)
}

Це можна прочитати так:

“Оголошуємо тип-перечислення із назвою Barcode, котрий може приймати або значення upc з асоційованим значенням типу (Int, Int, Int, Int), або значення qrCode з асоційованим значенням типу String.”

Це оголошення не надає жодного фактичного значення Int чи String – воно лише визначає тип асоційованих значень, що константи чи змінні типу Barcode можуть зберігати, коли вони дорівнюють Barcode.upc чи Barcode.qrCode.

Нові значення штрих-кодів можна створювати, шляхом використання одного з елементів перечислення:

var productBarcode = Barcode.upc(8, 85909, 51226, 3)

У цьому прикладу створено нову змінну на ім’я productBarcode, і їй присвоєно значення Barcode.upc з асоційованим значенням кортежу (8, 85909, 51226, 3).

Той же товар може мати штрих-код іншого типу:

productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

На даному етапі, початкове значення Barcode.upc та його цілочисельні значення було замінено новим значенням Barcode.qrCode та рядком. Константи та змінні типу Barcode можуть зберігати або значення .upc, або значення .qrCode (разом із їх асоційованими значеннями), але вони не можуть зберігати обидва значення одночасно.

Різні види штрих-кодів можуть визначатись за допомогою інструкції switch, як і раніше. Цього разу, однак, в інструкції switch можна витягувати асоційовані значення. Кожне з асоційованих значень можна витягнути у вигляді константи (за допомогою префіксу let) чи змінної (за допомогою префіксу var) для використання в тілі випадку інструкції switch:

switch productBarcode {
case .upc(let numberSystem, let manufacturer, let product, let check):
    print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case .qrCode(let productCode):
    print("QR: \(productCode).")
}
// Надрукує "QR: ABCDEFGHIJKLMNOP."

Якщо всі асоційовані значення елементу перечислення витягуються як константи, або якщо всі вони витягуються як змінні, для лаконічності можна вставляти лише одне ключове слово let чи var перед іменем цього елементу:

switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
    print("UPC : \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
    print("QR: \(productCode).")
}
// Надрукує "QR: ABCDEFGHIJKLMNOP."

Сирі значення

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

Ось приклад перечислення, у якому разом з елементами перечислення зберігаються сирі тестові символи з кодування ASCII:

enum ASCIIControlCharacter: Character {
    case tab = "\t"
    case lineFeed = "\n"
    case carriageReturn = "\r"
}

Тут визначено, що перечислення ASCIIControlCharacter має сирі значення типу Character, а його елементам відповідають деякі з досить уживаних контрольних символів з кодування ASCII. Детальніше текстові символи Character були описані в розділі Рядки та символи.

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

Примітка

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

Неявно присвоєні сирі значення

При роботі з перечисленнями, що зберігають цілочисельні чи рядкові сирі значення, не обов’язково завжди явно вказувати сире значення для кожного елементу. Якщо не вказувати їх, Swift автоматично прив’яже ці значення за вас.

Наприклад, для цілочисельних сирих значень, неявне сире значення для кожного наступного елемента на одиницю більше за сире значення попереднього елемента. Якщо для першого елемента не вказано сирого значення, Swift прийме його значення за 0.

Перечислення нижче є вдосконаленням попереднього перечислення Planet, з цілочисельними сирими значеннями, що представляють порядок планети від сонця:

enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}

У прикладі вище, елемент Planet.mercury має явне сире значення 1, елемент Planet.venus має неявне сире значення 2, і так далі.

Коли сирим значенням є рядок, неявним значенням для кожного елементу є текст назви самого елемента.

Перечислення нижче є вдосконаленням попереднього перечислення CompassPoint, із сирим значенням типу String, що представляє назву кожного напрямку:

enum CompassPoint: String {
    case north, south, east, west
}

У прикладі вище, елемент CompassPoint.south має сире значення "south", і так далі.

Отримати сире значення елементу перечислення можна за допомогою його властивості rawValue:

let earthsOrder = Planet.earth.rawValue
// earthsOrder дорівнює 3

let sunsetDirection = CompassPoint.west.rawValue
// sunsetDirection дорівнює "west"

Ініціалізація за допомогою сирого значення

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

Наступний приклад демонструє створення елементу, що відповідає планеті Уран, з числа 7:

let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet має тип Planet? та дорівнює Planet.uranus

Однак, не кожному можливому значенню Int відповідає планета з перечислення Planet. Через це, ініціалізатор сирим значенням завжди повертає опціональний елемент перечислення. У прикладі вище, possiblePlanet має тип Planet?, або “опціональна Planet.”

Примітка

Ініціалізатор сирим значенням є ненадійним ініціалізатором, бо не для кожного сирого значення він поверте елемент перечислення. Детальніше з ненадійними ініціалізаторами можна познайомитись в розділі Ненадійні ініціалізатори.

Якщо спробувати знайти планету в позиції 11, опціональне значення Planet, що повернеться з ініціалізатора сирим значенням, буде nil:

let positionToFind = 11
if let somePlanet = Planet(rawValue: positionToFind) {
    switch somePlanet {
    case .earth:
        print("Здебільшого, нешкідлива")
    default:
        print("Небезпечне місце для людей")
    }
} else {
    print("Не існує планети в позиції \(positionToFind)")
}
// Надрукує "Не існує планети в позиції 11"

У даному прикладі для доступу до планети, ініціалізованої сирим значенням 11, використовується прив’язування опціоналу. Якщо інструкція if let somePlanet = Planet(rawValue: 11) створить планету, константі somePlanet буде присвоєно значення опціональної Planet. У цьому ж випадку, планету з позицією 11 створити неможливо, тому виконується гілка коду else.

Рекурсивні перечислення

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

Наприклад, ось перечислення, що зберігає прості арифметичні вирази:

enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}

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

indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

Дане перечислення може зберігати три види арифметичних виразів: ціле число, додавання двох виразів, та множення двох виразів. Елементи addition та multiplication мають асоційовані значення, що є теж арифметичними виразами – ці асоційовані значення дозволяють створювати вкладені вирази. Наприклад, вираз (5 + 4) * 2 містить число справа від знаку множення та інший вираз зліва від знаку множення. Оскільки вираз (5 + 4) є вкладеним у вираз (5 + 4) * 2, перечислення, в якому ці вирази моделюються, теж повинні підтримувати вкладеність, тобто бути рекурсивними. У коді нижче демонструється рекурсивне перечислення ArithmeticExpression, створене для моделювання виразу (5 + 4) * 2:

let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))

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

func evaluate(_ expression: ArithmeticExpression) -> Int {
    switch expression {
    case let .number(value):
        return value
    case let .addition(left, right):
        return evaluate(left) + evaluate(right)
    case let .multiplication(left, right):
        return evaluate(left) * evaluate(right)
    }
}

print(evaluate(product))
// Надрукує "18"

Ця функція обчислює вираз “ціле число” просто повертаючи асоційоване значення елементу number. Додавання та множення обчислюється шляхом обчислення лівого виразу, обчислення правого виразу, та додавання/множення результатів цих обчислень.