Structs
Objects are great for encapsulating data and functionality behind a unifying and referenceable concept, such as a person. However, not everything is an object; we may have a set of data that is logically grouped together, but that isn't much more than that. It's not more than the sum of its parts--it is the sum of its parts.
For this, there are structs. Short for structure, structs can be found in the C programming language and were, therefore, available in Objective-C, which was built on top of C. If you are familiar with iOS/macOS development, CGRect is an example of a C struct.
Structs are value types, as opposed to classes, which are reference types, and as such behave differently when passed around. In this recipe, we will examine how structs work in Swift, and learn when and how to use them.
Getting ready
This recipe will build on top of the previous recipes, so open the playground you have used for the previous recipes. Don't worry if you haven't tried out the previous recipes; this one will contain all the code you need.
How to do it...
We have already defined a Person object as having three separate string properties relating to the person's name; however, these three separate strings don't exist in isolation from each other--they together define a person's name. Currently, to get a person's name, you have to access three separate properties and combine them. Let's tidy this up by defining a person's name as its own struct. Enter the following code into the playground and run the playground:
struct PersonName {
let givenName: String
let middleName: String
var familyName: String
func fullName() -> String {
return "\(givenName) \(middleName) \(familyName)"
}
mutating func change(familyName: String) {
self.familyName = familyName
}
}
var alissasName = PersonName(givenName: "Alissa", middleName: "May", familyName: "Jones") How it works...
Defining a struct is very similar to defining an object class, and that is intentional. Much of the functionality available to a class is also available to a struct.
Within the PersonName struct, we have properties for the three components of the name and the fullName method we saw earlier to combine the three name components into a full name string.
Next, we have a method to change the family name property, which is why we defined the familyName property as a var variable instead of a let constant. This method assigns a new value to a property of the struct; it is mutating, or changing, the struct, and therefore needs to be marked with the mutating keyword. This keyword is enforced by the compiler to remind us that when we mutate a struct, a new copy of the original struct is created with the new value. This is known as value-type semantics.
To see this in action, consider the following code:
let alissasBirthName = PersonName(givenName: "Alissa", middleName: "May", familyName: "Jones") print(alissasName.fullName()) // Alissa May Jones var alissasCurrentName = alissasBirthName print(alissasName.fullName()) // Alissa May Jones
So far, so good. We have created a PersonName struct and assigned it to a constant called alissasBirthName and a variable called alissasCurrentName.
When we change or "mutate" the alissasCurrentName variable, only this variable is changed; alissasBirthName is a copy, and so it doesn't have the amended family name, even though they were assigned from the same source:
alissasCurrentName.change(familyName: "Moon") print(alissasBirthName.fullName()) // Alissa May Jones print(alissasCurrentName.fullName()) // Alissa May Moon
There's more...
Now that we have a PersonName struct, let's amend our Person class so that it can use it:
class Person {
let birthName: PersonName
var currentName: PersonName
var countryOfResidence: String
init(name: PersonName, countryOfResidence: String = "UK") {
birthName = name
currentName = name
self.countryOfResidence = countryOfResidence
}
var displayString: String {
return "\(currentName.fullName()) - Location: \(countryOfResidence)"
}
} We've added the birthName and currentName properties of our new PersonName struct type, and we initiate them with the same value when the Person object is initiated. Since a person's birth name won't change, we define it as a constant, but their current name can change, so it's defined as a variable.
Now, let's create a new Person object:
var name = PersonName(givenName: "Alissa", middleName: "May", familyName: "Jones") let alissa = Person(name: name) print(alissa.currentName.fullName()) // Alissa May Jones
Since our PersonName struct has value semantics, we can use this to enforce the behavior that we expect our model to have. We would expect to not be able to change a person's birth name, and if you try, you will find that the compiler won't let you.
As we discussed earlier, changing the family name mutates the struct, and so a new copy is made. However, we defined birthName as a constant, which can't be changed, so the only way we would be able to change the family name would be to change our definition of birthName from let to var:
alissa.birthName.change(familyName: "Moon") // Does not compile. Compiler tells you to change let to var
When we change the currentName to have a new family name, which we can do, since we defined it as a var, it changes the currentName property, but not the birthName property, even though these were assigned from the same source:
print(alissa.birthName.fullName()) // Alissa May Jones print(alissa.currentName.fullName()) // Alissa May Jones alissa.currentName.change(familyName: "Moon") print(alissa.birthName.fullName()) // Alissa May Jones print(alissa.currentName.fullName()) // Alissa May Moon
We have used a combination of objects and structs to create a model that enforces our expected behavior.
See also
- Further information about structs can be found in Apple's documentation of the Swift language at http://swiftbook.link/docs/classes-and-structures.
- In Chapter 8, Performance and Responsiveness in Swift, we will examine value semantics in more detail, and see how it affects performance.