The article is from collated notes on objccn. IO/related books. “Thank you objC.io and its writers for selflessly sharing their knowledge with the world.”

Structs and classes are both record types. A record consists of zero or more fields (attributes) that have a type. A tuple is also a record type: it’s actually a lightweight anonymous structure with less functionality.

Swift’s enumeration falls into an entirely different category, sometimes called a tag union or variant type. Although it is as powerful as records, support for it is not as common in mainstream programming languages. However, it is commonplace in functional languages and has become popular in newer languages such as Rust.

An overview of the

An enumeration consists of zero or more cases, each of which can have a tuple-style list of associated values. A member can have multiple associated values and can be treated as a single tuple.

// A simple enumeration without associated values, which indicates the alignment of paragraphs
enum TextAlignment { 
    case left
    case center
    case right
}
Copy the code

Optional is a generic enumeration with none and some members. Some has an associated value that represents the current boxed value:

@_frozen enum Optional<Wrapped> { 
    /// No value.
    case none
    /// a value exists and is saved as' Wrapped '.
    case some(Wrapped)}Copy the code

The Result type is used to indicate the success or failure of an operation. It has a similar structure to the optional value, except that it adds a generic parameter and sets it to the associated value of failure so that detailed error information can be obtained:

enum Result<Success.Failure: Error> {
    /// Success, save a value of 'Success'.
    case success(Success)
    /// Fail, save a 'Failure' value.
    case failure(Failure)}Copy the code

An enumerated value can be created by specifying a member and its associated value (if any) :

let alignment = TextAlignment.left
let download: Result<String.NetworkError> = .success("

Hello world!

"
) Copy the code

Note in the second line above that you must provide the full type annotation, including all generic parameters. Expressions like result.success (htmlText) generate errors unless the compiler can infer the specific type of another Failure generic parameter from the context. Once the full type is specified, we can rely on type inference to use leading-dot syntax (the definition of NetworkError is ignored here).

Enumerations are value types

Just like structures, enumerations are value types. Its capabilities are almost identical to those of the structure:

  • Enumerations can have methods, evaluate properties, and subscript operations.
  • Methods can be declared mutable or immutable.
  • Extensions can be implemented for enumerations.
  • Enumerations can implement a variety of protocols.

But enumerations cannot have storage properties. The state of an enumeration is represented entirely by a combination of its members and their associated values. For a particular member, the associated value can be treated as its storage property.

Enumerations work the same way as mutable methods in structures. In a mutable method, self is mutable because it is an inout parameter. Since enumerations do not store attributes, and there is no way to directly change the associated value of a member, the way to modify an enumeration is to assign a new value directly to self.

Because a member of an enumeration is usually used to initialize a variable of an enumeration type, enumeration does not require an explicit initialization method. However, additional convenience initializers may be required for the type definition or extension. For example, using Foundation’s Locale API, we can add an initialization method to the TextAlignment enumeration that sets a default TextAlignment based on the Locale argument passed in:

extension TextAlignment {
    init(defaultFor locale: Locale) {
        guard let language = locale.languageCode else {
            // The following is the default value when the current language is not available.
            self = .left
            return
        }
        switch Locale.characterDirection(forLanguage: language) {
        case .rightToLeft:
            self = .right
        // Left is the default for all other cases.
        case .leftToRight, .topToBottom, .bottomToTop, .unknown:
            self = .left
        @unknown default:
                self = .left
        }
    }
}
let english = Locale(identifier: "en_AU")
TextAlignment(defaultFor: english) // left 
let arabic = Locale(identifier: "ar_EG")
TextAlignment(defaultFor: arabic) // right
Copy the code

Sum type and product type

An enumeration value will contain only one enumeration member (plus the associated value if the member has an associated value). Specifically, a Result variable contains values that are either success or failure, but not both (and not nothing). In contrast, an instance of a record type contains the values of all its fields: a tuple of type (String, Int) contains a String and an integer.

This ability to implement “or” relationships is quite special and makes enumerations very useful. Enumerations allow us to take advantage of strong typing to write safer, cleaner code, often in situations where logic cannot be expressed clearly in structs, tuples, or classes.

It’s “pretty special” because you can use protocols and subclasses for the same purpose, although the tradeoffs and applications are different. A variable of a protocol type (also called An Interface) can be one of any types that implements this protocol. Similarly, in iOS, an object of UIView type can point to any direct or indirect subclass of UIView, such as UILabel or UIButton. When operating on such an object, we either use a public interface defined in the base class (equivalent to calling a method defined on an enumeration) or try to cast the instance down to a specific subclass in order to access data specific to that subclass (equivalent to converting an enumeration value).

The difference between the two approaches (dynamic distribution through the public interface of the protocol and class, or the use of enumerations) lies in which is more general, as well as in the capabilities and limitations that are unique to both structures. For example, all members of an enumeration are fixed and you can’t extend it beyond the declaration, but you can always have multiple types that satisfy the same protocol, or add another subclass (subclassing across modules is prohibited unless you declare a class open). Whether this freedom is desirable, or even needed, depends on the problem to be solved. As value types, enumerations are generally lighter and better suited for implementing POD (Plain Old Values).

There is a one-to-one correspondence between these two types (or, and) and the mathematical concepts of addition and multiplication. It provides a useful idea when designing custom types.

The term “type” can be defined in many ways. A definition is given here: a type is the set of all possible values that an instance of it can represent. Values are also called inhabitants. Bool has two residents, false and true. UInt8 has 2^8 or 256 inhabitants.

In general, the number of residents of a tuple (or structure, or class) is equal to the product of the number of residents of its members. Therefore, structures, classes, and tuples are also called Product Types.

Enumerations stand in stark contrast to this type. Here is an enumeration with three members:

enum PrimaryColor { 
    case red
    case yellow
    case blue
}
Copy the code

This type has three residents, each corresponding to an enumeration member. It is not possible to build it with values other than.red,.yellow, or.blue. So if we add a member with an associated value to this type, what happens to the number of residents? Let’s add a fourth member that allows us to specify a grayscale value between 0 (black) and 255 (white) :

enum ExtendedColor {
    case red
    case yellow
    case blue
    case gray(brightness: UInt8)}Copy the code

For.gray alone, the number of possible values is 256, so the number of residents for the entire enumeration is 3 + 256,259. In general, the number of residents of an enumeration is equal to the sum of the residents of all its members. This is why enumerations are called Sum Types.

Adding a field to a structure increases the number of possible states, and the number is usually very large. Adding a member to the enumeration adds only one more resident (if the member has an associated value, the increase is the number of residents of the associated value). This is a very useful feature for implementing secure code.

Pattern matching

In order to do anything useful with enumeration values, we usually have to examine the enumeration member and extract its associated value.

The most common way to check an enumerated value is to use the Switch statement, which allows us to compare the value to multiple candidates in a single statement. The switch statement has the added benefit of having a convenient syntax for comparing values to members and extracting associated values in one sitting. This mechanism is called pattern matching. Pattern matching is not unique to switch, but it is the most obvious use case.

Pattern matching is useful because it allows us to deconstruct a data structure through structure rather than content. Each branch of a switch statement consists of one or more patterns that match the input value.

A pattern describes the structure of a value. For example, in the example below, pattern.success (42, _) matches the success member of the enumeration with a pair of associated values, and the first element of the associated value is 42. The underscore in this pattern is a wildcard pattern that indicates that the second element can be any value. In addition to this normal match, we can extract parts of the associated values and bind them to a variable. Again, the pattern.failure(let error) matches the failure member and binds the associated value to a new local constant error:

let result: Result< (Int.String), Error> = . 
switch result {
case .success(42._) :print("Found the magic number!") 
case .success(_) :print("Found another number") 
case .failure(let error):
    print("Error: \(error)")}Copy the code

Take a look at the types of modes that Swift supports:

  1. Wildcard mode – The symbol is underscore :_. It matches any value and ignores it. It is used when one part of an association value is matched and the other part is ignored. In the code above, we have seen examples of using wildcards in the.success(42, _) schema. In switch statements,case _ is equivalent to the default keyword: both match any value, and it makes sense to use them as the last branch.
  2. Tuple pattern – matches tuples with a comma-separated list of subpatterns. For example, (let x, 0, _) matches a tuple of three elements, the second of which is 0, and binds the first element to x. The tuple pattern itself matches only the structure of a tuple, that is, only the number of elements separated by commas within parentheses. The matching of the tuple content is done through the subpatterns. (Let x, 0, _) has three sub-patterns: value-binding pattern, expression pattern, and wildcard pattern. This pattern is useful when you need to convert multiple values in a single Switch statement.
  3. Enumerator pattern – Matches the specified enumerator. It can include subpatterns to handle associated values, such as equality checking (.success(42)) or value binding (.failure(let error)). To ignore an associated value, you can use underscores in the subschema or remove the entire subschema. For example,.success(_) and.success are equivalent. This pattern is the only way if you want to extract a member’s association value, or if you just want to match members and ignore the association value. To compare with a particular member that has a particular associated value, you can use the == operator in the if statement as long as the enumeration implements the Equatable protocol.
  4. Value binding mode – Binds part or all of a matched value to a new constant or variable. Grammar islet someIdentifiervar someIdentifier Like this. The bound variable is scoped in the case statement block in which it is declared.

As a shorthand for binding multiple values in a single pattern, you don’t need to repeat let before each binding variable, just prefix the pattern with let. So the modes let (x, y) and (let x, let y) are the same. Note the subtle differences when using both value binding and equality matching in a single pattern: for example, the pattern (let x, Y) binds the first element of a tuple to a new constant, but for the second element, the pattern simply compares it to an existing variable y.

To combine value bindings with other conditions that bound values must meet, you can extend a value binding pattern with the WHERE clause. For example, pattern.success (let httpStatus) where 200.. <300 ~= httpStatus will only match values with SUCCESS and associated values within the specified range. It is important to note that because the WHERE clause is executed after the value binding, we can use the bound value in the clause.

If you include multiple schemas in a single branch, the number of value binding schemas in each schema must be the same, as well as the variable names and types used for each value binding.

enum Shape {
case line(from: Point, to: Point)
case rectangle(origin: Point, width: Double, height: Double) 
case circle(center: Point, radius: Double)}switch shape {
case .line(let origin, _), 
     .rectangle(let origin, _._), 
     .circle(let origin, _) :print("Origin point:", origin) 
}
Copy the code
  1. Optional value pattern – provides a syntactic sugar for matching and unpacking optional values using the familiar question mark syntax. Pattern let value? Equivalent to.some(let value), that is, it matches an optional value that is not nil and binds the unpacked value to a constant.

We can also use nil to match a none member of an optional value. This shorthand doesn’t use any compiler dark magic; the library overloads the ~= operator as a generic expression pattern for comparison to nil

  1. A value’s runtime type must be SomeType or a subclass of it. The pattern let value as SomeType performs the same check, and in addition converts the matched value to the specified type, whereas is only checks the type:
let input: Any = . 
switch input {
case let integer as Int: . // Integer is of type Int.
case let string as String: . // String is of type string.
default: fatalError("Unexpected runtime type: \ [type(of: input)")}Copy the code
  1. Expression patterns – By passing input values and patterns as parameters to pattern matching operators defined in the standard library(~ =)To match the expression. For the type that implements the Equatable protocol,~ =The default implementation of forward to= =This is also how simple equation checking works in schema.

The library also provides an overload of ~= for ranges. This provides a nifty syntax for checking whether a value is within a range, especially when combined with one-sided ranges. The following switch statement checks if a number is positive, negative, or zero:

let randomNumber = Int8.random(in: .min.(.max)) 
switch randomNumber {
case ..<0: print("\(randomNumber) is negative") 
case 0: print("\(randomNumber) is zero")
case 1.: print("\(randomNumber) is positive") 
default: fatalError("Can never happen")}Copy the code

Note that because the compiler couldn’t be sure that the three specific branches covered all possible inputs (even if they did), it forced us to add a default branch. The switch statement must always be exhaustive.

Pattern matching in other contexts

While pattern matching is the only way to extract associated values from enumerations, it is not specific to enumerations or switch statements. In fact, even an assignment statement as simple as let x = 1 can be thought of as a value binding pattern: the variable on the left of the assignment operation matches the expression on the right. Some other examples of pattern matching include:

  1. Destruct tuples in assignment, e.g., let (word, PI) = (“Hello”, 3.1415) or iterated for (key, value) in dictionary {… }. Note that in the for loop example, we did not use let to indicate that this is a value binding. Because in this case, all identifiers are value bound by default. The for loop also supports where clauses, for example, for n in 1… 10 where n.isMultiple(of: 3) { … } the body of the loop is executed only if n is 3, 6, and 9.

  2. Use wildcards to ignore values we are not interested in, for example, for _ in 1… 3 will execute the loop three times without creating a variable for the loop count, or _ = someFunction() will avoid the compiler warning of “unused result” when we want to execute a function with side effects.

  3. Catch an error in a catch clause: for example, do {… } catch let error as NetworkError { … }.

  4. The IF case and Guard case statements are similar to switch statements with a single branch. Although in many cases we prefer switch statements to take advantage of compiler completeness checks, these two statements are occasionally useful because they require fewer lines than switch.

The syntax of if/guard case [let] is often a big hindrance to newcomers to Swift. The reason we think this is the case is that it uses the assignment operator for basic comparison operations and can contain no value bindings. For example, the following code tests whether an enumerated value is equal to a particular member, while ignoring its associated value:

let color: ExtendedColor = .
if case .gray = color { 
    print("Some shade of gray")}Copy the code

You can think of the assignment operator here as “making a pattern match between the value to the right of the operator and the pattern to the left.” The syntax becomes clearer when value binding is introduced. The syntax is the same. You just add let or var to the previous statement:


if case .gray(let brightness) = color { 
    print("Gray with brightness \(brightness)")}Copy the code
  1. For case and while case loops work similarly to if case. They allow you to execute the body of the loop only if the pattern matches successfully.

Finally, sometimes the argument lists of closure expressions look like patterns, because they also support a kind of tuple deconstruction. For example, for the dictionary map method, even if the parameter passed is specified as a single Element, We can use a (key, value) argument list in the closure that performs the transformation (Dictionary.Element is of type a (key, value) tuple).

dictionary.map { (key, value) in ... }

The (key, value) here looks like a tuple, but it’s really just a two-element argument list. The reason we can unpack tuples into argument lists here is because the compiler has special treatment for this, not because of pattern matching. Without this feature, we would have to use something like {element in… }, and then use two lines of code in the closure to break element (which is now really a tuple) into key and value.

Use enumerations for design

Because enumerations belong to a different category than structures and classes, the design pattern that applies to them is different. And because true summing types are a relatively uncommon (if rapidly evolving) feature in mainstream programming languages, you may not be used to them compared to traditional object-oriented approaches. So let’s take a look at some of the patterns you can use in your code that take full advantage of enumerating various features. We break them down into six main areas:

  1. Completeness of Switch statements
  2. It is impossible to create an illegal state
  3. Use enumerations to implement states
  4. Choose between enumerations and structs
  5. Similarities between enums and protocols
  6. Use enumeration to implement recursive data structures

Completeness of Switch statements

In most cases, switch is just a more convenient syntax for if case statements with multiple else if case conditions. In addition to the syntax differences, there is one important difference: A switch statement must be complete, that is, its branches must cover all possible input values. The compiler also enforces this completeness.

Completeness checking is an important tool for implementing secure code and keeping it correct when the program changes. Every time you add a member to an existing enumeration, the compiler issues a warning in all places where the switch statement is used on the enumeration, reminding you that you need to process the new member. The if statement does not perform a completeness check, nor does it apply to a switch statement with a default branch – since default can match any value, such a switch statement would never be complete.

Therefore, it is recommended to avoid using the default branch in switch statements as much as possible. Of course, it can’t be completely avoided, because the compiler is sometimes not smart enough to determine if a set of branches is really complete. The compiler can only err on the side of security, that is, it will never report an incomplete set of schemas as complete.

However, when the switch enumerates, False negatives do not occur. For the following types, completeness checks can be trusted

  • Boolean value
  • Enumeration, as long as any correlation value can be detected to be complete, or you match any correlation value with a pattern

(For example, wildcards or value bindings)

  • A tuple whose member type can be detected to be complete

In addition, the compiler verifies the weight of each schema in a Switch statement. If all patterns before a pattern already cover all cases, the compiler can prove that the pattern will never be matched, thus issuing a warning for that case.

The biggest benefit of completeness checking is if you want the enumeration and the code that uses it to evolve simultaneously, that is, every time a new member is added to the enumeration, all switch enumeration code can be updated at the same time. If you have access to the source code for your program’s dependencies, and the program and dependencies are compiled together, you can really take advantage of the benefits of completeness checking. But things get more complicated when a library is distributed in binary form, and programs that use the library must be prepared to use newer versions when compiled. In this case, you need to always include a default branch, even though the branch covers all existing cases.

It is impossible to create an illegal state

Why use a statically typed language like Swift? Performance is one of them: the more the compiler knows about the types of variables in a program, the faster it usually produces code.

An equally important reason is that the type system can guide developers on how they should use the API. If you pass an incorrect type to a function, the compiler will immediately report an error. This technique can be called compiler-driven development – using the compiler as a tool to find the right solution by using type information:

  • Carefully choosing the input and output types of a function reduces the chance of misuse of the function, because the type establishes an “upper bound” on the behavior of the function. Enumerations are often a perfect tool when it comes to defining precisely the range of allowed values.

  • Static type checking completely prevents certain types of errors; If code violates constraints set by the type system, it will not compile and will never be handled at run time.

  • Types are like documents that never expire. It’s not the same as comments in that people might forget to update comments as the code gets updated, but because a type is part of the program, it’s always up to date.

Here are some tips to get the most help from the compiler when designing custom types: Use types so that they cannot represent illegal states. As you saw in the previous section on summation types and product types, after adding a member to an enumeration, the type adds only one possible value. You can’t get much more granular than that, and enumerations are very useful for this purpose.

A good example of this is Optional. By adding the None member and adding a generic parameter as the wrapper type, you can accurately express the absence of a value without relying on the sentinel value.

Let’s look at an example of an API that is harder to use than you might think because it doesn’t meet the criteria above. In Apple’s iOS SDK, a common pattern for asynchronous operations (such as executing a network request) is to pass a final-time processing method (a callback function) to the asynchronous operation you invoke. When the task is complete, this method calls the previously passed callback function with the result of the task as an argument. Because most asynchronous operations have the potential to fail, typically the result of a task is either a success value (for example, a server response) or an error.

Take a look at the geolocation API in Apple’s Core Location framework. You pass a string representing an address and a callback to the API. The API will ask the server to return all the placemark objects that match the address. It then calls the incoming callback with either the resulting list of landmark objects or an error:

class CLGeocoder {
    func geocodeAddressString(_ addressString: String.completionHandler: @escaping ([CLPlacemark]?.Error?). ->Void) 
    // ...
}
Copy the code

Look at the type of callback function, ([CLPlacemark]? , Error?) – > Void. Both of its arguments are optional. This means that the function can present the caller with four possible states :(.some,.none), (.none,.some), (.some,.some), or (.none,.none) (this is a simplified perspective; Because there are actually infinitely many possible values of some, but here we only care if they are non-empty. The problem with the four legal states is that only the first two states make sense in practice. What should developers do if they receive both a list of landmarks and an error? Even worse, what if both values return nil? The compiler can’t help you here because the type isn’t precise enough.

So far, because Apple has probably been careful to implement this method so that it never returns any of these invalid states, in practice the above problem never occurs.

If these two optional values are replaced with a Result<[CLPlacemark], Error> type, the geolocation API will become more developer-friendly:

extension CLGeocoder {
    func geocodeAddressString(_ addressString: String.completionHandler: @escaping (Result"[CLPlacemark].Error- > >)Void) {
    // ...}}Copy the code

The Result type indicates that the operation will either succeed or fail, but neither state can exist at the same time, and neither state can exist at the same time. By using a type that cannot express invalid state, the API is made easier to use, and because the compiler disables many cases, a potential set of errors does not occur naturally. Since many of Apple’s iOS apis are implemented in Objective-C, they don’t take full advantage of Swift’s type system, since objective-C enumerations have no such thing as associated values. But that doesn’t mean we can’t do better with Swift.

Use enumerations to implement states

How to implement state in our programs without making it illegal is another major aspect of programming. At a given point in time, the state of a program consists of the contents of all variables plus (implicitly) its current execution state, which threads are running and which instructions they are executing. A state needs to “remember” many things, such as the mode a program is in, the data being displayed, the user interaction currently being processed, etc. In addition to the most simple program, all programs are stateful: a specific instruction is executed, is what happens next depends on the current state of the system in (HTTP is a stateless protocol example, this means that for the same client, the server must not consider when processing the current request previous request. Between multiple requests, Web developers must use features like cookies to remember state. Even if HTTP is stateless, a program that handles HTTP requests is still stateful because it needs to maintain internal state.

As the program runs, it changes state in response to external events such as user interactions or incoming data from the network. This can happen implicitly without the developer thinking too much about it – after all, state changes happen all the time. But as programs become more complex, it is best to consciously define the states that a program (or one of its subroutines) can exist in, as well as the legal transitions between those states. The set of states that a system can exist is also called its state space.

Try to keep your program’s state space as small as possible. The smaller the state space, the easier it is for you as a developer to do your job – a smaller state space reduces the number of cases your code needs to handle. Because enumerations have a finite number of states, they are ideal for implementing states and transitions between states. And because each enumerated state, or each member, has its own data (in the form of associated values), it’s easy to disallow combinations that express illegal states.

Suppose we are implementing a chat program. When a user opens a chat channel, the program should display a spinner during a request for a list of messages from the network. When the network request completes, the UI either displays a list of received messages or an error if the network fails. Let’s first consider how to implement the state of a program in the traditional way without enumerations (technically, we still use enumerations because we’ll use optional values, but you get the idea here). We can use three variables, one of which is a Boolean value, which we set to true to indicate that we are currently in the process of a network request, and the other two are optional values for message lists and errors:

struct StateStruct {
    var isLoading: Bool
    var messages: [Message]? var error: Error?
}
// Set the initial state.
var structState = StateStruct(isLoading: true, messages: nil, error: nil)
Copy the code

Both the messages and error variables should be nil when the message list is loaded, and then one of them should be assigned when the network request completes. Both variables should never be non-nil at the same time, and when neither of them is nil, isLoading should never be true.

Recall the discussion about how to determine the number of inhabitants a type can have in the summation and product types. A StateStruct structure is a product type with 2 × 2 × 2 = 8 possible states: any combination of a Boolean true or false and none or some of two optional values. This is actually a problem, because our program only needs to handle three of the eight states: load, display a list of messages, or display an error. The other five states should all be invalid combinations that wouldn’t happen if we implemented the program correctly, but we can’t expect any help from the compiler to avoid creating an invalid state.

Now, let’s implement our state with a custom enumeration that has three values: loading, loaded, and failed:

enum StateEnum {
    case loading
    case loaded([Message]) case failed(Error)}// Set the initial state
var enumState = StateEnum.loading
Copy the code

Because you no longer have to worry about properties that are irrelevant to the initial state, the code for setting the initial state becomes clearer. In addition, I eliminated the possibility of transitioning to an invalid state altogether. Because each state has its own associated data, the types of associated values loaded and failed need not be optional. Therefore, it is impossible to transition to the failed state unless there is indeed an Error value in the code. (For loaded, it is a little unclear because you can always assign an empty array to it, but this is not something you would do by accident.) When a program is in a particular state, we can be sure that the data needed for that state is already available. StateEnum enumeration can be used as the basis for a state machine.

The structure used and the enumeration that replaces it later are not the only ways to achieve this state. In fact, the statestruct. isLoading property is redundant because in our design, isLoading should only be true if messages and error are both nil. We can make isLoading a calculated property without losing anything:

struct StateStruct2 {
    var messages: [Message]? var error: Error?
    var isLoading: Bool {
        get { return messages = = nil && error = = nil } 
        set {
            messages = nil
            error = nil}}}Copy the code

This reduces the number of possible states from eight to four, leaving only one invalid state (when messages and error are non-nil) – not perfect, but better than the version of the structure we started with.

This pattern, which has two mutually exclusive optional values, was replaced ([CLPlacemark]?) with Result<[CLPlacemark], Error> in the previous section. , Error?) In the example. Using the same method for our current example would Result<[Message], Error>, but note that the two cases are not exactly the same; The chat program requires a third state – “loaded”, in which messages and error are nil. We can do this by making Result an optional value (recall that encapsulating a type as an optional value always adds a resident to that type), so here’s another way to represent our state:

/// nil means the state is "loaded".
typealias State2 = Result"[Message].Error>?
Copy the code

This is equivalent to our custom enumeration, that is, the number of states and the payload for each state are the same (Result<[Message]? Error> is an equivalent version. But semantically, this is a poor solution, because it doesn’t immediately make it clear that a value of nil means “loaded” state.

In short, enumerations are a great choice for implementing states. It largely prevents invalid states and puts the entire state of a subsystem (or even an entire program) in a single variable, making state transitions less error-prone. In addition, the completeness of the switch statement allows the compiler to indicate the code path to update when you add a new state or change the associated value of an existing state.

Choose between enumerations and structs

An enumerated value represents exactly one of all members (plus its associated value), but the value of a structure represents the values of all of its attributes.

Enumerations and structs each implement a data type for analyzing events. Here is the version of enumeration:

enum AnalyticsEvent {
    case loginFailed(reason: LoginFailureReason) 
    case loginSucceeded
    . // More enumerated values.
}
Copy the code

Extend this enumeration by adding several calculated properties, in which the switch enumerates and returns the data the user needs, namely strings and dictionaries that should actually be sent to the server:

extension AnalyticsEvent { 
var name: String {
        switch self {
        case .loginSucceeded:
            return "loginSucceeded" 
        case .loginFailed:
            return "loginFailed" 
            // ... more cases.}}var metadata: [String: String] {
        switch self {
        // ...}}}Copy the code

Alternatively, we could implement the same function with a structure, storing its name and metadata in two properties. We provide static methods (corresponding to each of the enumerated values above) to create instances of specific events:

struct AnalyticsEvent {
    let name: String
    let metadata: [String : String]
    private init(name: String.metadata: [String: String] = [:]) { 
        self.name = name
        self.metadata = metadata
    }
    static func loginFailed(reason: LoginFailureReason) -> AnalyticsEvent { 
        return AnalyticsEvent(
            name: "loginFailed"
            metadata: ["reason" : String(describing: reason)] )
    }
    static let loginSucceeded = AnalyticsEvent(name: "loginSucceeded") 
        // ...
    }
Copy the code

Since we declared the initialization method private, the exposed public interface is the same as the enumeration version: enumerations expose members such as.loginfailed (reason:) or.loginSucceeded, while structs expose static methods and properties. Name and metadata are available in both versions, except that they are computed properties in enumerations and stored properties in structures.

However, each version of the AnalyticsEvent type has unique features that can be a plus or a plus, depending on what your requirements are:

  • If we make the access level of the constructor’s initialization method internal or public, we can extend the constructor in other files or even in other modules by adding static methods or properties to add new parsing events to the API. Versions of enumerations don’t do this: you can’t add new members to the enumeration anywhere else.
  • Enumerations allow for more precise implementation of data types; It can only represent one of the predefined members, but the structure can represent an infinite number of values because of these two attributes. The precision and security of enumerations comes in handy if you want to do further processing on events (for example, merge sequences of events).
  • A structure can have private “members” (that is, static methods or static properties that are not visible to all consumers), and the visibility of the members in an enumeration is always the same as the enumeration itself.
  • You can use the switch statement for enumerations and take advantage of the completeness of the statement to ensure that no event type is missed. Because of this rigor, adding a new event type to the enumeration might break the source code for users of the API, but you can add static methods to the structure for the new event type without worrying about affecting the rest of the code.

Similarities between enums and protocols

In the section on summation types and product types, it was mentioned that enumerations are not the only structures that can represent “one” relationships; Protocols can also be used for this purpose.

enum Shape {
    case line(from: Point, to: Point)
    case rectangle(origin: Point, width: Double, height: Double) 
    case circle(center: Point, radius: Double)}Copy the code

A shape can be a line segment, rectangle or circle. We added a render method to the extension to render these shapes into the context of Core Graphics.

extension Shape {
    func render(into context: CGContext) {
        switch self {
        case let .line(from, to): // ...
        case let .rectangle(origin, width, height): // ...
        case let .circle(center, radius): // ...}}}Copy the code

Alternatively, you can define a protocol called Shape, and any type that implements this protocol can render itself into a Core Graphics context:

protocol Shape {
    func render(into context: CGContext)
}
Copy the code

The various Shape types previously represented as members are now concrete types that implement the Shape protocol. Each type implements its own render(into:) method:

struct Line: Shape { 
    var from: Point 
    var to: Point
    func render(into context: CGContext) { / *... * /}}struct Rectangle: Shape { 
    var origin: Point
    var width: Double
    var height: Double
    func render(into context: CGContext) { / *... * /}}Copy the code

While functionally equivalent, it is interesting to consider how enumerations and protocols organize code and how they can be extended for new functionality. Enumeration-based implementations are grouped by method: the CGContext rendering code for all shape types is in a single switch statement in the Render (into:) method. Protocol-based implementations, on the other hand, are grouped by “members” : each concrete type implements its own Render (into:) method, which contains each shape-specific rendering code.

This has important extensibility implications: in the version of enumeration, we can easily add new rendering methods in later Shape extensions – rendering as an SVG file, for example, even in different modules. However, unless we can control the source code that contains enumeration declarations, we cannot add new shapes to enumerations. And even though we can change the definition of an enumeration, adding a new member is a source-breaking change to all the methods that switch the enumeration in the implementation.

On the other hand, new shapes can be easily added in versions of the protocol: just create a new structure and have it implement the Shape protocol. But, if not modify existing Shape agreement, we cannot add a new rendering method, because we can not add new agreement requires outside agreement statement (we can be to add a new method in the extension of the agreement, but, as will be seen in this chapter, extension methods are usually not suitable for add new features to the agreement of this requirement, Because these methods are not distributed dynamically).

It turns out that enumerations and protocols have complementary strengths and weaknesses in this case. Each solution is scalable in one dimension and inflexible in the other. These extensibility differences between enumerations and protocols are less important when apis are declared and used in the same module. But if you are implementing library code, you should consider which dimension of extensibility is more important: adding new members or adding new methods.

Use enumeration to implement recursive data structures

Enumerations are great for implementing recursive data structures, that is, data structures that “contain” themselves. Think of tree structure: one tree has multiple branches, and each branch is actually another tree divided into multiple sub-trees, and so on, until you reach the leaves. Many common data formats are tree structures, such as HTML, XML, and JSON.

As an example of a recursive data structure, let’s implement a data structure simpler than a tree: a singly Linked List. The nodes of a list can be one of two things: the node contains a value and a reference to the next node, or the node represents the end of the list. This binary relationship strongly implies that a sum type (that is, an enumeration) is a good fit for defining the data structure. Here is the definition of a List whose element type is a generic parameter:

enum List<Element> {
case end
indirect case node(Element, next: List<Element>)}Copy the code

Note the indirect keyword, which is required for the code to compile. The indirect tells the compiler to list node members as a reference, thereby making the recursion work.

To understand why, recall that enumerations are value types. Value types cannot contain themselves, because if allowed, an infinite recursion would be created when evaluating the type size. The compiler must be able to determine a fixed and finite size for each type. It is possible to solve this problem by using the member that needs recursion as a reference, because the reference type adds a layer of indirection to it; And the compiler knows that the storage size of any reference is always 8 bytes (on a 64-bit system).

The indirect syntax applies only to enumerations. If it is not available or wants to implement a similar recursive structure on its own, the same behavior can be achieved by encapsulating the recursive value in a class, since this is manually creating an indirection layer. Here is a generic class that can encapsulate any value as a reference:

final class Box<A> {
    var unbox: A
    init(_ value: A) { self.unbox = value }
}
// Using this class, we can implement the previous List enumeration without an indirect:
enum BoxedList<Element> {
    case end
    case node(Element, next: Box<BoxedList<Element>>)
}
Copy the code

This enumeration is not very convenient because we have to do the boxing and unboxing manually all the time, but it is almost equivalent to the List type. We say “almost” because the indirection layer moves from the entire node member to the next Element of the associated value, and an identical solution would be to wrap the entire associated value in a Box, like this: Case node(Box<(Element, next: BoxedList Element < > >).

You can also add an indirect to the enumeration declaration itself, for example, indirect enum List {… }. This is a handy syntax for turning any member of an enumeration with an associated value into a reference. (Indirect applies only to an associated value and never to the tag bits that an enumeration uses to distinguish between members.) For our List type, these two versions are equivalent because there is no case that members with associated values should not be stored indirectly.

Discuss how to use List enumerations. Add an element to the list by creating a new node and setting the next value of the new node to the current node:

let emptyList = List<Int>.end
let oneElementList = List.node(1, next: emptyList)
// node(1, next: List<Swift.Int>.end)

extension List {
/// add a node with the value 'x' to the head of the list.
/// Then return the entire list.
    func cons(_ x: Element) -> List { 
    return .node(x, next: self)}}// A list of (3, 2, 1) elements.
let list = List<Int>.end.cons(1).cons(2).cons(3) 
/* node(3, next: List
      
       .node(2, next: List
       
        .node(1, next: List
        
         .end))) */
        
       
      
Copy the code

Chaining syntax can clearly indicate how a list is constructed, but it is a little ugly. We can make a list to realize ExpressibleByArrayLiteral agreement, make its can use an array literal to initialize a linked list. In a concrete implementation, we first reverse the array as input (since the list is built from the end), and then, starting with the.end node, use reduce to add elements to the list one by one:


extension List: ExpressibleByArrayLiteral { 
    public init(arrayLiteral elements: Element...). {
        self = elements.reversed().reduce(.end) { partialList, element
            in partialList.cons(element)
        } 
    }
}
let list2: List = [3.2.1]
/* node(3, next: List
      
       .node(2, next: List
       
        .node(1, next: List
        
         .end))) */
        
       
      
Copy the code

Another interesting feature of this list type is its persistence. Nodes are immutable – once created, you cannot change them. Adding an element to a linked list does not duplicate the list; It just gives you a new node that links to the head of the existing list.

This means that two lists can share the same tail. If you can modify the list (for example, remove the last node, or update the element in the preserved node), then the sharing is problematic – X may change the list, and this change affects Y.

However, you can define mutating in a List to push and pop elements:

extension List {
    mutating func push(_ x: Element) {
        self = self.cons(x) 
    }
    mutating func pop(a) -> Element? { 
        switch self {
        case .end: return nil
        case let .node(x, next: tail):
            self = tail
            return x 
        }
    } 
}
Copy the code

These variables don’t really change the list itself. Instead, they simply change the part of the list that the variable points to to another value:

var stack: List<Int> = [3.2.1] 
var a = stack
var b = stack
a.pop() // Optional(3) 
stack.pop() // Optional(3) 
stack.push(4)
b.pop() // Optional(3) 
b.pop() // Optional(2) 
stack.pop() // Optional(4) 
stack.pop() // Optional(2)
Copy the code

Mutable methods allow us to modify the value that self points to, but the value itself (the node in the list) is immutable. In this sense, a variable has become an iterator in a linked list through the use of an indirect.

In practice, these nodes are located in memory pointing to each other, and they occupy a certain amount of space that we want to reclaim if we don’t need them anymore. Swift uses automatic reference counting (ARC) to manage this and frees up memory for nodes that are no longer in use.

Raw Value

Sometimes you need to associate each member of an enumeration with a number or some other type of value. This is how enumerations in C or Objective-C work by default – in fact, at the bottom they are just integers. Swift enumerations are not interchangeable with arbitrary integers, but we can optionally declare a 1-to-1 mapping between the members of the enumeration and the so-called primitive values. This is useful for interoperating with the C API, or encoding an enumerated value as a data format like JSON.

Specifying a raw value for an enumeration requires that the enumeration name be followed by the type of the original value separated by a colon. Each member is then assigned a raw value using assignment syntax. Here is an example of an enumeration whose original value is Int to represent HTTP state:

enum HTTPStatus: Int { case ok = 200
    case created = 201 // ...
    case movedPermanently = 301 // ...
    case notFound = 404 // ...
}
Copy the code

The original value of each member must be unique. If you do not provide raw values for one or more members, the compiler tries to choose reasonable defaults. In this case, we don’t have to explicitly assign a primitive value to the Created member; The compiler selects the same value for Created as it does now, -201, by incrementing the original value of the previous member.

RawRepresentable agreement

A type that implements the RawRepresentable protocol gets two new apis: a rawValue property and a failable initialization method (init? (rawValue:)). Both apis are declared in the RawRepresentable protocol (which the compiler automatically implements for enumerations with raw values):

/// a type that can be converted to the associated original value.
protocol RawRepresentable {
    /// The type of the original value, such as Int or String.
    associatedtype RawValue
    init?(rawValue: RawValue)
    var rawValue: RawValue { get}}Copy the code

Because for every RawValue, there may be a value that is invalid for the type that implements the protocol, the initialization method is failable. For example, only some integers are valid HTTP status codes; For all other inputs, httpstatus.init? RawValue: must return nil:

HTTPStatus(rawValue: 404) // Optional(HTTPStatus.notFound) 
HTTPStatus(rawValue: 1000) // nil 
HTTPStatus.created.rawValue / / 201
Copy the code

Manually implement RawRepresentable

The syntax above, which assigns a primitive value to an enumeration, applies only to a limited set of types: String, Character, any integer, or floating point. This covers some use cases, but it doesn’t mean that types can only be these. Because the syntax above is just a syntactic sugar for implementing RawRepresentable, you can always choose to implement the protocol manually if you need more flexibility.

The following example defines an enumeration that represents points in a logical coordinate system with x and y coordinates between -1 (left/down) and 1 (right/up). This coordinate system is somewhat similar to CALayer’s anchorPoint property in Apple’s Core Animation framework. We use a pair of integers as the type of the original value, and because the syntactically generated RawRepresentable sugar does not support tuple types, we implement RawRepresentable manually:

enum AnchorPoint { 
    case center
    case topLeft 
    case topRight 
    case bottomLeft 
    case bottomRight
}
extension AnchorPoint: RawRepresentable {
    typealias RawValue = (x: Int, y: Int)
    var rawValue: (x: Int, y: Int) { 
        switch self {
        case .center: return (0.0)
        case .topLeft: return (-1.1)
        case .topRight: return (1.1) 
        case .bottomLeft: return (-1.-1) 
        case .bottomRight: return (1.-1)}}init?(rawValue: (x: Int, y: Int)) { 
        switch rawValue {
        case (0.0) :self = .center
        case (-1.1) :self = .topLeft
        case (1.1) :self = .topRight 
        case (-1.-1) :self = .bottomLeft 
        case (1.-1) :self = .bottomRight 
        default: return nil}}}/// This code is exactly what the compiler generated for us when it automatically synthesized RawRepresentable
/// For users using this enumeration, the behavior is the same in both cases:

AnchorPoint.topLeft.rawValue // (x: -1, y: 1) 
AnchorPoint(rawValue: (x: 0, y: 0)) // Optional(AnchorPoint.center) 
AnchorPoint(rawValue: (x: 2, y: 1)) // nil
Copy the code

One thing to be aware of when manually implementing RawRepresentable is assigning values with duplicate original values. The autocomposited syntax is unique in evaluating the raw value – repeating it will raise a compilation error. But in a manual implementation, the compiler does not prevent you from returning the same original value from multiple members. There may be good reasons to use duplicate original values (for example, when multiple members are synonymous with each other, or for backward compatibility), but it should be the exception. Switch an enumeration is always matched against members, not raw values. In other words, even if two members have the same original value, you cannot match one with the other.

Let structures and classes implement RawRepresentable

Also, RawRepresentable is not limited to enumerations; You can also have a structure or class that implements the protocol. Implementing the RawRepresentable protocol is often a good choice for simple encapsulated types introduced to protect type security. For example, a program might internally use a string to represent a user ID. Instead of using String directly, it’s better to define a new UserID to prevent accidental confusion with other String variables. You may also need to initialize a UserId instance with a string and extract its string value; RawRepresentable fits these needs well:

struct UserID: RawRepresentable { 
    var rawValue: String
}
Copy the code

The rawValue property here satisfies one of the two requirements for implementing the RawRepresentable protocol, but where is the implementation of the second requirement, the initialization method? It is provided by the feature of the member initialization method automatically generated by the Swift structure. The compiler is smart enough to treat an implementation of an init(rawValue:) method that does not fail as an implementation of the failable initialization method that the protocol requires. This has a nice side effect that we don’t have to deal with optional values when creating a UserID instance with strings. If we want to validate the input string (perhaps not all strings are valid user ids), we must init? (rawValue:) provides our own implementation.

An internal representation of the original value

Aside from adding the RawRepresentable API and automatic Codable composition, enumerations that actually have raw values are no different from all other enumerations. In particular, enumerations with original values preserve their complete type identity. Unlike C, where any integer value can be assigned to a variable of an enumeration type, a Swift enumeration with an Int raw value does not “become” an integer. An instance of an enumerated type can have only one of its members. The only way to get the rawValue is by calling rawValue and init, right? (rawValue:) both apis.

Having the original value also does not change how the enumeration is represented in memory. You can define an enumeration with a raw value of String and verify this by looking at its type size:

enum MenuItem: String { 
    case undo = "Undo" 
    case cut = "Cut"
    case copy = "Copy"
    case paste = "Paste" 
}
MemoryLayout<MenuItem>.size / / 1
Copy the code

The size of the MenuItem type is only 1 byte. This tells us that a MenuItem instance does not store raw values internally – if it did, its size must be at least 16 bytes (the size of a String on a 64-bit platform). The compiler-generated rawValue implementation acts like a calculated property, similar to the implementation of AnchorPoint we showed above.

Enumeration value

We’ve already discussed what a resident of a type is: the set of all possible values that an instance of a type can have. The need to operate on these values as a collection is often useful, such as iterating or counting. The CaseIterable protocol implements this by adding a static attribute, allCases (that is, calling this attribute not on instances, but on types):

/// A type that provides a set of all its values
protocol CaseIterable {
associatedtype AllCases: Collection where AllCases.Element = = Self
    static var allCases: AllCases { get}}Copy the code

For enumerations without associated values, the compiler automatically generates code that implements CaseIterable; All we have to do is add the agreement to the declaration.

enum MenuItem: String.CaseIterable { 
    case undo = "Undo"
    case cut = "Cut"
    case copy = "Copy"
    case paste = "Paste" 
}
Copy the code

Because the allCases attribute is of type Collection, it has all the usual properties and functionality that you know from arrays and other Collection types. In the following example, we use allCases to get the number of all menu items and convert them to strings suitable for display in the user interface:

MenuItem.allCases
// [MenuItem.undo, MenuItem.cut, MenuItem.copy, MenuItem.paste]
MenuItem.allCases.count / / 4
MenuItem.allCases.map { $0.rawValue } // ["Undo", "Cut", "Copy", "Paste"]
Copy the code

Like other protocols like Equatable and Hashable that the compiler automatically synthesizes, the biggest benefit of CaseIterable’s auto-synthesizing code isn’t that the code itself is difficult (it’s easy to implement the protocol manually), It’s that the code generated by the compiler is always up to date – it’s easy to forget that with a manual implementation, you have to manually update your implementation every time you add or remove a member.

The CaseIterable protocol does not specify a specific order of the values in the collection returned by allCases, but the CaseIterable documentation guarantees that the order of the values in the collection is the same as the order in which they were declared.

Implement CaseIterable manually

CaseIterable is particularly useful for ordinary enumerations with no associated values, and auto-compiler composition only supports this type. This makes sense because adding an associated value to an enumeration might make the number of residents of that enumeration infinite. But we can always implement this protocol manually as long as we can implement a method to generate a collection of all the inhabitants. In fact, this protocol is not limited to enumerations. Although the names CaseIterable and allCases imply that this feature is primarily used for enumerations (no other type has the concept of members), the compiler has no problem with a structure or class that implements this protocol.

The following code implements CaseIterable manually on one of the simplest types Bool:

extension Bool: CaseIterable {
    public static var allCases: [Bool] {
        return [false.true]}}Bool.allCases // [false, true]
Copy the code

Some integer types are also good choices. Note that the return type of allCases does not have to be array – it can be any type that implements Collection. Generating an array of all possible integers is wasteful when a range can represent the same set with less memory:

extension UInt8: CaseIterable {
    public static var allCases: ClosedRange<UInt8> {
        return .min . .max 
    }
}
UInt8.allCases.count / / 256
UInt8.allCases.prefix(3) + UInt8.allCases.suffix(3) // [0, 1, 2, 253, 254, 255]
Copy the code

Similarly, if you want to implement CaseIterable for a type with a large number of inhabitants, or if generating a value for a type is expensive, consider returning a LazyCollection so that you don’t have to perform unnecessary operations ahead of time.

Fixed and unfixed enumerations

One of the best things about enumerations is the completeness that comes with switching them. It is obvious that completeness checks can only be performed if, at compile time, the compiler knows all the possible members of an enumeration. This is easy to do when the declaration of the enumeration and the switch are in the same module. This is also easy to do if the declaration of the enumeration is in another library, but that library is compiled with our code (recompiling the declaration of the enumeration and our own code each time a member is added or removed, allowing the compiler to re-examine all relevant switch statements).

In some cases, however, the enumeration we use is in a library linked to our program in binary form. The standard library is the most obvious example: although the source code for the standard library is open source, we typically use binaries that come with Swift distributions or operating systems. So do some of the other libraries that come with Swift, including Foundation and Dispatch. Finally, Apple and others want to distribute Swift’s libraries in binary form.

Suppose we want to deal with a DecodingError instance in our code. It is an enumeration that, starting with Swift 5.0, has four members to represent different error conditions:


let error: DecodingError = .
// Completeness checks are done at compile time and may not be done at run time.
switch error {
case .typeMismatch: . 
case .valueNotFound: . 
case .keyNotFound: . 
case .dataCorrupted: . 
}
Copy the code

Additional members will likely be added to this enumeration in future Swift releases as the Codable system expands. But if we build an app that includes the code described above and send the app to users, those users may end up running the executable sent to them on a newer operating system with a newer version of Swift that includes a new DecodingError member. In this case, our program crashes because it encounters an error condition that it cannot handle.

Enumerations that may add new members in the future are called immutable. In order for programs to be able to guard against such changes to non-fixed enumerations, switching non-fixed enumerations in one module in another must always include a default clause to handle such cases in the future. In Swift 5.0, if you ignore the default branch, the compiler will only issue a warning (not an error), but this is just a stopgap to allow you to migrate your existing code easily. Warnings will become errors in future versions.

If you ask the compiler to fix this warning for you, you’ll notice that it adds an @unknown attribute to the default branch:

switch error { .
case .dataCorrupted: . 
@unknown default:
// Handle unknown cases.
. 
}
Copy the code

At run time, @Unknown Default behaves just like a normal default clause, but it is also a signal to the compiler that the default branch is only intended to handle cases of members that are not known at compile time. We still get a warning if the default branch matches a member known at compile time. This means that for a future new library interface, we can still benefit from completeness checks when recompiling the program. If a new member has been added to the library’s API since the last update, we get a warning to update all relevant Switch statements to explicitly handle the new member. @Unknown Default gives you the best of both worlds: compile time completeness checking and run time security.

In Swift 5.0, the distinction between fixed and non-fixed enumerations applies only to the standard library. The standard library and overlapping sections are compiled using a special Resilience mode, which is triggered by the -enable-Resilience compiler flag. Enumerations in elastic libraries (that is, libraries designed to maintain binary compatibility between versions) are non-fixed by default. There is also an attribute, @_frozen, which does not appear in the document and is used to declare a particular enumeration fixed. By using this property, the library developer is making a commitment never to add new members to the tagged enumeration, which would break binary compatibility.

Tips and tricks

  • Try to avoid using nested switch statements. You can use tuples to match multiple values at once. For example, if you were to set a variable based on two Boolean values, matching one after the other would require an embedded switch statement, which would quickly make the code ugly.

  • Avoid naming members with None or some. In the context of pattern matching, conflicts may occur with Optional members.

  • Use backquotes for members named with reserved keywords. If you use certain keywords for member names (for example, default), the type checker will generate an error because it cannot parse the code. You can use it by enclosing the name in backquotes:

enum Strategy {
    case custom
    case `default` // Backquotes are required.
}

// This has the advantage of eliminating the need for backquotes where the type checker can disambiguate them. The following code is fully valid:
let strategy = Strategy.default
Copy the code
  • Members can be used just like factory methods. If a member has an associated value, the enumerated value alone forms a function with the signature (AssocValue) -> Enum. The following enumeration is used to represent a color in one of two color Spaces (RGB or greyscale) :
enum OpaqueColor {
    case rgb(red: Float, green: Float, blue: Float) 
    case gray(intensity: Float)}OpaqueColor.RGB is one with threeFloatThe arguments and return types of type areOpaqueColorFunction:OpaqueColor.rgb // (Float, Float, Float) -> OpaqueColor

// We can also pass these functions to higher-order functions such as map.
// In the following code, we pass the member to the map as a factory method and create a gradient grayscale color from black to white:

let gradient = stride(from: 0.0, through: 1.0, by: 0.25).map(OpaqueColor.gray)

/* [opaquecolor. gray(intensity: 0.0), opaquecolor. gray(intensity: 0.25), opaquecolor. gray(intensity: 0.25), OpaqueColor. Gray (intensity: 0.75), Opaquecolor. gray(intensity: 1.0)] */
Copy the code
  • Do not use associated values to simulate storage properties. Please use structure instead. Enumerations cannot have storage properties. That sounds like a major limitation, but it’s not. If you think about it, adding a storage attribute of type T is really no different than adding an associated value of the same type for each member. For example, let’s add a transparent channel for the OpaqueColor type above, adding a corresponding associated value to each member:
enum AlphaColor {
    case rgba(red: Float, green: Float, blue: Float, alpha: Float) 
    case gray(intensity: Float, alpha: Float)}Copy the code

This works, but it’s not very convenient to extract the Alpha component from an AlphaColor instance – even though you know that every AlphaColor instance has an alpha component, you still have to switch the instance and extract the value in each branch. While it is possible to encapsulate this logic into a calculated property, a better solution might be to avoid the problem in the first place – encapsulate the previous OpaqueColor enumeration into a structure and make alpha a storage property of the structure:

struct Color {
    var color: OpaqueColor
    var alpha: Float
}
Copy the code

This is a general pattern: When you find that the associated values of each member of an enumeration are partially the same, consider encapsulating the enumeration into a structure and extracting the common parts. This changes the look of the result type, but not its basic nature. This is the same as extracting the common factors in a mathematical equation: A × b + a × c = a × (b + c).

  • Do not overuse associated value components. In this chapter we make extensive use of multiple tuples to represent associated values, such asOpaqueColor.rgb(red:green:blue:) This kind of. This is handy for short examples, but code in a production environment

In general, it is better to implement a custom structure for each member.

  • Use empty enumerations as namespaces.

Use empty enumerations as namespaces. Swift has no built-in namespace other than the implicit namespace formed by modules. But you can use enumerations to “emulate” namespaces. Because type definitions can be nested, an external type can act as a namespace for all declarations it contains. Empty enumerations like Never cannot be instantiated, which makes empty enumerations the best choice for defining custom namespaces. Standard libraries do the same, such as Unicode namespaces:

/// a namespace containing Unicode utility methods.
public enum Unicode {
    public struct Scalar { 
        internal var _value: UInt32 // ...
    }
// ...
}
Copy the code

Unfortunately, empty enumerations are not a perfect solution to the lack of proper namespaces: protocols cannot be embedded in other declarations, which is why the related standard library protocol is named UnicodeCodec rather than Unicod.codec.