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.