Storing key-value pairs with dictionaries
The last collection type we will look at is the dictionary. This is a familiar construct in programming languages, where it is sometimes referred to as a hash table. A dictionary holds a collection of pairings between a key and a value. The key can be any element that conforms to the Hashable
protocol (just like elements in a set), while the value can be any type. The contents of a dictionary are not stored in order, unlike an array; instead, key
is used both when storing a value and as a lookup when retrieving a value.
Getting ready
In this recipe, we will use a dictionary to store details of people at a place of work. We need to store and retrieve a person’s information based on their role in an organization, such as a company directory. To hold this person’s information, we will use a modified version of our Person
class from Chapter 1, Swift Fundamentals.
Enter the following code into a new playground:
struct PersonName { let givenName: String let familyName: String } enum CommunicationMethod { case phone case email case textMessage case fax case telepathy case subSpaceRelay case tachyons } class Person { let name: PersonName let preferredCommunicationMethod: CommunicationMethod convenience init(givenName: String, familyName: String, commsMethod: CommunicationMethod) { let name = PersonName(givenName: givenName, familyName: familyName) self.init(name: name, commsMethod: commsMethod) } init(name: PersonName, commsMethod: CommunicationMethod) { self.name = name preferredCommunicationMethod = commsMethod } var displayName: String { return "\(name.givenName) \(name.familyName)" } }
How to do it...
Let’s use the Person
object we defined previously to build up our workplace directory using a dictionary:
- Create a Dictionary for the employee directory:
var crew = Dictionary<String, Person>()
- Populate the dictionary with employee details:
crew["Captain"] = Person(givenName: "Jean-Luc", familyName: "Picard", commsMethod: .phone) crew["First Officer"] = Person(givenName: "William", familyName: "Riker", commsMethod: .email) crew["Chief Engineer"] = Person(givenName: "Geordi", familyName: "LaForge", commsMethod: .textMessage) crew["Second Officer"] = Person(givenName: "Data", familyName: "Soong", commsMethod: .fax) crew["Councillor"] = Person(givenName: "Deanna", familyName: "Troi", commsMethod: .telepathy) crew["Security Officer"] = Person(givenName: "Tasha", familyName: "Yar", commsMethod: .subSpaceRelay) crew["Chief Medical Officer"] = Person(givenName: "Beverly", familyName: "Crusher", commsMethod: .tachyons)
- Retrieve an array of all the keys in the dictionary. This will give us an array of all the roles in the organization:
let roles = Array(crew.keys) print(roles)
- Use a key to retrieve one of the employees, and print the result:
let firstRole = roles.first! // Chief Medical Officer let cmo = crew[firstRole]! // Person: Beverly Crusher print("\(firstRole): \(cmo.displayName)") // Chief Medical Officer: Beverly Crusher
- Replace a value in the dictionary by assigning a new value against an existing key. The previous value for the key is discarded when a new value is set:
print(crew["Security Officer"]!.name.givenName) // Tasha crew["Security Officer"] = Person(givenName: "Worf", familyName: "Son of Mogh", commsMethod: .subSpaceRelay) print(crew["Security Officer"]!.name.givenName) // Worf
With that, we have learned how to create, populate, and look up values in a dictionary.
How it works...
As with the other collection types, when we create a dictionary, we need to provide the types that the dictionary will hold. For dictionaries, there are two types that we need to define. The first is the type of the key (which must conform to Hashable
), while the second is the type of the value being stored against the key. For our dictionary, we are using String
for the key and Person
for the values being stored:
var crew = Dictionary<String, Person>()
As with an array, we can specify a dictionary
type using square brackets and create one using a dictionary literal, where :
separates the key and the value:
let intByName: [String: Int] = ["one": 1, "two": 2, "three": 3]
Therefore, we can change our dictionary definition so that it looks like this:
var crew: [String: Person] = [:]
The [:]
symbol denotes an empty dictionary as a dictionary literal.
Elements are added to a dictionary using a subscript. Unlike an array, which takes an Int
index in the subscript, a dictionary takes the key and then pairs the given value with the given key. In the following example, we assign a Person
object to the "
Captain"
key:
crew["Captain"] = Person(givenName: "Jean-Luc", familyName: "Picard", commsMethod: .phone)
If no value currently exists, the assigned value will be added. If a value already exists for the given key, the old value will be replaced with the new value, and the old value will be discarded.
There are properties in the dictionary that provide all the keys and values. These properties are of a custom collection type, which can be passed to an array initializer to create an array:
let roles = Array(crew.keys) print(roles)
To display all the dictionary’s keys, as provided by the keys
property, we can either create an array or iterate over the collection directly. We will cover iterating over a collection’s values in Chapter 3, Data Wrangling with Swift, so for now, we will create an array.
Now, we will use one of the values from an array of keys, alongside the crew, to retrieve full details about the associated Person
:
let firstRole = roles.first! // Chief Medical Officer let cmo = crew[firstRole]! // Person: Beverly Crusher print("\(firstRole): \(cmo.displayName)") // Chief Medical Officer: Beverly Crusher
We get the first element using the first
property, but since this is an optional type, we need to force-unwrap it using !
. We can pass firstRole
, which is now a non-optional String
to the dictionary subscript, to get the Person
object associated with that key. The return type to retrieve the value via subscript is also optional, so it also needs to be force-unwrapped before we print its values.
Note
Force unwrapping is usually an unsafe thing to do, as if we force unwrap a value that turns out to be nil, our code will crash. We advise you to check that a value isn’t nil before unwrapping the optional. We will cover how to do this in Chapter 3.
There’s more...
In this recipe, we used strings as the keys for our dictionary. However, we can also use a type that conforms to the Hashable
protocol.
One downside of using String
as a key for our employee directory is that it is very easy to mistype an employee’s role or look for a role that you expect to exist but doesn’t. So, we can improve our implementation by using something that conforms to Hashable
and is better suited to being used as a key in our model.
We have a finite set of employee roles in our model, and an enumeration is perfect for representing a finite number of options, so let’s define our roles as an enum:
enum Role: String { case captain = "Captain" case firstOfficer = "First Officer" case secondOfficer = "Second Officer" case chiefEngineer = "Chief Engineer" case councillor = "Councillor" case securityOfficer = "Security Officer" case chiefMedicalOfficer = "Chief Medical Officer" }
Now, let’s change our Dictionary
definition so that it uses this new enum
as a key, and then insert our employees using these enum
values:
var crew = Dictionary<Role, Person>() crew[.captain] = Person(givenName: "Jean-Luc", familyName: "Picard", commsMethod: .phone) crew[.firstOfficer] = Person(givenName: "William", familyName: "Riker", commsMethod: .email) crew[.chiefEngineer] = Person(givenName: "Geordi", familyName: "LaForge", commsMethod: .textMessage) crew[.secondOfficer] = Person(givenName: "Data", familyName: "Soong", commsMethod: .fax) crew[.councillor] = Person(givenName: "Deanna", familyName: "Troi", commsMethod: .telepathy) crew[.securityOfficer] = Person(givenName: "Tasha", familyName: "Yar", commsMethod: .subSpaceRelay) crew[.chiefMedicalOfficer] = Person(givenName: "Beverly", familyName: "Crusher", commsMethod: .tachyons)
You will also need to change all the other uses of crew
so that they use the new enum
-based key.
Let’s take a look at how and why this works. We created Role
as a String
-based enum
:
enum Role: String { //... }
Defining it in this way has two benefits:
- We intend to display these roles to the user, so we will need a string representation of the
Role enum
, regardless of how we defined it. - Enums have a little bit of protocol and generics magic in them, which means that if an enum is backed by a type that implements the
Hashable
protocol (asString
does), the enum also automatically implements theHashable
protocol. Therefore, definingRole
as beingString
-based satisfies the dictionary requirement of a key beingHashable
, without us having to do any extra work.
With our crew
dictionary now defined as having a Role
-based key, all subscript operations have to use a value in the enum
role:
crew[.captain] = Person(givenName: "Jean-Luc", familyName: "Picard", commsMethod: .phone) let cmo = crew[.chiefMedicalOfficer]
The compiler enforces this, so it’s no longer possible to use an incorrect role when interacting with our employee directory. This pattern of using Swift’s constructs and type system to enforce the correct use of code is something we should strive to do, as it can reduce bugs and prevent our code from being used in unexpected ways.
See also
Further information about dictionaries can be found in Apple’s documentation on the Swift language at http://swiftbook.link/docs/collections.