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.