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

Structs

The most basic way in which we can group together data and functionality in a logical unit or object is by defining something called a structure. Essentially, a structure is a named collection of data and functions. Actually, we have already seen many different structures because all of the types, such as a String, array, and dictionary, which we saw previously, are structures. Now, you will learn how to create your own structure.

Types versus instances

Let's jump straight to the topic of defining our first structure so that it represents a contact:

struct Contact {
    let firstName: String = "First"
    let lastName: String = "Last"
}

Here, we created a structure using the struct keyword followed by a name and curly brackets ({}) with some code within them. Just as with a function, everything about a structure is defined within its curly brackets. However, code within a structure is not run directly, it is all part of defining what the structure is. Think of a structure as a specification for future behavior instead of code to be run.

Here, we defined two constants for the first and last name. This code does not create any actual constants nor does it remember any data. Similar to a function, this code is not truly used until another piece of code uses it. Just as with a String, we must define a new variable or constant of this type. However, in the past, we always used literals such as Sarah or 10. With our own structures, we will have to initialize our own instances.

An instance is a specific incarnation of a type. For example, when we create a String variable and assign it the value Sarah. We create an instance of a String that has the value Sarah. The String itself is not a piece of data; it simply defines the nature of instances of a String that actually contain data.

Initialize is the formal name used to create a new instance. We initialize a new contact like this:

var someone = Contact()

You may have noticed that this looks a lot like calling a function and that is because it is very similar. Every type must have at least one special function called an initializer. As the name implies, this is a function that initializes a new instance of the type. All initializers are named after their type and they may or may not have parameters just like a function. In our case, we don't provide any parameters, so the first and last names will be left at their default values that we provided in our specification: "First" and "Last".

You can see this in a playground. If your side bar is large enough, you will see {firstName "First", lastName "Last"}. You can also see more by clicking on the plus sign next to the short description. We just initialized our first custom type!

Properties

The two constants, firstName and lastName, are called member constants and if we changed them to be variables, they would be called member variables. This is because they are pieces of information associated with a specific instance of the type. You can access member constants and variables on any instance of a structure:

println("\(someone.firstName) \(someone.lastName)")

This is in contrast to a static constant. We can add a static constant to our type by adding the following line to its definition:

struct Contact {
    static let UnitedStatesPhonePrefix = "+1"
}

Note the static keyword before the constant declaration. A static constant is accessed directly from the type and is independent of any instance:

println(Contact.UnitedStatesPhonePrefix) // "+1"

Note that we will add code to the existing code every so often like this. If you are following along in a playground, you should have the static let line added in the existing Contact structure.

Both the member and static constants and variables fall under the category of properties. A property is simply a piece of information associated with an instance or a type. This helps us reinforce the idea that every type is an object. A ball, for example, is an object that has many properties, including its radius, color, and elasticity. In code, we can represent a ball in an object-oriented way by creating a ball structure that has each of those properties:

struct Ball {
    var radius: Double
    var color: String
    var elasticity: Double
}

Note that this Ball type does not define default values for its properties. If default values are not provided in the declaration, they are required when you initialize an instance of the type. This means that an empty initializer is not available for that type. If you try to use one, you will get an error:

Ball() // Missing argument for parameter 'radius' in call

Just like with normal variables and constants, all properties must have a value once they are initialized. Therefore we must write:

var ball = Ball (radius: 1, color: "Green", elasticity: 2.3)

Member and static methods

Just as you can define constants and variables within a structure, you can also define member and static functions. These functions are referred to as methods used to distinguish the members from functions that are not associated with any type. You declare member methods very similar to functions, but you do so inside the type declaration:

struct Contact {
    let firstName: String = "First"
    let lastName: String = "Last"

    func print() {
        println("\(self.firstName) \(self.lastName)")
    }
}

Member methods always act on a specific instance of the type they are defined in. To access that instance within the method, use the self keyword. The self keyword acts similar to any other variable in which you can access properties and methods on it. The preceding code prints out the firstName and lastName properties. You can call this method like we called methods on any other type:

someone.print()

Within a normal structure method, self is constant, meaning, you can't modify any properties. If you tried, you would get an error:

struct Ball {
    var radius: Double
    var color: String
    var elasticity: Double

    func growByAmount(amount: Double) {
        self.radius = self.radius + amount // Cannot assign to 'radius' in 'self'
    }
}

In order for a method to modify itself, it must be declared as a mutating method using the mutating keyword:

mutating func growByAmount(amount: Double) {
    self.radius = self.radius + amount
}

Note that we could have used the operator += instead, but that causes a much harder to understand error:

func growByAmount(amount: Double) {
    self.radius += amount
    // Binary operator '+=' cannot be applied to two
    // Double operands
}

This error is confusing because the language is still young. Over time, the Swift team will likely improve the error messages to make debugging errors less difficult. A good rule of thumb to figure out confusing errors is to try and write the same code in a different way. In this case, writing out the expanded form gives out the much more understandable error that we already saw.

Similar to how we can define static properties that apply to the type itself, we can also define static methods that operate on the type itself using the static keyword. We can add a static method to our Contact structure that prints the available phone prefixes:

struct Contact {
    static let UnitedStatesPhonePrefix = "+1"

    static func printAvailablePhonePrefixes() {
        println(self.UnitedStatesPhonePrefix)
    }
}

Contact.printAvailablePhonePrefixes() // "+1"

Within a static method, self refers to the type instead of an instance of the type. In the preceding code, we used the static property UnitedStatesPhonePrefix through self instead of writing out the type name.

In both static and instance methods, Swift allows you to access properties without using self for brevity. The self keyword is simply implied:

func print2() {
    println("\(firstName) \(lastName)")
}

static func printAvailablePhonePrefixes2() {
    println(UnitedStatesPhonePrefix)
}

However, if you create a variable within the method with the same name, you will have to use self to distinguish which one you want:

func print3() {
    var firstName = ""
    println("\(self.firstName) \(lastName)")
}

I recommend you do not use this feature of Swift. I want to make sure that you know it is possible, so that you are not confused looking at other peoples' code, but I feel that using self always greatly increases the readability of your code. The self keyword makes it instantly clear that the variable is attached to the instance instead of being only defined in the function. Also, you could create bugs if you add code that creates a variable and hides a member variable. For example, you would create a bug if you introduced the firstName variable to the print method without realizing you were using firstName to access the member variable later in the code. Instead of accessing the member variable, the later code would start to access only the local variable.

Other than having access to self within a method, the other distinction between writing functions and writing methods is that the parameter labeling defaults to a different behavior. In a function, no parameter label is required when it is called, unless both an internal and external name is provided:

func addInviteeToList
    (
    invitees: [String],
    newInvitee: String
    )
    -> [String]
{
    return invitees + [newInvitee]
}
addInviteeToList([], "Sarah")

However, with a method, the parameter label is required by default for every parameter, except for the first one:

struct Contact {
    //...

    func addToInviteeList(
        invitees: [String],
        includeLastName: Bool
        ) -> [String]
    {
        if includeLastName {
            let fullName = "\(self.firstName) \(self.lastName)"
            return invitees + [fullName]
        }
        else {
            return invitees + [self.firstName]
        }
    }
}

var someone = Contact()
someone.addToInviteeList([], includeLastName: true)

Even though the preceding code only specified a single name for includeLastName, it must still be used while calling the method. If you want to remove this requirement, you can use an underscore (_) for the external name:

func addToInviteeList2(
    invitees: [String],
    _ includeLastName: Bool
    ) -> [String]
someone.addToInviteeList2([], true)

In most circumstances, Xcode will advise you to remove or add parameter labels as necessary, but it is still useful to have a good understanding of what the default behavior is.

Computed properties

So far, it seems that properties are used to store information and methods are used to perform calculations. While this is generally true, Swift has a feature called computed properties. These are properties that are calculated every time they are accessed. To do so, you define a property and then you provide a method called getter that returns the calculated value:

struct Ball2 {
    var radius: Double
    var diameter: Double {
        get {
            return self.radius * 2
        }
    }
}

var ball2 = Ball2(radius: 2)
println(ball2.diameter) // 14.0

This is a great way to avoid storing data that could potentially conflict with other data. If, instead, diameter were just another property, it would be possible for the diameter property to not match with the radius. Every time you changed the radius you would have to remember to change the diameter as well. Using a computed property eliminates this concern.

You can even provide a second function called setter that allows you to assign a value to this property, just like normal properties:

var diameter: Double {
    get {
        return self.radius * 2
    }
    set {
        self.radius = diameter / 2
    }
}

ball2.diameter = 4
println(ball2.radius) // 2.0

If you provide a setter, you must also provide a getter. Because of this, Swift allows you to leave out the get syntax if you are not defining a setter:

var diameter2: Double {
    return self.radius * 2
}

This provides a nice and concise way to define read-only computed properties.

Reacting to property changes

It is pretty common to want to perform some action whenever a property is changed. One way to achieve this would be by defining a computed property with a setter that performs the necessary action; however, Swift provides a better way to do this. On any stored property, you can define a willSet function and/or a didSet function. The willSet function will be called just before the property is changed and it is provided with the variable newValue. The didSet function will be called just after the property is changed and it is provided with the variable oldValue:

var radius: Double {
    willSet {
        println("changing from \(self.radius) to \(newValue)")
    }
    didSet {
        println("changed from \(oldValue) to \(self.radius)")
    }
}

Be careful while using didSet and willSet with multiple properties so that you don't create an infinite loop. For example, you should be careful when you try to use this technique to keep the diameter and radius in sync instead of using a computed property:

struct Ball3 {
    var radius: Double {
        didSet {
            self.diameter = self.radius * 2
        }
    }
    var diameter: Double {
        didSet {
            self.radius = self.diameter /  2
        }
    }
}

In this scenario, if you set the radius, it triggers a change on the diameter, which triggers another change on the radius and this continues forever.

Subscripts

You may also have realized that there is another way in which we have interacted with a structure in the past. With both arrays and dictionaries, we used square brackets ([]) to access elements. These square brackets are called subscripts and we can use these on our custom types as well. The syntax for them is similar to the computed properties that we saw before, except that you define these like methods with parameters and a return type:

struct MovieAssignment {
    var movies: [String:String]

    subscript(invitee: String) -> String? {
        get {
            return self.movies[invitee]
        }

        set {
            self.movies[invitee] = newValue
        }
    }
}

You can declare the arguments you want to use within square brackets as the parameters to the subscript method. The return type for the subscript function is the type that will be returned when it is used to access a value. It is also the type for any value that you assign to the subscript function:

var assignment = MovieAssignment(movies: [:])
assignment["Sarah"] = "Modern Family"
println(assignment["Sarah"]) // "Modern Family"

You may have noticed a question mark (?) in the return type. This is called an optional and we will discuss this in depth in the next chapter. For now, just know that this is the type that is returned while accessing a dictionary by key because a value does not exist for every possible key.

Just like with computed properties, you can also define subscripts as read only without using the get syntax:

struct MovieAssignment2 {
    var movies: [String:String]

    subscript(invitee: String) -> String? {
        return self.movies[invitee]
    }
}

A subscript function can have as many arguments as you want by adding additional parameters to the subscript declaration. In that case, you would separate each parameter by a comma within the square brackets when using the subscript:

struct MovieAssignment {
//...
    subscript(param1: String, param2: Int) -> Int {
        return 0
    }
}

println(assignment["Sarah", 2])

The subscript function can be a powerful tool to shorten your code, but you should always be careful so as you don't sacrifice clarity for brevity. Writing clear code is a balance between being too wordy and not wordy enough. If your code is too short, it will be hard to understand because the meaning will become ambiguous. It is much more clear to have a method called "movieForInvitee:" than using a subscript. However, if all of your code is too long, there will be too much noise around the overall goal of the code and you will lose clarity in that way. Use subscripts sparingly and only when they would be intuitive to another programmer based on the type of structure you create.

Custom initialization

If you are not satisfied with the default initializer provided to you, you can define your own initializer. This is done using the init keyword:

struct Contact2 {

    var firstName: String
    var lastName: String

    init (contact: Contact2) {
        self.firstName = contact.firstName
        self.lastName = contact.lastName
    }
}

Just like with a method, the initializer can take any number of parameters, including none at all. However, initializers have extra restrictions. One rule is that every member variable and constant must have a value by the end of the initializer. If we do not set a value for lastName in our initializer, we would get an error:

struct Contact3 {

    var firstName: String
    var lastName: String

    init(contact: Contact3) {
        self.firstName = contact.firstName
    } // Variable 'self.lastName' used before being initialized
}

Note that this code did not provide default values for firstName and lastName. If we add them back, we no longer get an error because now, a value is provided:

struct Contact4 {

    var firstName: String
    var lastName: String = "Last"

    init(contact: Contact4) {
        self.firstName = contact.firstName
    }
}

Once you provide your own initializer, Swift no longer provides any default initializers. In our preceding example, Contact2 can no longer be initialized with the firstName and lastName parameter. If we want both possibilities, we can add our own version of that initializer:

struct Contact3 {

    var firstName: String
    var lastName: String

    init(contact: Contact3) {
        self.firstName = contact.firstName
        self.lastName = contact.lastName
    }

    init(firstName: String, lastName: String) {
        self.firstName = firstName
        self.lastName = lastName
    }
}
var sarah = Contact(firstName: "Sarah", lastName: "Smith")
var sarahCopy = Contact(contact: Sarah)
var other = Contact(firstName: "First", lastName: "Last")

Another option to set up the initial values in an initializer is to call a different initializer:

init(contact: Contact2) {
    self.init(firstName: contact.firstName, lastName: contact.lastName)
}

This is a great tool to reduce duplicate code in multiple initializers. However, while using this, there is an extra rule that you must follow. You cannot access self before calling have another initializer:

init(contact: Contact2) {
    self.print()
    // Use of 'self' in delegating initializer
    // before self.init is called
    self.init(firstName: contact.firstName, lastName: contact.lastName)
}

This is a great example of why the requirement exists. If we were to call print before calling the other initializer, firstName and lastName would not have a value yet. What would be printed in that case? Instead, you can only access self after calling the other initializer:

init(contact: Contact2) {
    self.init(firstName: contact.firstName, lastName: contact.lastName)
    self.print()
}

This guarantees that all the properties have a valid value before any method is called.

You may have noticed that initializers follow a third pattern for parameter naming. By default, initializers require a label for all parameters. This concludes the three different patterns that are in Swift. So, just to summarize, the default behavior for parameter names is as follows:

  • Functions show no parameter names
  • Methods show the name for all, but the first parameter
  • Initializers show the name for all parameters

However, remember that this is only the default behavior. You can change the behavior by either providing an internal and external name or using an underscore (_) as the external name.

Structures are an incredibly powerful tool for programming. They are an important way that we can use to abstract away more complicated concepts. As we discussed in Chapter 2, Building Blocks – Variables, Collections, and Flow Control, this is how we get better at using computers. Other people can provide these abstractions to us for concepts that we don't understand yet or in circumstances, where it isn't worth our time to start from scratch. We can also use these abstractions for ourselves, so that we can better reason about the high-level logic going on in our app. This will greatly increase the reliability of our code. Structures make our code more understandable for other people and for ourselves in future.

However, structures are limited in an important way; they don't provide a good way to express parent-child relationships between types. For example, a dog and cat are both animals and share a lot of properties and actions. It would be great if we only had to implement the common attributes once. We could then split those types into different species. For this, Swift has a different system of types called classes.