Перейти к содержимому

Enumerated swift что это

  • автор:

Enumerated swift что это

Перечисление (enumeration) определяет общий тип для группы связанных значений. Причем сами объединенные в перечисление значения могут представлять любой тип — число, строку и так далее.

Для создания перечисления используется ключевое слово enum :

enum Season

Каждое отдельное значение в перечислении указывается после оператора case . В данном случае перечисление называется Season и представляет времена года и имеет четыре значения. То есть фактически Season представляет новый тип данных.

Также допустима сокращенная форма перечисления значений:

enum Season

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

var currentSeason = Season.Spring

Здесь переменная currentSeason представляет тип Season . Впоследствии мы можем присвоить этой переменной другое значение из Season:

var currentSeason = .Summer

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

let lastSeason: Season lastSeason = Season.Winter

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

enum Season < case Winter, Spring, Summer, Autumn >let currentSeason = Season.Spring switch(currentSeason)

Ассоциированные значения

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

Итак, пусть у нас перечисление представляет игрового персонажа:

enum Person

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

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

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

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

switch(hero)

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

switch(hero)

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

Чистые значения перечислений

Кроме ассоциированных значений члены перечисления могут иметь чистые значения (raw values). Например, пусть у нас есть перечисление, представляющее флагманы различных производителей:

enum Flagman: String

При определении чистых значений нам надо указать их тип. В данном случае типом будет выступать тип String. Затем в программе мы сможем получить эти чистые значения с помощью свойства rawValue :

var myPhone = Flagman.Apple print(myPhone) // Apple print(myPhone.rawValue) // iPhone X

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

Если тип — String, то чистые значения будут представлять строковое представление элементов перечисления:

enum Flagman: String < case Samsung, Apple, Microsoft, Google >var myPhone = Flagman.Apple print(myPhone) // Apple print(myPhone.rawValue) // Apple

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

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

enum DayOfWeek: Int < case Monday=1, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday >var currentDay = DayOfWeek.Wednesday print(currentDay) // Wednesday print(currentDay.rawValue) // 3

Первый элемент перечисления Monday=1 задает начальное значение для элементов перечисления. Если же мы не укажем его, то начальным значением будет 0.

Используя чистое значение, мы можем получить элемент перечисления:

enum DayOfWeek: Int < case Monday=1, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday >var currentDay = DayOfWeek(rawValue: 7) // Optional(DayOfWeek.Sunday) print(currentDay!)

В данном случае мы пытаемся получить элемент перечисления, который имеет чистое значение 7. Но данная операция возвращает не просто элемент перечисления, а объект Optional, то есть такой объект, который может иметь конкретное значение, а может иметь значение nil (отсутствие значения).

И если мы попытаемся получить, например, элемент с чистым значением 8, то мы получим nil. Поэтому мы можем применять условное выражение if для проверки полученного значения перед его использованием:

if let day = DayOfWeek(rawValue: 8)

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

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

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 someDay.getCurrentDay() // Воскресенье var secondDay = DayOfWeek.getDay(number: 2) // Вторник

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

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

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) // Понедельник print(DayOfWeek.Friday.label) // Пятница

В данном случае свойство 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) // 5

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

Перечисления (enumeration) в Swift

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

Задание 1

1) В конце главы про «Перечисления» мы с вами начали создавать перечисление ArithmeticExpression, позволяющее выполнять арифметические операции. Сейчас перед вами стоит задача доработать данное перечисление, чтобы оно могло производить любые арифметические операции, включая сложение, вычитание, умножение, деление и возведение в степень.
2) Проверьте работу перечисления на произвольных примерах

Правильный ответ

enum ArithmeticExpression < // указатель на конкретное значение case number( Int ) // указатель на операцию сложения indirect case addition( ArithmeticExpression, ArithmeticExpression ) // указатель на операцию вычитания indirect case subtraction( ArithmeticExpression, ArithmeticExpression ) // указатель на операцию умножения indirect case multiplaction( ArithmeticExpression, ArithmeticExpression ) // указатель на операцию деления indirect case division( ArithmeticExpression, ArithmeticExpression ) // указатель на операцию возведения в степень indirect case pow( ArithmeticExpression, ArithmeticExpression ) // метод, проводящий операцию func evaluate( _ expression: ArithmeticExpression? = nil ) ->Int< // определение типа операнда (значение или операция) switch expression ?? self< case let .number( value ): return value case let .addition( valueLeft, valueRight ): return self.evaluate( valueLeft )+self.evaluate( valueRight ) case .subtraction( let valueLeft, let valueRight ): return self.evaluate( valueLeft )-self.evaluate( valueRight ) case .multiplaction( let valueLeft, let valueRight ): return self.evaluate( valueLeft )*self.evaluate( valueRight ) case .division( let valueLeft, let valueRight ): return self.evaluate( valueLeft )/self.evaluate( valueRight ) case .pow( let valueLeft, let valueRight ): if self.evaluate( valueRight ) == 0 < return 1 >else < var result = self.evaluate( valueLeft ) for _ in 1..return result > > > > var hardExpr = ArithmeticExpression.pow( .number(3), .number(3) ) hardExpr.evaluate() // 27 hardExpr = ArithmeticExpression.multiplaction( .number(3), .number(3) ) hardExpr.evaluate() // 9

Задание 2

Корректно ли будет выполнен данный код? Если нет, то какие правки необходимо в него внести, чтобы исправить ошибки?

enum Seasons < case winter, spring, summer, autumn >let whiteSeason = Seasons.winter var greenSeason: Seasons = .spring greenSeason = .summer var orangeSeason = .autumn var bestSeason = whiteSeason bestSeason = .summer

Правильный ответ

Ошибка в строке 7. У переменной orangeSeason не указан тип данных

Доступ закрыт

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

4 Comments

Задание 1: enum ArithmeticExpression <
// указатель на конкретное значение
case number( Int )
// указатель на операцию сложения
indirect case addition( ArithmeticExpression, ArithmeticExpression )
// указатель на операцию вычитания
indirect case subtraction( ArithmeticExpression, ArithmeticExpression )
// метод, проводящий операцию умножения
indirect case multiplication( ArithmeticExpression, ArithmeticExpression )
// метод, проводящий операцию деления
indirect case division( ArithmeticExpression, ArithmeticExpression )
// метод, проводящий операцию возведения в корень
indirect case pow1( ArithmeticExpression, ArithmeticExpression )
// метод, проводящий операцию
func evaluate( _ expression: ArithmeticExpression? = nil ) -> Int <
// определение типа операнда (значение или операция)
switch expression ?? self <
case let .number( value ):
return value
case let .addition( valueLeft, valueRight ):
return self.evaluate( valueLeft ) + self.evaluate ( valueRight )
case .subtraction( let valueLeft, let valueRight ):
return self.evaluate( valueLeft ) — self.evaluate ( valueRight )
case .multiplication( let valueLeft, let valueRight ):
return self.evaluate( valueLeft ) * self.evaluate ( valueRight )
case .division( let valueLeft, let valueRight ):
return self.evaluate( valueLeft ) / self.evaluate ( valueRight )
case .pow1( let valueLeft, let valueRight ):
return Int (pow (Double (self.evaluate ( valueLeft )), Double (self.evaluate ( valueRight ))))
>
>
>
var hardExpr = ArithmeticExpression.addition( .number(20), .division( .number(10), .number(5) ) )
hardExpr.evaluate() // 22 var hardExpr1 = ArithmeticExpression.pow1( .number(10), .number(2) )
hardExpr1.evaluate() // 100

Согласен с Vlados. И в ответе на Задание 1 — в for in в теле функции должно стоять result *= self.evalute(valueLeft), что бы перемножал определенное количество раз self.evalute(valueRight) на число, которое должно ВОЗВОВОДИТЬСЯ в степень self.evalute(valueLeft). А в ответе получается наоборот.

enum Day: String <
case monday = «Понедельник»
case tuesday = «Вторник»
case wednesday = «Среда»
case thursday = «Четверг»
case friday = «Пятница»
case saturday = «Суббота»
case sunday = «Воскресенье»
var label: String
>

Добавить комментарий Отменить ответ

Для отправки комментария вам необходимо авторизоваться.

Немного о перечислениях в Swift

Не секрет, что на сегодняшний день Swift обладает одними из самых (возможно самыми) функциональными и гибкими в настройке перечислениями. Каждый Swift-разработчик может подтвердить, что работать с ними довольно приятно и удобно. Однако, мало кто задумывается как это устроено внутри.

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

Может показаться, что за подобную функциональность обязательно придётся заплатить либо производительностью, либо памятью. На самом деле, чтобы максимально избежать подобных издержек, под капотом в языке имеется шесть разных реализаций перечислений. Решение о том какую из них использовать, принимается автоматически во время компиляции на этапе формирования промежуточного представления кода для llvm, ну и иногда в рантайме. Таким образом, вы можете быть уверены в том, что при использовании классических С-подобных перечислений, компилятор не потащит вместе с ними весь скрытый багаж функциональности для работы с ассоциативными значениями, и наоборот, если вы работаете с перечислениями с ассоциативными значениями, то там уж всё будет оптимизировано как надо.

Диаграмма наследования реализаций перечислений

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

1. Singleton enums

Самый примитивный случай — перечисление с нулевым или одним, единственным вариантом:

enum Empty < >enum Singleton < case A >enum RawSingleton: String

Такое перечисление является вырожденным типом и не требует выделения памяти. Если у единственного варианта есть raw-значение, то при обращении к нему в коде, оно будет браться из метаданных типа. По сути, это будет аналогом глобальной константы. Однако, при наличии ассоциативного значения расклад меняется:

enum Singleton

Очевидно, что одинаковые варианты в общем случае не эквивалентны между собой (так-как в каждом случае хранимое ассоциативное значение будет уникально), следовательно под каждое созданное значение данного перечисления, будет выделяться объём памяти соответствующий типу ассоциативной нагрузки. Столько же, сколько необходимо «обычной» переменной данного типа, если бы она не была завёрнута в перечисление.

let a = RawSingleton.A // zero size let b = Singleton.number(5) // size: 8 bytes

2. No-payload strategy

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

В языке имеется две реализации для таких перечислений: Swift-native и C-compatable. На данный момент вторая в большей степени является частным случаем первой, поэтому обсуждать её отдельно мы не будем. По всей видимости, она существует для налаживания интеропа с С++.

Говоря о классическом перечислении, на ум сразу приходит C-подобные конструкции, где каждый вариант представлен просто порядковым числом. Также это работает и в Swift. Дополнительно, как и во многих других языках, каждому варианту можно назначить определённое raw-значение:

enum ImplStrategy: String < // size: 4 bits case singleton = "Singleton" // 0b0000 case noPayload = "No-payload" // 0b0001 case singlePayload = "Single-payload" // 0b0010 case manyPayload = "Many-payload" // 0b0011 case resilent = "Resilent" // 0b0100 >ImplStrategy.resilent == ImplStrategy(rawValue: "Resilent") // true

Процесс выделения памяти прост и прямолинеен:

  1. Компилятор последовательно, начиная с нуля, назначает каждому варианту целочисленное значение, оно же дискриминатор.
  2. Далее определяется минимальная необходимая память из ряда 2^n в в которой могут поместиться все дискриминаторы.
  3. Каждому дискриминатору назначается соответствующее raw-значение, которое затем размещается в метаданных этого типа и всегда доступно по запросу. Например, при создании значения перечисления через .init(rawValue: String) .

Extra inhabitants

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

Не трудно заметить что для представления всех дискриминаторов достаточно трёх бит. Из всего множества значений 0b000-0b111 которые в этот объём памяти можно записать, только подмножество 0b000-0b100 будет представлять собой корректные значения (valid values) для данного перечисления. Подмножество же незадействованных битовых последовательностей, 0b101-0b111 , является набором некорректных значений и носит название extra inhabitants (за неимением какого-то устойчивого перевода для этого термина, далее мы так и будем к нему обращаться).

Важно понимать что свой набор extra inhabitants может иметь любой тип в языке, а не только перечисления. Типобезопасность языка не позволит случиться ситуациям когда в участке памяти, выделенном под значение какого-либо типа, будет записан один из его extra inhabitants.

Итак, с extra inhabitants разобрались, теперь немного о запасных битах (spare bits). Как мы отметили, явно видно что для хранения любого варианта перечисления достаточно трёх бит памяти, однако при генерации промежуточного кода, выделено будет не три бита, а четыре. Связано это с тем, что при работе с размерностями отличными от «нормальных», LLVM начинает работать медленней и не так стабильно. Поэтому, после определения минимально необходимиго числа бит, это значение округляется вверх до ближайшей величины в ряде 2^n с которыми LLVM работать удобней. Выделенные, но не используемые, биты именуются «запасными» и также присуще любому типу в Swift, не только перечислениям.
В довесок ко всему, минимальная выделяемая память для размещения какого-либо объекта в Swift — один байт, таким образом в нашем примере мы получим не один, а целых пять запасных битов для типа ImplStrategy .

В зависимости от ситуации, запасные биты могут использоваться в разных целях, например для формирования дополнительных extra inhabitants, если в этом есть нужда, в итоге наш тип ImplStrategy потенциально имеет следующий набор extra inhabitants: 0x04-0xFF .

  1. Запасные биты — по определённым причинам выделяемая, но никогда не участвующая в формировании корректных значений определённого типа, память.
  2. Extra inhabitants — все комбинации битовых последовательностей, не являющиеся корректными значениями определённого типа, но потенциально размещаемые в выделяемую под значение этого типа память.
  3. Множества корректных значений и extra inhabitants в рамках одного типа никогда не пересекаются.
  4. Каждый тип в языке имеет свой набор extra inhabitants и запасных бит известный уже на момент компиляции (если это не шаблонный, a.k.a. generic, тип).

3. Single-payload strategy

Наконец-то мы подбираемся к более интересным случаям. Для перечислений с единственной нагрузкой (payload, оно же ассоциативное значение) плюс одним или несколькими пустыми вариантами, используется своя, отдельная реализация.

enum FontSize

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

Обратите внимание, что теперь одна ячейка на изображении это уже полноценный байт из 8 бит. Как мы видим, для тегового бита выделяется один дополнительный байт. Общий размер выделяемой памяти с учётом нагрузки равен 9 байтам, что в общем-то не страшно, но приводит к ряду неприятных последствий. Так как значение не помещается в одно машинное слово, то считывание и запись осуществляются уже за две операции. Так же, исходя из величин stride = 16 и alignment = 8 , мы понимаем что на каждое хранимое в памяти значение типа FontSize будет приходиться 7 байт не используемой памяти. Если мы планируем хранить большой массив таких данных, то ~46% памяти выделенной под массив останется незадействованной (на хабре есть отличная статья на эту тему).

Не приятно что один теговый бит так радикально портит нам общую картину, однако это не такая уж необычная ситуация. Вспомним, что опционал в Swift это перечисление, и оно как раз такого типа, так-что любой условный Int? автоматически будет страдать от тех же болячек. К счастью, в ряде случаев компилятор постарается оптимизировать используемую память и отказаться от использования тегового бита. Если тип данных в ассоциативном значении имеет достаточное количество extra inhabitants, они будут задействованы как дискриминаторы для пустых вариантов.

Рассмотрим такой случай на конкретном примере. В Swift предполагается что в начале адресного пространства программы всегда есть неразмеченная область памяти размером минимум 4КБ. Иными словами, и на стадии компиляции и в рантайме, заранее известно что любая ссылка на эту область будет некорректной, а значит все эти значения (адреса 0x0-0xFFF ) для любых объектов типа ссылки являются её extra inhabitans.

class Director < . >enum CardBuilder < case profile // 0x0 case document // 0x1 case nomenclature // 0x2 case custom(Director) // 0x1000 - 0xFFFFFFFF_FFFFFFFF >

Таким образом, используя extra enhabitans типа ассоциативного значения и заранее зная что это перечисление с одним ассоциативным вариантом, всегда можно однозначно идентифицировать какой вариант записан в выделеный участок памяти даже без тегового бита:

Все оставшиеся не задействованными extra enhabitans ( 0x003 — 0xFFF ) наследуются объявленым перечислением CardBuilder и становятся его собственными extra inhabitants, которые в будущем могут быть использованы аналогичным образом. Например, если CardBuilder будет ассоциативным значением в другом перечислении.

4. Many-payloads strategy

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

enum State

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

Опять же, нам интересно как Swift это всё оптимизирует и оптимизирует ли вообще. Так как мы должны суметь различить между собой разные варианты с ассоциативными значениями, трюк с использованием extra inhabitants в качестве дискриминаторов для пустых вариантов уже не сработает. В этот момент в дело вступают запасные биты.

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

class Storage < . >class Service < . >class Item < . >enum ItemProvider

Ранее мы уже рассматривали случай с экземпляром класса в качестве ассоциативного значения. Опытные товарищи заметили определённую некорректность этого примера. Известно что в 64 битных системах под указатель выделяется 8 байт памяти, однако для хранения адреса реально используется лишь 48 из 64 бит (иногда 52). Может показаться что оставшиеся 16 бит мы можем рассматривать как запасные, однако разные платформы накладывают различные ограничения на их применение. В зависимости от платформы и языка, они могут быть использованы для хранения различной дополнительной информации (Top byte ignore, Memory Tagging, Pointer Authentication), либо вообще быть запрещены.

К сожалению мне не удалось найти официальных данных как это устроено на процессорах Apple. Судя по комментариям в исходном коде, а так-же некоторым in-situ экспериментам, указатели на native-Swift классы (в данном контексте в первую очередь подразумевается классы не насладующиеся от NSObject ) имеют как минимум 3 запасных бита. В целом вопрос об устройстве указателей довольно комплексный и уходит далеко за рамки данной статьи.

Так как в нашем примере все ассоциативные значения являются объектами типа ссылки, неудивительно что их запасные биты будут пересекаться. Ниже приведено изображение как это примерно выглядит на Apple Silicon:

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

5. Resilient strategy

Последняя реализация — ResilientEnumImplStrategy . Большая часть её методов переопределяет методы базового класса EnumImplStrategy заглушками, которые, как предполагается, никогда не будут вызваны. По сути сама эта реализация лишь временная заглушка назначаемая перечислениям на время компиляции программы, если вычислить их метаданные на этом этапе невозможно. Например, если это перечисление использует шаблонные типы Enum < . >, либо если в качестве ассоциативных значений стоят типы из внешних библиотек с включенной, по неведомой причине, опцией resilience. Тем не менее, во время рантайма метаданные этих типов в любом случае будут вычислены и перечислению в итоге будет назначена одна из вышеописанных реализаций.

Заключение

Перечисления очень мощный и выразительный инструмент в Swift. Как мы увидели, внутри языка имеется целый набор различных реализаций, оптимизированных под каждую конкретную конструкцию перечисления. В данной статье мы рассмотрели основные принципы выделения памяти в этих реализациях, хотя и не задели множество нюансов. Например, не были затронуты случаи когда в качестве нагрузки выступает кортеж из нескольких значений, хотя там всё работает по схожим принципам.
На самом деле, чтобы писать хороший код, разработчику не обязательно всё это знать. Всё спроектировано так, чтобы не обременять нас подобным знанием. Понимание нюансов в реализации разных видов перечислений может сыграть роль разве что в редких случаях в server-side разработке, если вы работаете с большими объёмами данных. В остальном же это, по большей части, интересно с академической точки зрения, для желающих повысить общее понимание внутреннего устройства языка.

  • swift
  • enumeration
  • memory management
  • optimizations
  • Блог компании Тензор
  • Разработка под iOS
  • Разработка мобильных приложений
  • Swift

Перечисления (enum) в Swift: ассоциированные и чистые значения

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

Хорошим примером являются направления на компасе:

enum Compass  case north case east case south case west >

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

Проще всего рассматривать перечисления как структурированные списки связанных элементов. К примеру:

  • Цвета: красный, зеленый, синий, фиолетовый, желтый.
  • Вкусы мороженого: ваниль, шоколад, клубника.
  • Состояния аутентификации: .authenticated, .unauthenticated, .undetermined.
  • Состояния воспроизведения: .playing, .paused, .stoppped.

Синтаксис перечислений довольно прост:

  • Мы используем ключевое слово enum для объявления перечисления с используем конструкцию enum имя < тело >.
  • В теле перечислений мы включаем ряд элементов перечисления с помощью ключевого слова case. Варианты перечисления – это разные значения, которые у него есть, например, вышеупомянутые ароматы мороженого.
  • Перечисления могут иметь ассоциированные значения (associated types), а также чистые значения (raw value).

Мы можем определить все перечисления в одной строке:

case Weekday  case monday, tuesday, wednesday, thursday, friday, saturday, sunday >

Использовать перечисление довольно просто:

let today: Weekday = Weekday.monday

Приведенный выше код можно сократить:

let today: Weekday = .monday

Также можно использовать:

let today = Weekday.monday

Благодаря выводу типа нам не нужно каждый раз явно объявлять тип переменной или константы. Как только тип перечисления был объявлен, мы можем использовать более короткий синтаксис:

var direction: Compass = .north direction = .south

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

enum AuthenticationState  case authenticated case unauthenticated case undetermined >

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

Перечисления, состояния и switch

Перечисления эффективно комбинировать с оператором switch. Возможно, вы слышали о понятии «состояние» раньше. В разработке для iOS мы часто управляем состоянием различных параметров и взаимодействий в нашем приложении.

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

enum Emotion  case happy, sad, angry, scared, surprised >

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

switch robot.mood  case .angry: robot.destroyAllHumans() case .sad: robot.rust() // Робот плачет и ржавеет. case .happy: robot.play("happy.mp3") case default: print("Неизвестная эмоция.") >

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

Ассоциированные значения (associated values) в перечислениях

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

Представьте, что мы создаем приложение для ролевой игры (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("Это не броня.") > // Броня слишком тяжелая!

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

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

public enum ResultValue>  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 values) для перечислений

Вместо связанных значений перечисления могут иметь чистые значения:

enum Flavor: String  case vanilla = "vanilla" case strawberry = "strawberry" case chocolate = "chocolate" >

Каждый случай перечисления Flavor использует значения типа String.

Посмотрим, как можно работать с чистыми значениями:

let icecream = Flavor.vanilla print(icecream.rawValue) // vanilla

Мы также можем получить значение Flavor прямо из чистого значения. При этом инициализатор чистого значения возвращает опциональный параметр:

let icecream = Flavor(rawValue: "vanilla") print(icecream) // Optional(Flavor.vanilla)

Чистые значения также могут назначаться неявно:

enum Weekday: Int  case monday = 1, tuesday, wednesday, thursday, friday, saturday, sunday >

В приведенном выше примере исходное значение .sunday равно 7.

Для значений типа String это работает аналогично. Чистые значения будут представлять собой строковые значения элементов перечисления:

enum Compass: String  case north, east, south, west > let direction = Compass.west print(direction.rawValue == "west") // true

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *