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

21 декабря 2015

Сила перечислений (энумов) в Swift

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

Думаю, что не будет ошибкой сказать, что птица может быть одной из следующих: Galah (розовый какаду), Kookaburra (Кукаберра) или какой-то другой.

В C мы могли бы представить концепцию следующим образом:

enum {

    Galah,
    Kookaburra,
    Other

};

int main(int argc, char *argv[]) {
    int bird = Kookaburra;
    printf("%d", bird);

}

Нужно признать, что перечисления в C довольно “дырявая абстракция”. Так как они являются по существу только перечислениями ints , мы можем делать странные вещи, например добавлять их вместе. Что должно значить Galah + Kookaburra, если они равны 1?

В Swift также есть перечисления и они работают очень похоже с C:

enum Bird: Int {

    case Galah
    case Kookaburra
    case Other

}

print(Bird.Kookaburra.rawValue)

Для того, чтобы в Swift перечисления начали работать как в C, мы должны явно заявить, что они основаны на Int , а затем извлечь значения со свойством rawValue. Так происходит потому, что Swift "менее дырявый", чем C и строже в соблюдении типа безопасности.

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

enum Bird {

    case Galah
    case Kookaburra
    case Other

}

print(Bird.Kookaburra.rawValue)

Но теперь наша программа просто выдает  (Enum Value), а нам это не очень поможет. Как бы мы могли посмотреть фактическое значение?

Есть несколько вариантов. Мы можем использовать трюк с Int, как указано выше, но мы можем сделать и лучше. Мы можем сделать так, что наше перечисление будет основываться на другом типу, скажем, на Strings :

enum Bird: String {

    case Galah = "Galah"
    case Kookaburra = "Kookaburra"
    case Other = "Other"

}

print(Bird.Kookaburra.rawValue)

Еще более мощная альтернатива- это использовать протокол CustomStringConvertible:

enum Bird: CustomStringConvertible {

    case Galah
    case Kookaburra
    case Other


    var description: String {

        switch self {

        case Galah:
            return "Galah"
            
        case Kookaburra:
            return "Kookaburra"
            
        case Other:
            return "Other"
        }
    }
}

print(Bird.Kookaburra)

Протокол объявляет свойство, называемое description, которое возвращает строку, описывающую тип. Концептуально это похоже на метод description протокола NSObject в Objective-C.

Этот пример также показывает нам еще одну интересную особенность перечислений в Swift: они могут содержать свойства и функции, как классы.

Чего-чего?

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

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

case Other(name: String)

Теперь давайте исправим свойство description, для того чтобы вернуть это имя. Мы делаем это, связывая переменную с образцом в нашем выражении switch.

var description: String {

    switch self {
        //...
    case let Other(name):
        return name
    }
}

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

let bird = Bird.Other(name: "Cockatoo")

print(bird)

Бинарные деревья

Скажем, мы хотим построить наше собственное бинарное дерево типа данных, который будет хранить отсортированные Int.

Если бы мы хотели использовать класс, мы могли бы сделать это так:

class Tree {

    let value: Int
    let left: Tree?
    let right: Tree?

}

Мы хотим выбросить в constructor:

init(value: Int, left: Tree?, right: Tree?) {

    self.value = value
    self.left = left
    self.right = right

}

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

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

func insert(newValue: Int) -> Tree {

    if newValue > value {
        if let theRight = right {
            return Tree(value: value,
                left: left,
                right: theRight.insert(newValue))
        } else {
            return Tree(value: value,
                left: left,
                right: Tree(value: newValue, left: nil, right: nil))
        }
    } else {
        if let theLeft = left {
            return Tree(value: value,
                left: theLeft.insert(newValue)
                right: right)
        } else {
            return Tree(value: value,
                left: Tree(value: newValue, left: nil, right: nil),
                right: right)
        }
    }
}

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

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

Бинарные деревья с перечислениями

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

indirect enum Tree {
    case Node(value: Int, left: Tree, right: Tree)
}

У нас все еще есть проблема, что поддеревья (subtrees) узла могут не существовать, но вместо использования опционалов, давайте смоделируем это с другим кейсом в нашем перечислении:

indirect enum Tree {
    case Empty
    case Node(Int, left: Tree, right: Tree)

}

Наши левые и правые поддеревья теперь гарантированно не будут nil. Вместо этого, они являются "одними из" узла или пустым деревом. Следовательно, наш метод вставки может быть упрощен, так как места, с которыми мы имеем дело с пустого дерева могут быть объединены.

Сначала мы должны добавить функцию insert() в наше перечисление и реализовать ее там:

func insert(newValue: Int) -> Tree {
    switch self {
    case .Empty:
        return Tree.Node(value: newValue, left: Tree.Empty, right: Tree.Empty)
    case let .Node(value, left, right):
        if newValue > value {
            return Tree.Node(value: value, left: left, right: right.insert(newValue))
        } else {
            return Tree.Node(value: value, left: left.insert(newValue), right: right)
        }
    }
}

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

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

Подведем итог

Мы заканчиваем с полным перечислением древовидного типа, и давайте добавим свойтсво depth и сделаем перечисление CustomStringConvertible :

indirect enum Tree: CustomStringConvertible {
    case Empty
    case Node(value: Int, left: Tree, right: Tree)
    
    var depth: Int {
        switch self {
        case .Empty:
            return 0
        case let .Node(_, left, right):
            return 1 + max(left.depth, right.depth)
        }
    }
    
    var description: String {
        switch self {
        case .Empty:
            return "."
        case let .Node(value, left, right):
            return "[\(left) \(value) \(right)]"
        }
    }
    
    func insert(newValue: Int) -> Tree {
        switch self {
            
        case .Empty:
            return Tree.Node(value: newValue, left: Tree.Empty, right: Tree.Empty)
            
        case let .Node(value, left, right):
            
            if newValue > value {
                return Tree.Node(value: value, left: left, right: right.insert(newValue))
            } else {
                return Tree.Node(value: value, left: left.insert(newValue), right: right)
            }
        }
    }
}

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

Что дальше?

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

Источник урока: туториал по Swift1.2

Урок подготовил: Акулов Иван

Содержание