Книга: Swift. Основы разработки приложений под iOS, iPadOS и macOS. 5-е изд. дополненное и переработанное
Назад: 21. Введение в объектно-ориентированное программирование
Дальше: 23. Структуры

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

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

22.1. Синтаксис перечислений

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

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

Листинг 22.1

var russianCurrency: String = "Rouble"

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

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

Листинг 22.2

var currencyUnit: [String] = ["Rouble", "Euro"]

var euroCurrency = currencyUnit[1]

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

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

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

Синтаксис

enum ИмяПеречисления {

    case значение1

    case значение2

...

}

• Значение — значение очередного члена перечисления, может быть произвольного типа данных.

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

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

ПРИМЕЧАНИЕ Объявляя перечисление, вы создаете новый тип данных.

Решим задачу указания типа денежной единицы с использованием перечислений (листинг 22.3). Перечисление подобно массиву. Оно содержит список значений, одно из которых мы можем присвоить некоторому параметру.

Листинг 22.3

enum CurrencyUnit {

    case rouble

    case euro

}

Несколько членов перечисления можно писать в одну строку через запятую (листинг 22.4).

Листинг 22.4

enum CurrencyUnit {

    case rouble, euro

}

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

Синтаксис

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

• Краткий синтаксис (точка и имя члена перечисления). При этом требуется явно указать тип данных.

var имяПараметра: ИмяПеречисления =.значение

• Полный синтаксис. При этом тип определяется неявно

var имяПараметра = ИмяПеречисления.значение

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

имяПараметра = .значение

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

В листинге 22.5 показаны примеры создания параметров и инициализации им членов перечислений.

Листинг 22.5

var roubleCurrency: CurrencyUnit = .rouble

var otherCurrency = CurrencyUnit.euro

// сменим значение одного параметра

otherCurrency = .rouble

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

ПРИМЕЧАНИЕ Члены перечисления не являются значениями какого-либо типа данных, например String или Int. Поэтому значения в следующих переменных currency1 и currency2 не эквивалентны:

var currency1 = CurrencyUnit.rouble

var currency2 = "rouble"

22.2. Ассоциированные параметры

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

Создадим новое усовершенствованное перечисление AdvancedCur­rencyUnit, основанное на CurrencyUnit, но имеющее ассоциированные параметры, с помощью которых можно указать список стран, в которых данная валюта используется, а также кратких наименований валюты (листинг 22.6).

Листинг 22.6

enum AdvancedCurrencyUnit {

    case rouble(сountries: [String], shortName: String)

    case euro(сountries: [String], shortName: String)

}

Параметр countries является массивом, так как валюта может использоваться не в одной, а в нескольких странах: например, евро используется на территории Европейского союза.

Теперь для того, чтобы создать переменную или константу типа AdvancedCurrencyUnit, необходимо указать значения для всех ассоциированных параметров (листинг 22.7).

Листинг 22.7

var euroCurrency: AdvancedCurrencyUnit =  .euro(сountries: ["German", "France"], shortName: "EUR")

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

Ассоциированные параметры могут различаться для каждого члена перечисления. Для демонстрации этого расширим возможности перечисления AdvancedCurrencyUnit, добавив в него новый член, описывающий доллар. При этом ассоциированные с ним параметры будут отличаться от параметров уже имеющихся членов. Как известно, доллар является национальной валютой большого количества стран: США, Австралии, Канады и т.д. По этой причине создадим еще одно перечисление, DollarCountries, и укажем его в качестве типа данных ассоциированного параметра нового члена перечисления AdvancedCurrencyUnit (листинг 22.8).

Листинг 22.8

// страны, использующие доллар

enum DollarCountries {

    case usa

    case canada

    case australia

}

// дополненное перечисление

enum AdvancedCurrencyUnit {

    case rouble(сountries: [String], shortName: String)

    case euro(сountries: [String], shortName: String)

    case dollar(nation: DollarCountries, shortName: String)

}

var dollarCurrency: AdvancedCurrencyUnit = .dollar( nation: .usa, shortName: "USD" )

Для параметра nation члена dollar перечисления Advanced­CurrencyUnit используется тип данных DollarCountries. Обратите внимание, что при инициализации значения этого параметра используется сокращенный синтаксис (.usa). Это связано с тем, что его тип данных уже задан при определении перечисления AdvancedCurrencyUnit.

22.3. Вложенные перечисления

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

Так как перечисление DollarCountries используется исключительно в перечислении AdvancedCurrencyUnit и создано для него, его можно перенести внутрь этого перечисления (листинг 22.9).

Листинг 22.9

enum AdvancedCurrencyUnit {

    enum DollarCountries {

        case usa

        case canada

        case australia

    }

    case rouble(сountries: [String], shortName: String)

    case euro(сountries: [String], shortName: String)

    case dollar(nation: DollarCountries, shortName: String)

}

Теперь перечисление DollarCountries обладает ограниченной областью видимости и доступно только через родительское перечисление. Можно сказать, что это подтип типа, или вложенный тип. Тем не менее при необходимости вы можете создать параметр, содержащий значение этого перечисления, и вне перечисления AdvancedCurrencyUnit (листинг 22.10).

Листинг 22.10

var australia: AdvancedCurrencyUnit.DollarCountries = .australia

Так как перечисление DollarCountries находится в пределах перечисления AdvancedCurrencyUnit, обращаться к нему необходимо как к свойству этого типа, то есть через точку.

ПРИМЕЧАНИЕ Мы уже встречались с вложенными типами при изучении словарей (Dictionary). Помните Dictionary<T1,T2>.Keys и Dictionary<T1,T2>.Values?

В очередной раз отмечу, насколько язык Swift удобен в использовании. После перемещения перечисления DollarCountries в AdvancedCurrencyUnit код продолжает работать, а Xcode дает корректные подсказки в окне автодополнения.

22.4. Оператор switch для перечислений

Для анализа и разбора значений перечислений можно использовать оператор switch.

Рассмотрим пример из листинга 22.11, в котором анализируется значение переменной типа AdvancedCurrencyUnit.

Листинг 22.11

switch dollarCurrency {

    case .rouble:

        print("Рубль")

    case let .euro(countries, shortname):

        print("Евро. Страны: \(countries). Краткое наименование: \(shortname)")

    case .dollar(let nation, let shortname):

        print("Доллар \(nation). Краткое наименование: \(shortname) ")

}

Консоль

Доллар usa. Краткое наименование: USD

В операторе switch описан каждый элемент перечисления Advanced­CurrencyUnit, поэтому использовать оператор default не обязательно. Доступ к ассоциированным параметрам реализуется связыванием значений: после ключевого слова case и указания значения в скобках объявляются константы, которым будут присвоены ассоциированные с членом перечисления значения. Так как для всех ассоциированных параметров создаются константы со связываемым значением, оператор let можно ставить сразу после ключевого слова case (это продемонстрировано для члена euro).

22.5. Связанные значения членов перечисления

Как альтернативу ассоциированным параметрам для членов перечислений им можно задать связанные значения некоторого типа данных (например, String, Character или Int). В результате вы получаете член перечисления и постоянно привязанное к нему значение.

ПРИМЕЧАНИЕ Связанные значения также называют исходными, или сырыми. Но в данном случае термин «связанные» значительно лучше отражает их предназначение.

Указание связанных значений

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

Листинг 22.12

enum Smile: String {

    case joy = ":)"

    case laugh = ":D"

    case sorrow = ":("

    case surprise = "o_O"

}

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

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

Внимание Одновременное определение исходных значений и ассоциированных параметров запрещено.

Если в качестве типа данных перечисления указать Int, то исходные значения создаются автоматически путем увеличения значения на единицу для каждого последующего члена (значение первого члена равно 0). Тем не менее, конечно же, можно указать эти значения самостоятельно. Например, в листинге 22.13 представлено перечисление, содержащее список планет Солнечной системы в порядке удаленности от Солнца.

Листинг 22.13

enum Planet: Int {

      case mercury = 1, venus, earth, mars, jupiter, saturn, uranus,

                        neptune, pluton = 999

}

Для первого члена перечисления в качестве исходного значения указано целое число 1. Для каждого следующего члена значение увеличивается на единицу, так как не указано иное: для venus — это 2, для earth3 и т.д.

Для члена pluton связанное значение указано конкретно, поэтому оно равно 999.

Доступ к связанным значениям

При создании экземпляра перечисления можно получить доступ к исходному значению члена этого экземпляра перечисления. Для этого используется свойство rawValue. Создадим экземпляр объявленного ранее перечисления Smile и получим исходное значение установленного в этом экземпляре члена (листинг 22.14).

Листинг 22.14

var iAmHappy = Smile.joy

iAmHappy.rawValue // ":)"

В результате использования свойства rawValue мы получаем исходное значение члена joy типа String.

22.6. Инициализатор

При объявлении структуры в ее состав обязательно входит специальный метод-инициализатор. Более того, вам даже не требуется его объявлять, так как эта возможность заложена в Swift изначально. Как мы говорили ранее, при изучении фундаментальных типов, инициализаторы всегда имеют имя init. Другими словами, инициализатор — это метод в составе объектного типа (перечисления, или, как вы узнаете далее, структуры или класса), имеющий имя init.

Перечисления имеют всего один инициализатор init(rawValue:). Он позволяет передать связанное значение, соответствующее требуемому члену перечисления. Таким образом, у нас есть возможность инициализировать параметру конкретный член перечисления по связанному с ним значению.

В листинге 22.15 показан пример использования инициализатора перечисления.

Листинг 22.15

var myPlanet = Planet.init(rawValue: 3) // earth

var anotherPlanet = Planet.init(rawValue: 11) // nil

Повторю:

• Инициализатор перечисления Planet — это метод init(rawValue:). Ему передается указатель на исходное значение, связанное с искомым членом этого перечисления.

• Данный метод не описан в теле перечисления, — он существует там всегда по умолчанию и закреплен в исходном коде языка Swift.

Инициализатор init(rawValue:) возвращает опционал, поэтому если вы укажете несуществующее связанное значение, возвратится nil.

ПРИМЕЧАНИЕ Инициализаторы вызываются каждый раз при создании нового экземпляра какого-либо перечисления, структуры или класса. Для некоторых конструкций их можно и нужно создавать самостоятельно, а для некоторых, вроде перечислений, они существуют по умолчанию.

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

Инициализатор — это всегда метод с именем init.

С инициализаторами мы познакомимся подробнее в следующих главах книги.

22.7. Свойства в перечислениях

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

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

Свойство в перечислении — это хранилище, аналогичное переменной или константе, объявленное в пределах перечисления и доступное через его экземпляр. В Swift существует определенное ограничение для свойств в перечислениях: в качестве их значений не могут выступать фиксированные значения-литералы, а лишь замыкания. Такие свойства называются вычисляемыми. При каждом обращении к ним происходит вычисление присвоенного замыкания с возвращением получившегося значения.

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

Объявим вычисляемое свойство для разработанного ранее перечисления (листинг 22.16). За основу возьмем перечисление Smile и создадим вычисляемое перечисление, которое возвращает связанное с текущим членом перечисления значение.

Листинг 22.16

enum Smile: String {

    case joy = ":)"

    case laugh = ":D"

    case sorrow = ":("

    case surprise = "o_O"

    // вычисляемое свойство

    var description: String {return self.rawValue}

}

var mySmile: Smile = .sorrow

mySmile.description // ":("

Вычисляемое свойство должно быть объявлено как переменная (var). В противном случае, если вы используете оператор let, то получите сообщение об ошибке.

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

22.8. Методы в перечислениях

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

Вернемся к примеру с перечислением Smile и создадим метод, который выводит на консоль справочную информацию о предназначении перечисления (листинг 22.17).

Листинг 22.17

enum Smile: String {

    case joy = ":)"

    case laugh = ":D"

    case sorrow = ":("

    case surprise = "o_O"

    var description: String {return self.rawValue}

    func about(){

        print("Перечисление содержит список смайликов ")

    }

}

var otherSmile = Smile.joy

otherSmile.about()

Консоль

Перечисление содержит список смайликов

В этом перечислении объявлен метод about(). После создания экземпляра метода и помещения его в переменную данный метод может быть вызван.

22.9. Оператор self

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

Рассмотрим пример.

Требуется написать два метода, один будет возвращать сам член перечисления, а второй — его связанное значение. Используем для этого уже знакомое перечисление Smile (листинг 22.18).

Листинг 22.18

enum Smile: String {

    case joy = ":)"

    case laugh = ":D"

    case sorrow = ":("

    case surprise = "o_O"

    var description: String {return self.rawValue}

    func about(){

        print("Перечисление содержит список  смайликов")

    }

    func descriptionValue() -> Smile{

        return self

    }

    func descriptionRawValue() -> String{

        return self.rawValue

    }

}

var otherSmile = Smile.joy

otherSmile.descriptionValue() // joy

otherSmile.descriptionRawValue() // ":)"

При вызове метода descriptionValue() происходит возврат self, то есть самого экземпляра. Именно поэтому тип возвращаемого значения данного метода — Smile, он соответствует типу экземпляра перечисления.

Метод descriptionRawValue() возвращает связанное значение члена данного экземпляра также с использованием оператора self.

При необходимости вы даже можете выполнить анализ перечисления внутри самого перечисления с помощью конструкции switch self {}, где значениями являются члены перечисления.

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

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

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

Возьмем, к примеру, простейшие арифметические операции: сложение, вычитание, умножение и деление. Все они заранее известны, поэтому могут быть помещены в перечисление в качестве его членов (листинг 22.19). Для простоты оставим только две операции: сложение и вычитание.

Листинг 22.19

enum ArithmeticExpression{

    // операция сложения

    case addition(Int, Int)

    // операция вычитания

    case substraction(Int, Int)

}

var expr = ArithmeticExpression.addition(10, 14)

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

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

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

Листинг 22.20

enum ArithmeticExpression{

    case addition(Int, Int)

    case substraction(Int, Int)

    func evaluate() -> Int {

        switch self{

        case .addition(let left, let right):

            return left+right

        case .substraction(let left, let right):

            return left-right

        }

    }

}

var expr = ArithmeticExpression.addition(10, 14)

expr.evaluate() // 24

При вызове метода evaluate() происходит поиск определенного в данном экземпляре члена перечисления. Для этого используются операторы switch и self. Далее, после того как член определен, путем связывания значений возвращается результат требуемой арифметической операции.

Данный способ работает просто замечательно, но имеет серьезное ограничение: он способен моделировать только одноуровневые арифметические выражения: 1 + 5, 6 + 19 и т.д. В ситуации, когда выражение имеет вложенные выражения: 1 + (5 — 7), 6 — 5 + 4 и т.д., нам придется вычислять каждое отдельное действие с использованием собственного экземпляра типа ArithmeticExpression.

Для решения этой проблемы необходимо доработать перечисление ArithmeticExpression таким образом, чтобы оно давало возможность складывать не только значения типа Int, но и значения типа ArithmeticExpression.

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

• либо перед оператором enum — в этом случае каждый член перечисления может обратиться к данному перечислению;

• либо перед оператором case того члена, в котором необходимо обратиться к перечислению.

Если в качестве ассоциированных параметров перечисления указывать значения типа самого перечисления ArithmeticExpression, то возникает вопрос: а где же хранить числа, над которыми совершаются операции? Такие числа также необходимо хранить в самом перечислении, в его отдельном члене.

Рассмотрим пример из листинга 22.21. В данном примере вычисляется значение выражения 20 + 10 — 34.

Листинг 22.21

enum ArithmeticExpression {

    // указатель на конкретное значение

    case number( Int )

    // указатель на операцию сложения

    indirect case addition( ArithmeticExpression, ArithmeticExpression )

    // указатель на операцию вычитания

    indirect case subtraction( 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 )

        }

    }

}

var hardExpr = ArithmeticExpression.addition( .number(20), .subtraction( .number(10), .number(34) ) )

hardExpr.evaluate() // -4

У перечисления появился новый член number, который определяет целое число — операнд для проведения очередной операции. Для членов арифметических операций использовано ключевое слово indirect, позволяющее передать значение типа ArithmeticExpression в качестве ассоциированного параметра.

Метод evaluate(expression:) принимает на входе опциональное значение типа ArithmeticExpression?. Опционал в данном случае позволяет вызвать метод, не передавая ему экземпляр, из которого этот метод был вызван. В противном случае последняя строка листинга выглядела бы следующим образом:

expr.evaluate(expression: expr)

Согласитесь, что существующий вариант значительно удобнее.

Оператор switch, используя принудительное извлечение, определяет, какой член перечисления передан, и возвращает соответствующее значение.

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

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

Назад: 21. Введение в объектно-ориентированное программирование
Дальше: 23. Структуры