Learning Swift
上QQ阅读APP看书,第一时间看更新

Enumerations

So far, we covered two of the three type classifications in Swift: structure and class. The third classification is called enumeration. Enumerations are used to define a group of related possible values that an instance can be. For example, if we want to represent the values of one of the three primary colors, an enumeration would be a great tool to do so.

Basic declaration

An enumeration is made up of cases much like a switch case and uses the keyword enum instead of struct or class. An enumeration for primary colors would look like this:

enum PrimaryColor {
    case Red
    case Green
    case Blue
}

You can then define a variable with this type and assign it one of the cases:

var color = PrimaryColor.Green

Note that to use one of the values, we use the name of the type followed by a dot (.) and then the specific case. If the type of the variable can be inferred, you can even leave out the enumeration name and just start with a dot:

color = .Red

During the assignment to .Red, the compiler already knows that the color variable is of the type PrimaryColor, so it doesn't need us to specify that again. This can be a great tool to make your code more concise, but make sure that you don't sacrifice understandability. If you leave out the type name, it should still be obvious what type it is.

Testing enumeration values

Enumeration instances can be tested for a specific value, similar to any other type, using the equality operator (==):

if color == PrimaryColor.Red {
}
else if color == .Blue {
}

Note that in the second if statement, where the color is checked for blue, the preceding code takes advantage of type inference and doesn't bother specifying PrimaryColor.

This method of comparison is familiar and good for one or two possible values. However, there is a better way to test an enumeration for different values. Instead of using an if statement, you can use switch. This is a logical solution considering that enumerations are made up of cases and switch test for cases:

switch color {
    case .Red:
        println("Color is red")
    case .Green:
        println("color is green")
    case .Blue:
        println("color is blue")
}

This is great for all the same reasons that switch is great for. In fact, switches works even better with enumerations because the possible values for an enumeration are always finite unlike other basic types. You may remember that switches require you to have a case for every possible value. This means that if you don't have a test case for every case of the enumeration, the compiler will produce an error. This is usually a great protection and that is why I would recommend that you use switch over simple if statements in most circumstances. If you ever add additional cases to an enumeration, it is great to get an error everywhere in your code that doesn't consider the new case, to make sure that you address it.

Raw values

Enumerations are great because they provide you with the ability to store information that is not based on the basic types provided by Swift, such as String, Int, and Double. There are many abstract concepts, like our color example, that are not at all related to a basic type. However, sometimes you want each enumeration case to have a raw value that is of another type. For example, if we want to represent all the coins in the U.S. currency along with their monetary value, we could make our enumeration have an integer raw value type:

enum USCoins: Int {
    case Quarter = 25
    case Dime = 10
    case Nickel = 5
    case Penny = 1
}

The raw value type is specified just like an inheritance is specified with classes and then, each case is inpidually assigned a specific value of that type.

At any time, you can access the raw value of a case using the rawValue property:

println("A Quarter is worth \(USCoins.Quarter.rawValue) cents.")

Keep in mind that an enumeration can only have raw value types that can be defined with literals such as 10 or "String". You cannot define an enumeration with your own custom type as its raw value.

Associated values

Raw values are great when every case in your enumeration has the same type of value associated with them and their value never changes. However, there are also scenarios where each case will have different values associated with it and those values will be different for each instance of the enumeration. You may even want a case that has multiple values associated with it. To do this, we can use a feature of enumerations called associated values.

With associated values, you can specify zero or more types to be associated separately with each case. Then, while creating an instance of the enumeration, you can give it any value you want:

enum Height {
    case Imperial(feet: Int, inches: Double)
    case Metric(meters: Double)
    case Other(String)
}
var height1 = Height.Imperial(feet: 6, inches: 2)
var height2 = Height.Metric(meters: 1.72)
var height3 = Height.Other("1.9 × 10-16 light years")

Here, we defined an enumeration to store a height measurement in various measurement systems. There is a case for the imperial system that uses feet and inches and a case for the metric system that is in just meters. Both of these cases have labels for their associated values similar to a tuple. The last case is there to illustrate that you don't have to provide a label if you don't want to. It simply takes a string.

Comparing and accessing values of enumerations with associated values is a little bit more complex than regular enumerations. We can no longer use the equality operator (==). Instead, we must always use a switch. Within the case of a switch, there are multiple ways in which you can handle the associated values. The most common thing you will want to do is access the specific associated value. To do that, you can assign it to a temporary variable:

switch height1 {
    case .Imperial(let feet, var inches):
        println("\(feet)ft \(inches)in")
    case let .Metric(meters):
        println("\(meters) meters")
    case var .Other(text):
        println(text)
}

In the Imperial case, the preceding code has assigned feet to a temporary constant and inches to a temporary variable. The names happen to match the labels used for the associated values, but that is not necessary. The Metric case shows that if you want all the temporary values to be constant, you can declare the let before the enumeration case. No matter how many associated values there are, the let would only have to be written once instead of once for every value. The Other case is the same as the Metric case, except that it creates a temporary variable instead of a constant.

If you want to create separate cases for conditions on the associated values, you can use the where syntax, which we saw in the previous chapter:

switch height1 {
    case .Imperial(let feet, var inches) where feet > 1:
        println("\(feet)ft \(inches)in")
    case let .Metric(meters) where meters > 0.3:
        println("\(meters) meters")
    case var .Other(text):
        println(text)
    default:
        "Too Small"
}

Note that we had to add a default case because our restrictions on the other cases did not end up exhaustive anymore.

Lastly, if you don't actually care about the associated value, you can use an underscore (_) to ignore it:

switch height1 {
    case .Imperial(_, _):
        println("Imperial")
    case .Metric(_):
        println("Metric")
    case var .Other(_):
        println("Other")
}

This shows you that with enumerations, switches have even more power than what we saw previously.

Now that you understand how to use associated values, you might have noted that they can change the conceptual nature of enumerations. Without associated values, an enumeration represents a list of abstract and constant possible values. An enumeration with associated values is different because two instances with the same case are not necessarily equal; each case could have different associated values. This makes the conceptual nature of enumerations become a list of ways to look at a certain type of information. This is not a concrete rule, but it is common and it gives you a better idea of the different types of information that can be represented best by enumerations. It will also help you make your own enumerations more understandable. Each case could theoretically represent a completely unrelated concept from the rest of the cases using associated values, but this should be a sign that an enumeration may not be the best tool for that particular job.

Methods and properties

Enumerations are actually very similar to structures. Just like structures, enumerations can have methods and properties. To improve the Height enumeration, we could add some methods to access the height in any measurement system we want. As an example, let's implement a meters method:

enum Height2 {
    case Imperial(feet: Int, inches: Double)
    case Metric(meters: Double)

    func meters() -> Double {
        switch self {
            case let .Imperial(feet, inches):
                return Double(feet)*0.3048+inches*0.3048/12
            case let .Metric(meters):
                return meters
        }
    }
}
var height4 = Height2.Imperial(feet: 6, inches: 2)
height4.meters() // 1.8796

In this method, we switch on self, which tells us which unit of measurement this instance was created with. If it is already in meters, we can just return the method, but if it is in feet and inches, we must do the conversion. As an exercise, I recommend you to try and implement a feetAndInches method that returns a tuple with two values. The biggest challenge will be to handle the mathematical operations using the correct types. You cannot perform mathematical operations with mismatching types. If you need to convert one number type to another, you can do so by initializing a copy like in the preceding code: Double(feet). Unlike casting, which we discussed earlier, this process simply creates a new copy of the feet variable that is now of the Double type instead of an Int type. This is only possible because the Double type happens to define an initializer that takes an Int type. Most different number types can be initialized with any of the others.

You now have a great overview of all the different ways in which we can organize Swift code within a single file to make the code more understandable and maintainable. It is now time to discuss how we can separate our code in multiple files to improve it even more.