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

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

enum DayOfWeek {
    case sunday
    case monday
    case tuesday
    case wednesday
    case thursday
    case friday
    case saturday
}

Это определяет перечисление DayOfWeek, которое может принимать одно из значений: sunday, monday, tuesday и т.д

Простое использование:

let currentDay = DayOfWeek.sunday

Поскольку набор определенных значений в перечислении конечен очень распространено использование оператора switch для перебора значений вместо использования нескольких операторов if else:

switch currentDay {
case .sunday, .saturday:
    print("It's the weekend!")
default:
    print("It's a weekday.")
}

Associated Values

Однако иногда нам может потребоваться хранить дополнительные данные для каждого случая перечисления. Эта дополнительная информация, прикрепленная к значениям перечисления, называется связанными значениями (Associated Values).

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

enum Result<T> {
    case success(T)
    case failure(String)
}

В этом примере, Result имеет два случая:

  1. .success(T): представляет успешный результат операции и ассоциированное значение типа T

  2. .failure(String): представляет ошибку и ассоциированное текстовое сообщение об ошибке

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

func divide(_ dividend: Double, by divisor: Double) -> Result<Double> {
    if divisor == 0 {
        return .failure("Division by zero is not allowed.")
    }
    
    let result = dividend / divisor
    return .success(result)
}

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

let result1 = divide(10, by: 2)
switch result1 {
case .success(let value):
    print("Результат: \(value)")
case .failure(let error):
    print("Ошибка: \(error)")
}

let result2 = divide(5, by: 0)
switch result2 {
case .success(let value):
    print("Результат: \(value)")
case .failure(let error):
    print("Ошибка: \(error)")
}

В первом случае result1 будет .success(5.0), и мы получим "Результат: 5.0". Во втором случае result2 будет .failure("Division by zero is not allowed."), и мы получим "Ошибка: Делить на ноль нельзя"

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

Raw Values

Рассмотрим пример с перечислением, использующим сырые значения (raw values - могут быть строками, символами или числами). Возьмем перечисление HTTPStatus, которое представляет стандартные HTTP статусы, и свяжем каждый статус с его числовым кодом:

enum HTTPStatus: Int {
    case ok = 200
    case created = 201
    case accepted = 202
    case badRequest = 400
    case unauthorized = 401
    case forbidden = 403
    case notFound = 404
    case internalServerError = 500
}

Здесь каждый статус HTTP связан с числовым значением, которое соответствует его коду ответа.

Теперь мы можем использовать это перечисление для представления статусов HTTP в нашем коде:

let responseCode = 404
if let httpStatus = HTTPStatus(rawValue: responseCode) {
    switch httpStatus {
    case .ok:
        print("Success: 200 OK")
    case .notFound:
        print("Error: 404 Not Found")
    case .internalServerError:
        print("Error: 500 Internal Server Error")
    default:
        print("Received an HTTP status with code \(httpStatus.rawValue)")
    }
} else {
    print("Invalid HTTP status code: \(responseCode)")
}

В этом примере мы получаем код ответа от сервера (404), затем создаем экземпляр перечисления HTTPStatus с использованием rawValue, и в конечном итоге, на основе значения, выполняем соответствующие действия. Это помогает нам легко работать с HTTP статусами и улучшает читаемость кода.

Продвинутые концепции

Ассоциированные значения с функциями

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

Рассмотрим подробный пример, используя перечисление Operation, которое представляет различные математические операции с ассоциированными функциями:

enum Action {
    case printMessage(() -> Void)
    case calculateSum((Int, Int) -> Int)
}

// Создадим экземпляры перечисления с ассоциированными функциями
let printAction = Action.printMessage({
    print("Hello, World!")
})

let sumAction = Action.calculateSum({ a, b in
    return a + b
})

// Выполним ассоциированные функции
switch printAction {
case .printMessage(let action):
    action()
case .calculateSum(let action):
    // Этот случай не будет выполнен в данном примере
    break
}

switch sumAction {
case .printMessage(let action):
    // Этот случай не будет выполнен в данном примере
    break
case .calculateSum(let action):
    let result = action(5, 3)
    print("Result is: \(result)")
}

//Вывод:
//Hello, World!
//Result is: 8

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

  1. Мы создаем перечисление Action, где каждый случай перечисления имеет ассоциированную функцию

  2. Мы создаем экземпляры перечисления Action, привязывая к ним соответствующие функции

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

  4. В первом switch мы выполняем функцию для printAction, которая печатает "Hello, World!"

  5. Во втором switch мы выполняем функцию для sumAction, которая выполняет сложение двух чисел (5 и 3) и печатает результат

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

Использование associatedtype с ассоциированными значениями

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

Предположим, у нас есть перечисление Shape, которое представляет разные геометрические фигуры, такие как круг, прямоугольник и треугольник. Мы хотим, чтобы каждая фигура имела разные ассоциированные значения, такие как радиус для круга, ширина и высота для прямоугольника и стороны для треугольника. Вот как это можно сделать с использованием associatedtype:

enum Shape {
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)
    case triangle(side1: Double, side2: Double, side3: Double)
}

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

protocol Drawable {
    associatedtype DrawingType
    func draw() -> DrawingType
}

Расширим перечисление Shape, чтобы оно соответствовало этому протоколу и определит ассоциированный тип DrawingType для каждой фигуры:

extension Shape: Drawable {
    typealias DrawingType = String

    func draw() -> DrawingType {
        switch self {
        case .circle(let radius):
            return "Рисуем круг с радиусом \(radius)"
        case .rectangle(let width, let height):
            return "Рисуем прямоугольник с шириной \(width) и высотой \(height)"
        case .triangle(let side1, let side2, let side3):
            return "Рисуем треугольник со сторонами \(side1), \(side2), \(side3)"
        }
    }
}

Теперь мы можем создавать экземпляры фигур и вызывать метод draw(), который возвращает строку, представляющую отрисовку фигуры:

let circle = Shape.circle(radius: 5.0)
let rectangle = Shape.rectangle(width: 10.0, height: 8.0)
let triangle = Shape.triangle(side1: 3.0, side2: 4.0, side3: 5.0)

print(circle.draw())     // Вывод: Рисуем круг с радиусом 5.0
print(rectangle.draw())  // Вывод: Рисуем прямоугольник с шириной 10.0 и высотой 8.0
print(triangle.draw())   // Вывод: Рисуем треугольник со сторонами 3.0, 4.0, 5.0

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

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

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

indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

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

  • ArithmeticExpression - это рекурсивное перечисление, которое может иметь три случая: .number, представляющий целое число, .addition, представляющий операцию сложения между двумя выражениями, и .multiplication, представляющий операцию умножения между двумя выражениями.

  • Ключевое слово indirect используется перед случаями .addition и .multiplication, чтобы указать, что эти случаи могут содержать рекурсивные значения (другие выражения).

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

let expression = ArithmeticExpression.addition(
    .number(5),
    .multiplication(
        .number(2),
        .number(3)
    )
)

func evaluate(_ expression: ArithmeticExpression) -> Int {
    switch expression {
    case .number(let value):
        return value
    case .addition(let left, let right):
        return evaluate(left) + evaluate(right)
    case .multiplication(let left, let right):
        return evaluate(left) * evaluate(right)
    }
}

let result = evaluate(expression)
print("Результат вычисления: \(result)") // Вывод: Результат вычисления: 11

В этом примере мы создаем арифметическое выражение, которое представляет выражение 5 + (2 * 3). Затем мы используем рекурсивную функцию evaluate, чтобы вычислить значение этого выражения.

Протоколы и перечисления

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

// Протокол Playable определяет объекты, которые можно воспроизводить
protocol Playable {
    func play()
}

// Перечисление MediaType представляет различные типы медиафайлов
enum MediaType {
    case audio(title: String, duration: TimeInterval)
    case video(title: String, duration: TimeInterval)
    case text(title: String, content: String)
}

// Реализация протокола Playable для перечисления MediaType
extension MediaType: Playable {
    func play() {
        switch self {
        case .audio(let title, let duration):
            print("Воспроизведение аудио: \(title), Длительность: \(duration) секунд")
        case .video(let title, let duration):
            print("Воспроизведение видео: \(title), Длительность: \(duration) секунд")
        case .text(let title, let content):
            print("Отображение текста: \(title) - \(content)")
        }
    }
}

// Создаем медиафайлы с использованием перечисления MediaType
let audioFile = MediaType.audio(title: "Music", duration: 180.0)
let videoFile = MediaType.video(title: "Movie", duration: 1200.0)
let textFile = MediaType.text(title: "Document", content: "Это образец документа")

// Воспроизводим или отображаем медиафайлы
audioFile.play()
videoFile.play()
textFile.play()

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

  1. Мы создали протокол Playable, который определяет единственное требование - метод play(), предназначенный для воспроизведения объекта.

  2. Создано перечисление MediaType, которое представляет различные типы медиафайлов: аудио, видео и текст. Каждый случай перечисления имеет связанные значения для хранения информации о медиафайле.

  3. Расширение MediaType реализует протокол Playable, предоставляя метод play(), который выводит информацию о воспроизведении или отображении медиафайла.

  4. Мы создали разные медиафайлы, используя случаи перечисления MediaType.

  5. Вызвали метод play() для каждого медиафайла, демонстрируя, как протокол Playable позволяет воспроизводить или отображать разные типы медиафайлов с использованием единого интерфейса.

Пользовательские операторы

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

Оператор для сравнения приоритетов задач:

Допустим, у вас есть перечисление TaskPriority, которое представляет разные приоритеты задач:

enum TaskPriority {
    case low
    case medium
    case high
}

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

infix operator <

func < (lhs: TaskPriority, rhs: TaskPriority) -> Bool {
    switch (lhs, rhs) {
    case (.low, .medium), (.low, .high), (.medium, .high):
        return true
    default:
        return false
    }
}

let lowPriority = TaskPriority.low
let mediumPriority = TaskPriority.medium
let highPriority = TaskPriority.high

if lowPriority < mediumPriority {
    print("Low priority меньше чем Medium priority")
}

//Вывод: Low priority меньше чем Medium priority

В этом примере мы определили пользовательский оператор <, который сравнивает приоритеты задач и возвращает true, если левый приоритет меньше правого. Это делает код более читаемым и позволяет использовать оператор < для сравнения приоритетов задач.

Оператор для объединения имен файлов:

Предположим, у вас есть перечисление File, которое представляет файлы, и вы хотите объединить их имена с помощью оператора +:

enum File {
    case image(String)
    case document(String)
}

infix operator +

func + (lhs: File, rhs: File) -> File {
    switch (lhs, rhs) {
    case let (.image(name1), .image(name2)):
        return .image(name1 + name2)
    case let (.document(name1), .document(name2)):
        return .document(name1 + name2)
    default:
        return .document("Invalid")
    }
}

let imageFile1 = File.image("image1.jpg")
let imageFile2 = File.image("image2.jpg")
let documentFile1 = File.document("document1.pdf")

let combinedImageFiles = imageFile1 + imageFile2
let combinedDocumentFiles = documentFile1 + imageFile1

print("Combined Image Files: \(combinedImageFiles)")
print("Combined Document Files: \(combinedDocumentFiles)")

В этом примере мы определили пользовательский оператор +, который объединяет имена файлов из случаев File. Мы также проверяем типы файлов и возвращаем "Invalid", если пытаемся объединить файлы разных типов.

Заключение:

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

Комментарии (3)


  1. shoorickus
    09.09.2023 10:46

    Что-то пример с переопределением оператора "+" для File у вас выглядит слишком искусственным. Не понятен смысл такого переопределения. Предположим в Associated Value мы хранили имя файла (например "image1.jpg"), которое затем передавали в FileManager, который по нему мог выполнять копирование или удаление файла. Что теперь делать с абстракцией File, когда Associated Value после "сложения" стало таким: "image1.jpgimage2.jpg"?


  1. AceRodstin
    09.09.2023 10:46

    Интересно было прочитать про использование closure и associated type в перичислении. Хочется спросить, возможно ли карирование при использовании indirect перечислений? И в каком случае будет использоваться dynamic dispatch вместо традиционного для перечисления static dispatch?

    Кстати, оператор сравнения вручную реализовывать не нужно. Достаточно прописать Comparable протокол в объявлении.


    1. anzmax Автор
      09.09.2023 10:46

      Про оператор сравнения спасибо, буду знать!

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

      Возможно, почему нет:

      Скриншот