obo.dev

Перечисления (Enum)

10 Dec 2022

Перечисления (Enum)

В Swift перечисление (enum, сокращение от enumeration) — это определяемый пользователем тип данных, который имеет фиксированный набор связанных значений. Перечисления позволяют работать с этими значениями в типобезопасном режиме в коде.

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

Перечисления в Swift - типы “первого класса” - обладают особенностями, которые обычно поддерживаются классами, например:

  • могут иметь вычисляемые свойства, для предоставления дополнительной информации о текущем значении перечисления;
  • могут иметь методы экземпляра для дополнительной функциональности, относящейся к значениям, которые предоставляет перечисление;
  • могут иметь инициализаторы для предоставления начального значения элементам;
  • могут быть расширены для наращивания своей функциональности над её начальной реализацией;
  • могут соответствовать протоколам для обеспечения стандартной функциональности.

Больше про свойства, методы, расширения и протоколы можно прочитать в отдельных статьях. См. ссылки внизу.

Общая конструкция - синтаксис перечислений

Для того, чтоб определить перечисление, необходимо его объявить с помощью ключевого слова enum. Затем указать имя перечисления. Затем, в фигурных скобках указать добавляемый необходимый функционал:

enum EnumName {
	// code
}

Здесь указано, что будет объявлено перечисление с именем EnumName.

Так как перечисление - это тип данных, то имя перечисления должно начинаться с большой буквы. Используется так называемый “UpperCamelCase”.

Внутри перечисления используются значения перечисления, которые также называются кейсами перечисления или случаями перечисления (enum cases). Для объявления значений внутри перечисления используется ключевое слово case.

enum EnumName {
	case value1
	case value2
}

Можно указывать несколько значений перечисления в одной строке. Для этого, эти значения указываются через запятую:

enum EnumName {
	case value1, value2
}

Можно комбинировать оба вида объявления перечисления. Например:

enum EnumName {
	case value1, value2
	case value3, value4
}

Значения перечисления должны быть уникальными.

Пример перечисления:

enum Season {
    case spring, summer, autumn, winter
}

В этом примере

  • Season - название перечисления;
  • spring, summer, autumn, winter - значения, определенные внутри перечисления.

Создание переменных с типом данных перечисления

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

Например:

var currentSeason: Season

Здесь currentSeason — это переменная типа Season.

Эта переменная может хранить только значения (spring, summer, autumn, winter), которые присутствуют внутри перечисления Season.

Присвоение значения переменным

Для присвоения переменной значения перечисления необходимо использовать имя перечисления и через точку . (dot-notation) присваивается значение перечисления. Например:

// assign summer to currentSeason variable
var currentSeason = Season.summer

Здесь было присвоено переменной currentSeason значение summer из перечисления currentSeason.

Тип данных переменной нельзя изменить.

Если у переменной уже указан тип данных перечисления, то значение этой переменной можно присваивать без указания имени перечисления. Например:

var currentSeason: Season
currentSeason = .summer

Подобным образом можно менять значение переменной. Пример:

var currentSeason = Season.spring
print(currentSeason)

// Output
// spring

currentSeason = .summer
print(currentSeason)

// Output
// summer

Пример использования перечисления:

// define enum 
enum Season {
    // define enum values
    case spring, summer, autumn, winter
}

// create enum variable
var currentSeason: Season

// assign value to enum variable
currentSeason = Season.summer

print("Current Season:", currentSeason)

// Output
// Current Season: summer

В приведенном выше примере

  • Season — перечисление с 4 значениями: spring, summer, autumn, winter;
  • currentSeason - переменная типа перечисления;
  • currentSeason = Season.summer — присваивается значение перечисления currentSeason.

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

var lastSeason = Season.spring

Перечисления нельзя изменить после установки.

enum AuthenticationState {
    case authenticated
    case unauthenticated
    case undetermined
}

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

Перечисление с оператором Switch

Для того, чтоб менять значения из перечисления для переменной в зависимости от условий - можно использовать перечисление с оператором switch. Например:

enum PizzaSize {
    case small, medium, large
}

var size = PizzaSize.medium

switch(size) {
    case .small:
        print("I ordered a small size pizza.")

    case .medium:
        print("I ordered a medium size pizza.")

    case .large:
        print("I ordered a large size pizza.");
}

// Output
// I ordered a medium size pizza.

В приведенном выше примере было создано перечисление с именем PizzaSize со значениями: small, medium, large.

Обратите внимание на строку:

var size = PizzaSize.medium

Здесь присваиваем значение media для переменной size с типом перечисления.

Также использовали значение переменной size внутри оператора switch. И значение size сравнивается со значением каждого оператора case.

Поскольку, значение size совпадает с case .medium, выполняется код внутри оператора case.

Ещё пример. Представьте, что мы кодируем робота. У робота есть эмоции. Мы моделируем эти эмоции или состояния с помощью перечисления:

enum Emotion {
    case happy, sad, angry, scared, surprised
}

Теперь можно использовать оператор switch, чтобы реагировать на эти эмоции:

struct Robot {
    var mood: Emotion

    func destroyAllHumans() {
        print("I kill you!!!")
    }

    func rust() {
        print("I cry...")
    }

    func play(_ track: String) {}
}

var robot = Robot(mood: .angry)

switch robot.mood {
	case .angry:
    	robot.destroyAllHumans()
	case .sad:
    	robot.rust()
	case .happy:
    	robot.play("happy.mp3")
	default:
    	print("Неизвестная эмоция.")
}

// Output
// I kill you!!!

Каждый case в блоке switch соответствует отдельному case в перечислении, и выполняет определённую инструкцию.

Так как перечисления являются исчерпывающими - то в операторе switch не нужно указывать значения для default, если указать в операторе switch все варианты из перечисления.

enum AuthenticationState {
    case authenticated
    case unauthenticated
    case undetermined
}

func checkAuth(_ state: AuthenticationState) {
    switch state {
    case .authenticated:
            print("Ok")
    case .unauthenticated:
        print("Access denied!")
    case .undetermined:
        print("Ask admin")
    }
}
var auth: AuthenticationState = .undetermined
checkAuth(auth)

// Output
// Ask admin

auth = .authenticated
checkAuth(auth)

// Output
// Ok

В этом примере в функции checkAuth() были перебраны в switch все варианты из перечисления AuthenticationState. Поэтому указывать значения для default не нужно.

Итерация по значениям перечисления

Для некоторых перечислений полезно иметь коллекцию всех случаев этого перечисления. В Swift используется протокол CaseIterable для получения доступа ко всем значениям в перечислении и, в дальнейшем, для перебора перечисления с помощью цикла. Для этого необходимо подписать перечисление под протокол CaseIterable. Например:

enum Season: CaseIterable {
    ...
}

В этом протоколе указано свойство allCases, которое включает в себя все значения перечисления. Теперь можно использовать свойство allCases, как обычный массив всех значений перечисления, в том же порядке, что и в перечислении

Теперь можно использовать свойство allCases для просмотра каждого варианта перечисления с помощью цикла.

for currentSeason in Season.allCases {
    ...
}

Пример перебора перечисления циклом:

// conform Languages to caseIterable 
enum Season: CaseIterable {
    case spring, summer, autumn, winter 
}

// for loop to iterate over all cases
for currentSeason in Season.allCases {
    print(currentSeason)
}

// Output
// spring
// summer
// autumn
// winter

В приведенном выше примере перечисление Season было согласовано с протоколом CaseIterable - перечисление Season было подписано под протокол CaseIterable.

Затем используется свойство allCases с циклом for-in для перебора каждого варианта перечисления.


Связанные значения перечисления (Associated Values)

К значениям перечислений можно добавить дополнительную информацию с помощью связанных значений (ассоциированные значения, Associated Values).

Например:

enum Laptop {
    // associate value 
    case name(String)
    ...
}

В перечислении Laptop для значения name можно прикрепить связанное значение по типу String.

Теперь можно присвоить связанное значение для значения name перечисления.

Laptop.name("Razer")

Здесь Razer — это связанное значение для name.

Пример перечисления со связанными значениями:

enum Laptop {
    // associate string value
    case name(String)

    // associate integer value  
    case price(Int)
}

// pass string value to name
var brand = Laptop.name("Razer")
print(brand)

// pass integer value to price
var offer = Laptop.price(1599)
print(offer)

// Output
// name("Razer")
// price(1599)

В приведенном выше примере связанное значение

  • Razor присваивается name;
  • 1599 присваивается price.

Итак, при необходимости можно добавить дополнительную информацию к значениям перечисления. И эта дополнительная информация, прикрепленная к значениям перечисления, называется ассоциированными значениями (связанными значениями).

Давайте посмотрим пример:

enum Distance {

    // associate value 
    case km(String)
    ...
}

Здесь (String) - это дополнительная информация, которая связанная со значением km. Это означает, что связанное значение km может быть только строкой.

Теперь, вот так мы присваиваем связанное значение значению перечисления:

Distance.km("Metric System")

Здесь "Metric System" — это значение, связанное со значением km.

Пример: перечисление Swift со связанными значениями:

enum Distance {

    // associate value
    case km(String)
    case miles(String)
}

// pass string value to km
var dist1 = Distance.km("Metric System")
print(dist1)

// pass string value to miles
var dist2 = Distance.miles("Imperial System")
print(dist2)

// Output
// km("Metric System")
// miles("Imperial System")

В приведенном выше примере было создано перечисление Distance со следующими связанными значениями:

  • "Metric System" присваивается значению km;
  • "Imperial System" присваивается значению miles.

Несколько связанных значений

В Swift также можно связать несколько значений с одним значением перечисления. Например:

enum Marks {

    // associate multiple Double values
    case gpa(Double, Double, Double)

    // associate multiple String values
    case grade(String, String, String)
}

// pass three Double values to gpa
var marks1 = Marks.gpa(3.6, 2.9, 3.8)
print(marks1)

// pass three string values to grade
var marks2 = Marks.grade("A", "B", "C")
print(marks2)

// Output
// gpa(3.6, 2.9, 3.8)
// grade("A", "B", "C")

Здесь мы создали перечисление с двумя значениями: gpa и grade. Обратите внимание на значения перечисления,

// associate multiple Double values
case gpa(Double, Double, Double)

// associate multiple String values
case grade(String, String, String)

Этот код указывает на то, что необходимо связать три значения Double и String с gpa и grade соответственно. Эти связанные значения также называются кортежами.

Итак, к gpa было привязано 3.6, 2.9, 3.8.

// pass three integer values to gpa
var marks1 = Marks.gpa(3.5, 2.9, 3.8)

И также было привязано "A", "B", "C" к grade.

// pass three string values to grade
var marks2 = Marks.grade("A", "B", "C")

Именованные связанные значения (Named Associated Values)

В Swift также можно предоставить имя связанному значению, чтобы сделать код более читабельным. Например:

enum Pizza {
    case small(inches: Int)
}

Здесь предоставили имя inches для связанного значения.

Это дает больше смысла при чтении именованного связанного значения вместо

case small(Int)

Пример именованного связанного значения:

enum Pizza {

    // named associated value
    case small(inches: Int)
    case medium(inches: Int)
    case large(inches: Int)
}

// pass value using provided names
var pizza1 = Pizza.medium(inches: 12)
print(pizza1)

// Output
// medium(inches: 12)

В приведенном выше примере было предоставлено имя inches для связанного значения.

При передаче связанного значения в работу было использовано предоставленное имя:

Pizza.medium(inches: 12)

Связанные значения и оператор switch (Enum Associated Values and Switch Statement)

Можно использовать оператор switch для сопоставления значений, связанных с перечислением. Например:

enum Mercedes {

    case sedan(height: Double)
    case suv(height: Double)
    case roadster(height: Double)
}

var choice = Mercedes.suv(height: 5.4)

switch(choice) {
    case let .sedan(height):
        print("Height:", height)

    case let .suv(height):
        print("Height:", height)

    case let .roadster(height):
        print("Height:", height)
}

// Output
// Height: 5.4

В приведенном выше примере был использован оператор switch для сопоставления значений, связанных с перечислением. Обратите внимание на объявления переменной,

var choice = Mercedes.suv(height: 5.4)

Здесь привязали значение 5.4 к внедорожнику и присвоили ему выбор переменной перечисления.

Затем использовали выбор внутри оператора switch, чтобы сравнить регистр enum со значением каждого оператора case. Обратите внимание на оператор case в switch:

case let .suv(height):

Здесь let связывает связанное значение с переменной height, которую можно использовать в теле case.

Поскольку значение совпадает с case let .suv, оператор внутри case выполняется.

Еще пример. Пусть перечисление представляет игрового персонажа:

enum Person{
    case Human(String, Int)
    case Elf(String)
	case Gnom(String)
}

Здесь перечисление определяет три возможных значения - трёх игровых персонажей: человека (Human), эльфа (Elf) и гнома (Gnom). Однако у человека можно задать два параметра: имя (String) и количество жизней (Int). А у эльфа и гнома нужен только один параметр - имя (String).

То есть в данном случае значение Person.Human будет ассоциировано с двумя значениями String и Int, а значение Person.Elf и Person.Gnom - с одним значением типа String:

var heroOne = Person.Human("Trogvar", 5)
var heroTwo = Person.Elf("Feonor")

С помощью конструкции switch также можно определить значение объекта:

switch(heroOne){
    case .Human: 
        print("Вы играете человеком")
    case .Elf:
        print("Вы играете эльфом")
    case .Gnom:
        print("Вы играете гномом")
}

// Output
// Вы играете человеком

Но при необходимости также можем получить ассоциированные значения:

switch(heroOne){
    case .Human(let name, let lives): 
        print("Вы играете человеком. Имя: \(name), количество жизней: \(lives)")
    case .Elf(let name):
        print("Вы играете эльфом. Имя: \(name)")
    case .Gnom:
        print("Вы играете гномом")
}

// Output
// Вы играете человеком. Имя: Trogvar, количество жизней: 5

Для получения значений можно определить константы или переменные, в которые будут передаваться ассоциированные значения.

Ещё пример. Представьте, что создаем приложение для ролевой игры (RPG). Игрок может добавить в свой инвентарь несколько типов предметов. Каждый предмет имеет разные свойства. Например:

  • У оружия есть очки урона и вес.
  • У еды есть очки здоровья.
  • У брони есть очки защиты, вес и состояние.

Вот как это выглядит с ассоциированными значениями:

enum Item {
    case weapon(Int, Int)
    case food(Int)
    case armor(Int, Int, Double)
}
 
var sword = Item.weapon(10, 5)

В каждом case перечисления указываем дополнительные типы в круглых скобках, которые называются кортежами. Можно присвоить им любое значение.

Далее можно написать функцию для использования определенного предмета из инвентаря игрока:

func use(item: Item) {
    switch item {
    case .weapon(let hitPoints, _):
        player.attack(hitPoints)
    case .food(let health):
        player.health += health
    case .armor(let damageThreshold, let weight, let condition):
        player.damageThreshold = Double(damageThreshold) * condition
    }
}

В приведенном выше коде - разбиваем значения кортежей на отдельные константы. К примеру, константа hitPoints соответствует первому значению Int для case weapon(Int, Int).

Если хотите получить доступ ко всем ассоциированным значениям, можно использовать одиночный let:

switch item {
    case let .armor(damageThreshold, weight, condition):
        player.damageThreshold = ···
}

Также можно сопоставлять ассоциированные значения:

let item = Item.armor(15, 10, 0.75)
 
switch item {
case let .armor(damageThreshold, weight, condition) where weight < 10:
    print("DT = \(Double(damageThreshold) * condition)")
case .armor:
    print("Броня слишком тяжелая!")
default:
    print("Это не броня.")
}

// Output 
// Броня слишком тяжелая!

В приведенном выше коде используется ключевое слово where для сопоставления .armor случаев, когда weight меньше 10. Во втором случае мы сопоставляем любой .armor случай, который не соответствует первому, то есть любой, для которого weight больше или равен 10.

В Swift часто встречаются перечисления с ассоциированными значениями. Возьмем, к примеру, перечисление Result для популярной сетевой библиотеки “Alamofire”.

public enum Result<Value> {
    case success(Value)
    case failure(Error)
 
    ···
 
    public var value: Value? {
        switch self {
        case .success(let value):
            return value
        case .failure:
            return nil
        }
    }
}

В перечислении Result используется дженерик Value и протокол Error для представления двух возможных состояний: результат успешного или неудачного запроса. Когда запрос выполнен успешно, то можно получить Value, а когда он потерпел неудачу, то можно получить Error.

Вычисляемое свойство value перечисления Result возвращает опциональное значение Value?. Используется оператор switch для возврата значения или “nil”.


Перечисления с исходными значениями (чистыми значениями, raw Value)

В Swift мы также можем присваивать значения каждому случаю перечисления. Например:

enum Size: Int {
    case small = 10
    case medium = 12
}

Здесь были присвоены значения 10 и 12 для значений small и medium в перечислении соответственно. Эти значения называются исходными значениями (raw value).

Внимание. После имени перечисления Size через двоеточие указан тип данных Int, чтоб указать, что для значений перечисления в качестве исходных значений можно указывать только целочисленные значения.

Доступ к исходным значениям перечисления

Чтобы получить доступ к исходному значению перечисления необходимо использовать свойство rawValue у значения перечисления. Например:

// access raw value 
Size.small.rawValue

Здесь получили доступ к исходному значению для small.

Пример перечисления с исходными значениями:

enum Size : Int {
    case small = 10
    case medium = 12
    case large = 14
}

// access raw value of case
var result = Size.small.rawValue
print(result)

// Output
// 10

В приведенном выше примере было создано перечисление с именем Size, которое имеет исходные значение по типу Int.

Здесь был получен доступ к значению small, используя свойство rawValue.

Примечание. Исходные значения могут быть строками, символами, целыми числами, числами с плавающей точкой. Каждое исходное значение должно быть уникальным при его объявлении.

Вот пример перечисления, члены которого хранят исходные значения ASCII:

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

Исходные значения перечисления ASCIIControlCharacter определены как тип Character, и им присвоены распространенные контрольные символы ASCII.

Заметка. Исходные значения - это не то же самое, что ассоциативные значения. Исходные значения устанавливаются в качестве дефолтных значений (значений по умолчанию), когда в первый раз определяете перечисление в коде, как три ASCII кода выше. Исходное значение для конкретного кейса перечисления всегда одно и то же. Ассоциативные значения устанавливаются при создании новой константы или переменной, основываясь на одном из кейсов перечисления, и могут быть разными каждый раз, когда вы делаете это.

Неявно установленные исходные значения

Если работаете с перечислениями, которые хранят целочисленные или строковые исходные значения, то не нужно явно присваивать исходные значения для каждого конкретного кейса. Swift автоматически сделает это за вас.

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

enum Planet: Int {
    case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}
var planetFirst = Planet.mercury
var planetFirstNum = planetFirst.rawValue

print(planetFirst)
print(planetFirstNum)

// Output
// mercury
// 0

Теперь уточним ранее указанное перечисление Planet, с целочисленными исходными значениями для представления удаленности каждой планеты от солнца:

enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}
var planetFirst = Planet.mercury
var planetFirstNum = planetFirst.rawValue

print(planetFirst)
print(planetFirstNum)

// Output
// mercury
// 1

В приведенном выше примере, Planet.mercury имеет явное исходное значение 1, Planet.venus имеет неявное исходное значение 2, и так далее.

Изменение в исходных значениях с указанного элемента перечисления и дальше, до конца перечисления.

enum Planet: Int {
    case mercury = 1, venus, earth = 30, mars, jupiter, saturn, uranus, neptune
}
var planetThird = Planet.earth
var planetThirdNum = planetThird.rawValue

print(planetThird)
print(planetThirdNum)

// Output
// earth
// 30

var planetFour = Planet.mars
var planetFourNum = planetFour.rawValue

print(planetFour)
print(planetFourNum)

// Output
// mars
// 31

В этом примере было переопределено исходное значение для элемента earth в перечислении Planet. И исходные значения в последующих элементах перечисления тоже поменялись. Для элемента mars исходное значение стало 31, и для neptune - 35.

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

Ниже пример с четырьмя сторонами света:

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

var pathOne = CompassPoint.north
var pathOneName = pathOne.rawValue

print(pathOne)
print(pathOneName)

// Output
// north
// north

var pathTwo = CompassPoint.south
var pathTwoName = pathTwo.rawValue

print(pathTwo)
print(pathTwoName)

// Output
// south
// south

В приведенном выше примере, CompassPoint.north имеет неявное исходное значение "north", CompassPoint.south имеет неявное исходное значение "south" и так далее. Заменим первое:

enum CompassPoint: String {
    case north = "Север"
    case south
    case east
    case west
}

var pathOne = CompassPoint.north
var pathOneName = pathOne.rawValue

print(pathOne)
print(pathOneName)

// Output
// north
// Север

var pathTwo = CompassPoint.south
var pathTwoName = pathTwo.rawValue

print(pathTwo)
print(pathTwoName)

// Output
// south
// south

Здесь явно указано исходное значение "Север" для элемента north в перечислении. Для остальных элементов в перечислении - исходные значения не поменялись и так и остались текстом имени этих кейсов.

Инициализация через исходное значение

Если объявить перечисление вместе с типом исходного значения, то перечисление автоматически получает инициализатор, который берет значение типа исходного значения (как параметр rawValue) и возвращает либо член перечисления либо nil. Можно использовать этот инициализатор, чтобы попытаться создать новый экземпляр перечисления.

В этом примере Uranus инициализируется через его исходное значение 7:

enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}
let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet имеет тип Planet? и его значение равно Planet.uranus

Так как не все возможные значения Int будут в данном перечислении, то инициализаторы исходных значений всегда возвращают опциональный член перечисления. В этом примере possiblePlanet типа Planet? или “опциональный Planet”.

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

Больше про с инициализаторы можно прочитать в отдельных статьях. См. ссылки внизу.

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

enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}
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)")
}

// Output
// There isn't a planet at position 11

Этот пример использует опциональное связывание для попытки добраться до Planet с исходным значением 11. Выражение if let somePlanet = Planet(rawValue: 11) создает опциональную Planet и устанавливает значение somePlanet опциональной Planet, если она может быть восстановлена.

В этом случае, невозможно добраться до планеты с позицией 11. Таким образом срабатывает ветка else в условной конструкции и выводится соответствующее сообщение.

Исходные значения или связанные значения

В Swift исходные значения — это предопределенные постоянные значения, предоставляемые каждому значению перечисления. Например,

enum Vehicle: String {
    case car = "Four Wheeler"
    case bike = "Two Wheeler"
}

Здесь мы предоставили исходные значения: "Four Wheeler" («Четырехколесный») и "Two Wheeler" («Двухколесный») для автомобиля и велосипеда соответственно.

Однако, связанные значения больше похожи на переменные, связанные со значениями перечисления. Например:

enum Marks {
    case grade(String)
    case gpa(Double)
}

Здесь grade и gpa имеют связанные значения типа String и Double соответственно.

И связанное значение присваивается значению перечисления при создании переменной.

var marks = Marks.grade("A")

Заметка. Перечисление не может иметь как исходные значения, так и связанные значения одновременно. Исходные значения перечисления должны иметь один и тот же тип данных. А связанные значения могут быть любого типа.


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

Рекурсивные перечисления - это такие перечисления, экземпляры которого являются ассоциативным значением одного или более кейсов перечисления. Вы обозначаете такие кейсы перечисления при помощи ключевого слова 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))

// Output
// 18

Эта функция вычисляет простое число, просто возвращая ассоциативное значение. Она вычисляет сложение или умножение, вычисляя выражение по левую сторону, затем по правую сторону, затем складывает или умножает их.


Методы перечислений

Как классы и структуры, перечисления могут определять методы. Например:

enum DayOfWeek: Int {

    case Monday = 1, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
     
    func getCurrentDay() -> String {
        return DayOfWeek.getDay(number: rawValue)
    }

    static func getDay(number: Int) -> String {
        switch number {             
            case 1:
                return "Понедельник"
            case 2:
                return "Вторник"
            case 3:
                return "Среда"
            case 4:
                return "Четверг"
            case 5:
                return "Пятница"
            case 6:
                return "Суббота"
            case 7:
                return "Воскресенье"
            default:
                return "undefined"
        }
    }
}

var someDay: DayOfWeek = DayOfWeek.Sunday
print(someDay.getCurrentDay())

// Output
// Воскресенье

var secondDay = DayOfWeek.getDay(number: 2) // Вторник
print(secondDay)

// Output
// Вторник

В этом примере:

  • в перечислении DayOfWeek определён метод типа getDay(number: Int) -> String, который возвращает строку с названием дня недели в зависимости от введённого номера;
  • в перечислении DayOfWeek определён метод getCurrentDay() -> String, который возвращает строку с названием дня недели текущего экземпляра перечисления. Для этого, данный метод вызывает метод типа getDay(number: Int) -> String, в который передаёт в качестве входящего параметра number исходное значение текущего экземпляра перечисления;
  • была создана переменная someDay со значением DayOfWeek.Sunday;
  • у переменной someDay вызван метод getCurrentDay() и результат вызова был выведен в консоль;
  • была создана переменная secondDay по типу String как результат вызова метода типа getDay(number: 2) перечисления DayOfWeek и получила значение Вторник.

Ещё пример:

enum SwitchState {
    case off, low, high
    
    mutating func nextState() {
        switch self {
            case .off:
                self = .low
            case .low:
                self = .high
            case .high:
                self = .off
        }
        print("Switched to state: \(self)")
    }
}

var state: SwitchState = .off
print(state)

// Output
// off

state.nextState()
print(state)

// Output
// Switched to state: low
// low

state.nextState()
print(state)

// Output
// Switched to state: high
// high

state.nextState()
print(state)

// Output
// Switched to state: off
// off

В этом примере, метод nextState() в перечислении SwitchState меняет значение экземпляра перечисления state.

Следует обратить внимание на то, что данный метод меняет значение экземпляра перечисления. Для этого, в операторе switch указано, что изменение было пременено для экземпляра - self. И, если метод меняет значение экземпляра, то перед объявлением метода указано ключевое слово mutating.

Свойства перечислений

Перечисления также могут иметь свойства. Но это не могут быть хранимые свойства, а только вычисляемые свойства:

enum DayOfWeek: Int{
     
    case Monday = 1, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday

    var label : String {
        switch self {
            case .Monday: return "Понедельник"
            case .Tuesday: return "Вторник"
            case .Wednesday: return "Среда"
            case .Thursday: return "Четверг"
            case .Friday: return "Пятница"
            case .Saturday: return "Суббота"
            case .Sunday: return "Воскресенье"
        }
    }
}

let day1 = DayOfWeek.Monday
print(day1.label)

// Output
// Понедельник

print(DayOfWeek.Friday.label)

// Output
// Пятница

В данном случае свойство label автоматически вычисляется на основании значения текущего объекта перечисления. Текущий объект перечисления можно получить с помощью ключевого слова self.

Инициализаторы в перечислениях

И перечисления также могут иметь иницализаторы. В перечислениях используются проваливающийся инициализатор:

enum DayOfWeek: Int{
     
    case Monday = 1, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
    
	init?(_ val:String) {     
        switch val {
        case "Понедельник"  : self = .Monday
        case "Вторник": self = .Tuesday
        case "Среда": self = .Wednesday
        case "Четверг": self = .Thursday
        case "Пятница": self = .Friday
        case "Суббота": self = .Saturday
        case "Воскресенье": self = .Sunday
        case _ : return nil
        }
    }
}
let day1 = DayOfWeek("Пятница")
print(day1!.rawValue)

// Output
//  5

В данном случае инициализатор принимает название дня недели и на его основании устанавливает значение текущего объекта. Если переданное название не распознано, то инициализатор возвращает nil.

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

Для того чтобы справиться с условиями инициализации, которые могут провалиться, следует определить один или несколько проваливающихся инициализаторов как часть определения класса, структуры или перечисления. Можно написать проваливающийся инициализатор поместив вопросительный знак после ключевого слова init (init?).

Больше про инициализаторы можно прочитать в отдельной статье. См. ссылки внизу.


Еще полезные ссылки

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

Ссылки на официальную документацию: