Enums + Associated Values = Swift
Swift — значит быстрый. Быстрый — значит понятный, простой. Но достичь простоты и понятности непросто: сейчас в Swift скорость компиляции так себе, да и некоторые моменты языка вызывают вопросы. Тем не менее возможность перечислений (enum’ов), про которую я расскажу (associated values — присоединяемые значения) — одна из самых крутых. Она позволяет сократить код, сделать его понятнее и надёжнее.
Исходная задача
Представим, что нужно описать структуру вариантов оплаты: наличными, картой, по подарочному сертификату, пейпалом. У каждого способа оплаты могут быть свои параметры. Вариантов реализации при таких слабо определенных условиях — бесконечное количество, что-то можно сгруппировать, что-то переобозначить. Не стоит придираться, эта структура тут живёт для примера.
class PaymentWithClass {
// true, значит платим наличными, а false нужен, чтобы init() не писать
var isCash: Bool = false
// .some, значит платим картой, номер такой-то, параметры такие-то
var cardNumber: String?
var cardHolderName: String?
var cardExpirationDate: String?
// .some, значит платим подарочным сертификатом
var giftCertificateNumber: String?
// тут какой-то идентификатор и код авторизации (что бы это ни значило)
var paypalTransactionId: String?
var paypalTransactionAuthCode: String?
}
(Что-то много опшналов получилось)
Работать с таким классом тяжело. Swift мешает, ведь в отличие от многих других языков, он заставит обработать все опшналы (например, if let
или guard let
). Будет много лишнего кода, который спрячет бизнес-логику.
Неприятно такое и верифицировать. Представить себе код, проверяющий валидность этого класса ещё можно, а вот писать такое — уже совсем не хочется.
Структуры!
Вспомним, что в Swift есть value-типы, которые называются структуры. Говорят, их хорошо использовать для такого рода модельных описаний. Попробуем заодно разбить предыдущий код на несколько структур, соответствующих разным типам оплаты.
struct PaymentWithStructs {
struct Cash {}
struct Card {
var number: String
var holderName: String
var expirationDate: String
}
struct GiftCertificate {
var number: String
}
struct PayPal {
var transactionId: String
var transactionAuthCode: String?
}
var cash: Cash?
var card: Card?
var giftCertificate: GiftCertificate?
var payPal: PayPal?
}
Получилось лучше. Корректность отдельных типов оплаты проверяется средствами самого языка (например, видно, что авторизационный код PayPal может отсутствовать, а вот для карты все поля обязательны), нам остаётся провалидировать только что одно (и только одно) из полей cash
, card
, giftCertificate
, payPal
— не нулевое.
Удобно? Вполне. В этом месте во многих языках можно поставить точку и считать работу выполненной. Но не в Swift.
Enum’ы. Ассоциированные значения
Свифт, как и любой современный язык, предоставляет возможность создавать типизированные перечисления (enum’ы). Их удобно использовать, когда нужно описать, что поле может быть одним из нескольких, заранее определенных, значений или вставить в switch
, чтобы проконтролировать, что все возможные варианты перебраны. По идее в нашем примере максимум, что можно сделать — это вставить тип оплаты, чтобы ещё точнее валидировать структуру:
struct PaymentWithStructs {
enum Kind {
case cash
case card
case giftCertificate
case payPal
}
struct Card {
var number: String
var holderName: String
var expirationDate: String
}
struct GiftCertificate {
var number: String
}
struct PayPal {
var transactionId: String
var transactionAuthCode: String?
}
var kind: Kind
var card: Card?
var giftCertificate: GiftCertificate?
var payPal: PayPal?
}
Заметьте, что структура Cash
пропала, она пустая и висела только для индикации типа, который мы теперь ввели явно. Стало лучше? Да, проще стало проверять, какой тип оплаты мы используем. Он прописан явно, не нужно анализировать, какое поле не nil
.
Следующий шаг — попробовать как-то избавиться от необходимости отдельных опшналов для каждого типа. Было бы круто, если бы существовала возможность привязать к типу соответствующие структуры. К карте — номер и владельца, к PayPal — идентификатор транзакции и так далее.
Именно для этого в Swift есть ассоциированные значения (associated values, не путайте с associated types, которые используются в протоколах и совсем не про то). Записываются они вот так:
enum PaymentWithEnums {
case cash
case card(
number: String,
holderName: String,
expirationDate: String
)
case giftCertificate(
number: String
)
case payPal(
transactionId: String,
transactionAuthCode: String?
)
}
Самый важный момент — точно определенный тип. Сразу видно, что PaymentWithEnums
может принимать ровно одно из четырёх значений, и у каждого значения могут быть (или не быть) как дата или transactionAuthCode
определённые параметры. Физически нельзя проставить параметры сразу и для карты и для подарочного сертификата, как это можно было сделать в варианте с классами или структурами.
Получается, что предыдущие варианты требуют дополнительных проверок. Также была возможность забыть обработать какой-нибудь из вариантов оплаты, особенно, если появятся новые. Enum’ы исключают все эти проблемы. Если добавится новый case, следующая перекомпиляция потребует добавить его во все switch’и. Никаких опшналов кроме действительно необходимых.
Пользоваться такими сложными enum’ами можно как обычно:
if case .cash = payment {
// сделать что-то специфичное для оплаты наличными
}
Параметры получаются при помощи паттерн-матчинга:
if case .giftCertificate(let number) = payment {
print("O_o подарочный сертификат! Номер: \(number)")
}
А перебрать все варианты можно свичом:
switch payment {
case .cash:
print("Оффлайновые деньги!")
case .card(let number, let holderName, let expirationDate):
let last4 = String(number.characters.suffix(4))
print("Хехе, карточка! Последние цифры \(last4), хозяин: \(holderName)!")
case .giftCertificate(let number):
print("O_o подарочный сертификат! Номер: \(number)")
case .payPal(let transactionId, let transactionAuthCode):
print("Пейпалом платим! Транзакция: \(transactionId)")
}
В качестве упражнения можно написать похожий код для варианта с классом/структурой. Чтобы упражнение было полным, нужно не полениться и обработать все необходимые варианты, включая ошибочные.
Кайф. Swift. Ещё бы компилился побыстрее, и вообще будет счастье. :-)
Немного граблей
Enum’ы — не панацея. Вот несколько моментов, которые вам могут встретиться.
Во-первых, enum’ы не могут содержать хранимые поля. Если мы захотим добавить в PaymentWithEnums
, к примеру, дату платежа (одно и то же поле для всех вариантов оплаты), то получим ошибку: enums may not contain stored properties
. Как же быть? Можно положить дату в каждый кейс enum’а. Можно сделать структуру и положить туда enum и дату.
Во-вторых, если обычные enum’ы можно сравнивать оператором ==
(он синтезируется автоматически), то как только появляются ассоциированные значения, возможность сравнения «пропадает». Поправить это можно легко, поддержать протокол Equatable
. Впрочем даже после этого сравнивать будет неудобно, так как нельзя просто написать payment == PaymentWithEnums.giftCertificate
, потребуется создать правую часть целиком: PaymentWithEnums.giftCertificate(number: "")
. Гораздо удобнее в таком случае создавать специальные методы, возвращающие Bool (isGiftCertificate(_ payment: PaymentWithEnums) -> Bool
), и перенести туда if case
. Если же нужно сравнивать несколько значений, то, возможно, switch
будет удобнее.