Enums + Associated Values ​​= Swift

  • Tutorial

Swift means fast. Fast means clear, simple. But achieving simplicity and comprehensibility is not easy: now in Swift, the compilation speed is so-so, and some aspects of the language raise questions. Nevertheless, the possibility of enums, about which I will talk about (associated values), is one of the coolest. It allows you to reduce the code, make it more understandable and reliable.




Initial task


Imagine that you need to describe the structure of payment options: cash, card, gift certificate, paypal. Each payment method may have its own parameters. The implementation options under such poorly defined conditions are an infinite number, something can be grouped, something can be redesignated. You should not find fault, this structure lives here for an example.


classPaymentWithClass{
    // 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?
}

(Something turned out a lot of opshnal)


It’s hard to work with such a class. Swift interferes, because unlike many other languages, it will force you to process all the ophnals (for example, if letor guard let). There will be a lot of extra code that will hide the business logic.


It is unpleasant to verify this. You can still imagine code that checks the validity of this class, but you don’t feel like writing it at all.


Structures!


Recall that in Swift there are value types called structures. They say they are good to use for this kind of model descriptions. Let's try to break the previous code into several structures corresponding to different types of payment.


structPaymentWithStructs{
    structCash{}
    structCard{
        var number: Stringvar holderName: Stringvar expirationDate: String
    }
    structGiftCertificate{
        var number: String
    }
    structPayPal{
        var transactionId: Stringvar transactionAuthCode: String?
    }
    var cash: Cash?var card: Card?var giftCertificate: GiftCertificate?var payPal: PayPal?
}

It turned out better. The correctness of certain types of payment is verified by means of the language (for example, it is clear that the authorization PayPal code may be missing, but the card all fields are required), we are left provalidirovat just one (and only one) of the fields cash, card, giftCertificate, payPal- not zero.


Conveniently? Quite. In this place in many languages ​​you can put an end to and consider the work done. But not in Swift.


Enum's. Associated Values


Swift, like any modern language, provides the ability to create typed enums (enum). They are conveniently used when it is necessary to describe that a field can be one of several predefined values ​​or insert into switchto check that all possible options are enumerated. In theory, in our example, the maximum that can be done is to insert a payment type in order to validate the structure more precisely:


structPaymentWithStructs{
    enumKind{
        case cash
        case card
        case giftCertificate
        case payPal
    }
    structCard{
        var number: Stringvar holderName: Stringvar expirationDate: String
    }
    structGiftCertificate{
        var number: String
    }
    structPayPal{
        var transactionId: Stringvar transactionAuthCode: String?
    }
    var kind: Kindvar card: Card?var giftCertificate: GiftCertificate?var payPal: PayPal?
}

Note that the structure has Cashdisappeared, it is empty and hung only to indicate the type that we have now explicitly entered. Got better? Yes, it’s easier to check what type of payment we use. It is spelled out explicitly, no need to analyze which field is not nil.


The next step is to try to somehow get rid of the need for separate ophnals for each type. It would be cool if there was an opportunity to bind the corresponding structures to the type. To the card - the number and owner, to PayPal - the transaction identifier and so on.


That's why Swift has associated values ​​(associated values, do not confuse with associated types, which are used in protocols and not at all about that). They are written like this:


enum PaymentWithEnums {
    case cash
    case card(
        number: String,
        holderName: String,
        expirationDate: String
    )
    case giftCertificate(
        number: String
    )
    case payPal(
        transactionId: String,
        transactionAuthCode: String?
    )
}

The most important point is the precisely defined type. It’s immediately obvious that it PaymentWithEnumscan take exactly one of the four values, and each value can have (or not have) as a date or transactionAuthCodecertain parameters. It is physically impossible to put down the parameters for both the card and the gift certificate at once, as it could be done in the case with classes or structures.


It turns out that the previous options require additional checks. There was also the opportunity to forget to process any of the payment options, especially if new ones appear. Enum's eliminate all of these problems. If a new case is added, the next recompilation will require adding it to all switches. No opshnalov except really necessary.


You can use such complex enums as usual:


ifcase .cash = payment {
    // сделать что-то специфичное для оплаты наличными
}

Parameters are obtained using pattern matching:


ifcase .giftCertificate(let number) = payment {
    print("O_o подарочный сертификат! Номер: \(number)")
}

And you can sort through all the options with a switch:


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)")
}

As an exercise, you can write similar code for a class / structure variant. To complete the exercise, you need not be too lazy and process all the necessary options, including erroneous ones.


Buzz. Swift It would have compiled faster, and in general there would be happiness. :-)


A little rake


Enum's are not a panacea. Here are a few things you might come across.


Firstly, enums cannot contain stored fields. If we want to add PaymentWithEnums, for example, the date of payment (the same field for all payment options), we get an error: enums may not contain stored properties. How to be? You can put a date in each enum case. You can make a structure and put enum and date there.


Secondly, if ordinary enums can be compared by an operator ==(it is synthesized automatically), then as soon as the associated values ​​appear, the possibility of comparison “disappears”. You can fix this easily, support the protocol Equatable. But even after that compare will be uncomfortable, because you can not just write payment == PaymentWithEnums.giftCertificate, you need to create the right part of the whole: PaymentWithEnums.giftCertificate(number: ""). In this case, it is much more convenient to create special methods that return Bool ( isGiftCertificate(_ payment: PaymentWithEnums) -> Bool) and transfer them there if case. If you need to compare multiple values, it may switchbe more convenient.


Also popular now: