Перечисления
Перечисления определяют общий тип для группы ассоциативных значений и позволяют работать с этими значениями в типобезопасном режиме в вашем коде.
Если вы знакомы с C, то вы знаете, что перечисления в C присваивают соответствующие имена набору целочисленных значений. Перечисления в Swift более гибкий инструмент и не должны предоставлять значения для каждого члена перечисления. Если значение (известное как “сырое” значение) предоставляется каждому члену перечисления, то это значение может быть строкой, символом или целочисленным значением, числом с плавающей точкой.
Кроме того, членам перечисления можно задать соответствующие значения любого типа, которые должны быть сохранены вместе с каждым кейсом перечисления. Вы можете определить общий набор соответствующих значений как часть одного перечисления, каждый из которых будет иметь разные наборы значений ассоциативных типов связанных с ними.
Перечисления в Swift - типы “первого класса”. Они обладают особенностями, которые обычно поддерживаются классами, например, вычисляемые свойства, для предоставления дополнительной информации о текущем значении перечисления, методы экземпляра для дополнительной функциональности, относящейся к значениям, которые предоставляет перечисление.
Перечисления так же могут объявлять инициализаторы для предоставления начального значения элементам. Они так же могут быть расширены для наращивания своей функциональности над её начальной реализацией. Могут соответствовать протоколам для обеспечения стандартной функциональности.
Больше про эти возможности можно прочитать в разделах Свойства, Методы, Инициализация, Расширения и Протоколы.
Синтаксис перечислений
Перечисления начинаются с ключевого слова enum, после которого идет имя перечисления и полное его определение в фигурных скобках:
enum SomeEnumeration {
//здесь будет объявление перечисления
}
Ниже пример с четырьмя сторонами света:
enum CompassPoint {
case north
case south
case east
case west
}
Значения, объявленные в перечислении (north, south, east, и west), называются кейсами перечисления. Используйте ключевое слово case для включения нового кейса перечисления.
Заметка
В отличии от C и Objective-C в Swift кейсам перечисления не присваиваются целочисленные значения по умолчанию при их создании. В примере выше CompassPoint, значения членов north, south, east и west неявно не равны 0, 1, 2, 3. Вместо этого различные члены перечисления по праву полностью самостоятельны, с явно объявленным типом CompassPoint.
Множественные значения члена перечисления могут записываться в одну строку, разделяясь между собой запятой:
enum Planet {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}
Каждое объявление перечисления объявляет и новый тип. Как и остальные типы в Swift, их имена (к примеру CompassPoint и Planet) должны начинаться с заглавной буквы. Имена перечислениям лучше давать особенные, а не те, которые вы можете использовать в нескольких местах, так чтобы они читались как само собой разумеющиеся:
var directionToHead = CompassPoint.west
Тип directionToHead выведен при инициализации одного из возможных значений CompassPoint. Если directionToHead объявлена как CompassPoint, то можем использовать различные значения CompassPoint через сокращенный точечный синтаксис:
directionToHead = .east
Тип directionToHead уже известен, так что вы можете не указывать тип, присваивая значения. Так делается для хорошо читаемого кода, когда работаете с явно указанными типами значений перечисления.
Использование перечислений с инструкцией switch
Вы можете сочетать индивидуальные значения перечисления с инструкцией switch:
directionToHead = .south
switch directionToHead {
case .north:
print("Lots of planets have a north")
case .south:
print("Watch out for penguins")
case .east:
print("Where the sun rises")
case .west:
print("Where the skies are blue")
}
// Выводит "Watch out for penguins"
Вы можете прочитать этот код как:
“Рассмотрим значение directionToHead. В случае, когда directionToHead равняется .north, выводится сообщение ”Lots of planets have a north“. В кейсе, где оно равняется .south, выводится сообщение ”Watch out for penguins".
…и так далее…
Как сказано в главе “Управление потоком”, оператор switch должен быть исчерпывающим, когда рассматриваются члены перечисления. Если мы пропустим case .west, то код не скомпилируется, так как не рассматривается полный перечень членов CompassPoint. Требования к конструкции быть исчерпывающей, помогает случайно не пропустить член перечисления.
Если не удобно описывать кейс для каждого члена перечисления, то вы можете использовать кейс default, для закрытия всех остальных вариантов перечисления:
let somePlanet = Planet.earth
switch somePlanet {
case .earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
// Выведет "Mostly harmless"
Итерация по кейсам перечисления
Для некоторых перечислений можно получить коллекцию всех кейсов перечисления. Нужно лишь написать: CaseIterable после имени перечисления. Swift предоставляет коллекцию всех кейсов, как свойство allCases типа перечисления. Пример:
enum Beverage: CaseIterable {
case coffee, tea, juice
}
let numberOfChoices = Beverage.allCases.count
print("\(numberOfChoices) beverages available")
// Выведет "3 beverages available"
В приведенном выше примере следует писать Beverage.allCases для доступа к коллекции, содержащей все кейсы перечисления Beverage. Можно использовать allCases, как и любую другую коллекцию - элементы коллекции являются экземплярами типа перечисления, поэтому в этом случае они являются значениями Beverage. В приведенном выше примере подсчитывается количество кейсов, приведенный ниже пример использует цикл for для итерации по всем кейсам.
for beverage in Beverage.allCases {
print(beverage)
}
// coffee
// tea
// juice
Синтаксис, использованный в приведенных выше примерах, обозначает перечисление как соответствующее протоколу CaseIterable. Для получения информации о протоколах см. Протоколы.
Ассоциативные значения
Примеры в предыдущей секции показывают, как кейсы перечисления определены (и типизированы) как самостоятельные значения. Вы можете установить Planet.earth как константу или переменную и посмотреть какое значение она содержит. Однако бывает удобно хранить ассоциативные значения других типов вместе с этими значениями кейсов перечисления. Это позволяет вам хранить дополнительную пользовательскую информацию вместе со значением кейса и разрешает изменять эту информацию каждый раз как вы используете этот кейс перечисления в вашем коде.
Вы можете объявить перечисления Swift для хранения ассоциативных значений любого необходимого типа, и типы значений могут отличаться для каждого члена перечисления, если это необходимо. Перечисления такого типа так же известны как размеченные объединения, маркированные объединения или варианты в других языках программирования.
Для примера, предположим систему инвентаризации, которая должна отслеживать товар двумя различными типами штрих-кодов. Одни товары имеют коды типа 1D формата UPC-A, которые используют цифры от 0 до 9. Каждый штрих-код имеет свою “систему цифр”, где идут пять цифр “кода производителя” и пять цифр “кода продукта”. Затем идет “проверочная” цифра, которая проверяет, что код был отсканирован корректно:
Другие продукты имеют маркировку штрих-кодом 2D формата QR, который может использовать любой символ из ISO 8859–1 и может закодировать строку длиною 2953 символа:
Было бы удобно, если бы система контроля и учета товара могла бы хранить штрих-коды формата UPC-A, как кортеж из четырех целых чисел и 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 code: \(productCode).")
}
// Выводит "QR code: ABCDEFGHIJKLMNOP."
Если все ассоциативные значения для членов перечисления извлекаются как константы или переменные, то для краткости вы можете разместить одиночное let или var перед именем члена:
switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
print("UPC : \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
print("QR code: \(productCode).")
}
// Выведет "QR code: ABCDEFGHIJKLMNOP."
Исходные значения
В примере с Barcode в главе “Ассоциативные значения” можно увидеть как кейсы перечисления могут объявлять значения различных типов, которые они могут хранить. Как альтернатива ассоциативным значениям, кейсы перечисления могут иметь начальные значения (называются “исходными значениями”), которые всегда одного типа.
Вот пример перечисления, члены которого хранят исходные значения ASCII, прописанные рядом:
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}
Исходные значения перечисления ASCIIControlCharacter определены как тип Character, и им присвоены распространенные контрольные символы ASCII, которые описаны в разделе “Строки и символы”.
Исходные значения могут быть строками, символами или любым целым числом или числом с плавающей точкой. Каждое исходное значение должно быть уникальным при его объявлении.
Заметка
Исходные значения - это не то же самое, что ассоциативные значения. Исходные значения устанавливаются в качестве дефолтных значений, когда вы в первый раз определяете перечисление в вашем коде, как три ASCII кода выше. Исходное значение для конкретного кейса перечисления всегда одно и то же. Ассоциативные значения устанавливаются при создании новой константы или переменной, основываясь на одном из кейсов перечисления, и могут быть разными каждый раз, когда вы делаете это.
Неявно установленные исходные значения
Когда вы работаете с перечислениями, которые хранят целочисленные или строковые исходные значения, вам не нужно явно присваивать исходные значения для каждого конкретного кейса. Swift автоматически сделает это за вас.
Например, когда целые числа используются в качестве исходных значений, неявное значение для каждого кейса будет на единицу больше, чем в предыдущем кейсе. Если первый кейс не имеет заданного значения, его значение равно 0.
Перечисление ниже представляет собой уточнение ранее указанного перечисления Planet, с целочисленными исходными значениями для представления удаленности каждой планеты от солнца:
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}
В приведенном выше примере, Planet.mercury имеет явное исходное значение 1, Planet.venus имеет неявное исходное значение 2, и так далее.
Когда строки используются в качестве исходных значений, неявное значение для каждого кейса является текстом имени этого кейса.
Перечисление ниже представляет собой уточнение ранее указанного перечисления CompassPoint с исходными значениями строк для представления имени каждого направления:
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"
Инициализация через исходное значение
Если вы объявили перечисление вместе с типом исходного значения, то перечисление автоматически получает инициализатор, который берет значение типа исходного значения (как параметр rawValue) и возвращает либо член перечисления либо nil. Вы можете использовать этот инициализатор, чтобы попытаться создать новый экземпляр перечисления.
В этом примере Uranus инициализируется через его исходное значение 7:
let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet имеет тип Planet? и его значение равно Planet.uranus
Конечно не все возможные значения Int найдут отклик в данном перечислении. Из-за этого инициализаторы исходных значений всегда возвращают опциональный член перечисления. В этом примере possiblePlanet типа Planet? или “опциональный Planet”.
Заметка
Инициализатор исходного значения - проваливающийся инициализатор, потому как не каждое исходное значение будет возвращать кейс перечисления. Более подробно см. Проваливающиеся инициализаторы.
Если вы попытаетесь найти планету с номером позиции 11, то значение опциональной Planet, возвращенное исходным значением инициализатора, будет nil:
let positionToFind = 11
if let somePlanet = Planet(rawValue: positionToFind) {
switch somePlanet {
case .earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
} else {
print("There isn't a planet at position \(positionToFind)")
}
// Выведет "There isn't a planet at position 11"
Этот пример использует привязку опционалов для попытки добраться до Planet с исходным значением 11. Выражение if let somePlanet = Planet(rawValue: 11) создает опциональную Planet и устанавливает значение somePlanet опциональной Planet, если она может быть восстановлена. В этом случае невозможно добраться до планеты с позицией 11, таким образом срабатывает ветка else.
Рекурсивные перечисления
Рекурсивные перечисления - это такие перечисления, экземпляры которого являются ассоциативным значением одного или более кейсов перечисления. Вы обозначаете такие кейсы перечисления при помощи ключевого слова 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 имеет цифру справа от умножения и другое выражение слева от умножения. Поскольку данные вложены, перечисление использующееся для хранения данных, также должно поддерживать вложенность-это означает, что перечисление должно быть рекурсивными. Приведенный ниже код показывает как работает рекурсивное перечисление 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"
Эта функция вычисляет простое число, просто возвращая ассоциативное значение. Она вычисляет сложение или умножение, вычисляя выражение по левую сторону, затем по правую сторону, затем складывает или умножает их.