A Dictionary
is an unordered collection of mappings/associations from keys to values. It is very similar to a Set and has the same performance, but stores a key/value pair, and only the key has to be Hashable
. It can be used for storing preferences, or when you have a group of named values where there are either too many or change too often to be hardcoded. Then, you can use the names as keys:
The full name is Dictionary<Key, Value>
, but it is more commonly written as [Key: Value]
.
Dictionary ignores the order in which values are added or removed, and may change them arbitrarily, just like Set.
Now it's time to lay our hands on dictionaries. Follow these steps to get started:
var numbers = [0: "zero", 1: "one", 10: "ten", 100: "one hundred"]
print(numbers) // [100: "one hundred", 10: "ten", 0: "zero", 1: "one"]
// Add or change value numbers[20] = "twenty"
// Lookup returns an optional if let one = numbers[1] { // ... }
Or, you can use a default value if the key is not found:
// Or you can use a default value if the key is not found let two = numbers[2, default: "no sensible default"]
nil
:// Remove a value by setting it to nil numbers[2] = nil
// You can iterate over the contents (again: the order is not defined) for (key, value) in numbers { // ... }
// A collection of all keys numbers.keys
// A collection of all values numbers.values
This is the end of this section. Here, we have looked at dictionaries extensively and evaluated the differences between arrays, sets, and dictionaries.
A CountedSet
allows you to add equal elements more than once, and keeps count of how many of each element it contains. Naturally, it is very useful for counting things, such as how many times a word appears in a text, without having to store each word more than once.
To use an Xcode playground to develop a new CountedSet
type using a dictionary internally.
CollectionsExtra
Xcode project we used earlier, and go to CountedSet.swift
.public struct CountedSet<Element: Hashable> { typealias ElementsDictionary = [Element: Int] private var elements: ElementsDictionary public init() { elements = ElementsDictionary() } }
We use a type alias here because ElementsDictionary
will be referred to several times in the code.
public mutating func insert(_ newelement: Element, count: Int = 1) { elements[newelement, default: 0] += count }
When inserting, we first get the current count of the element (or 0 if the element is not in the dictionary), then we add how many times the element should be inserted (1 by default) to this and insert the new value into the dictionary. +=
here means this:
elements[newelement] = elements[newelement, default: 0] + count
Sequence
of elements to the set:public mutating func insert<S>(contentsOf other: S) where S:Sequence, S.Element == Element { for newelement in other { insert(newelement) } }
The generic <S>
combined with the where
clause allows us to use any sequence here, as long as its elements are the same type as the elements of this set.
public func count(for element: Element) -> Int { return elements[element, default: 0] }
If the elements
dictionary does not contain the element, we return 0
instead.
public var count: Int { var result = 0 for count in elements.values { result += count } return result }
CountedSetTests.swift
, uncomment the testInsert
unit test, and run it.CountedSet.swift
.public init<S>(_ other: S) where S:Sequence, S.Element == Element { self.init() insert(contentsOf: other) }
This allows us to initialise from a sequence:
CountedSet(["a","b","c","a"])
extension CountedSet: ExpressibleByArrayLiteral { public init(arrayLiteral elementarray: Element...) { self.init(elementarray) } }
Now, if a function asks for a CountedSet
, we can use an array literal directly.
cou
nt
method, insert the following code:@discardableResult public mutating func remove(_ element: Element, count countToRemove: Int = 1) -> Int { guard var count = elements[element] else { return 0 } count -= countToRemove guard count > 0 else { elements.removeValue(forKey: element) return 0 } elements[element] = count return count }
This is the most complex code we have used so far. It lowers the count of the element by the provided amount, and returns the new count. Here's the explanation:
@discardableResult
means if we do not use the return value from this method, we don't want a warning from the compiler.0
.0
, we remove the element from the dictionary and return 0
.Collection
protocol. Uncomment it. It is too long to go through in detail here, but feel free to look through it.CountedSetTests.swift
, and verify whether they all pass or not. Notice how the unit tests use methods such as contains
and isEmpty
that we did not implement, but got for free because we adopted the Collection
protocol.contains
method from the collection protocol is quite inefficient for our type, because it goes through every single element and compares it to the element it is searching for. We can do better. Add the following code below the remove
method in the struct declaration:public func contains(_ element: Element) -> Bool { return elements[element] != nil }
This checks the dictionary directly, which as we mentioned earlier is much faster.