Learning Swift - Sets

Sets are the forgotten collection type in many languages, including Swift. I think most developers use Arrays without really considering the advantages of using a Set but they have some amazingly useful features that should make them a part of any progammer’s toolkit.

If you want to follow along with a playground, you can download it here.

What is a Set?

A Set is an un-ordered collection of unique items. That’s it - nothing more than that. So it is very similar to an Array, but it is not indexed like an Array and it cannot contain more than one of each entry.

Creating a Set:

Creating a Set is as easy as creating an Array:

var myArray = ["dog", "cat", "hamster", "dog"]
var mySet: Set = ["dog", "cat", "hamster", "dog"]

If you are running these commands in a playground, notice that the differences between the 2 results:

["dog", "cat", "hamster", "dog"]	// myArray
{"hamster", "cat", "dog"}		// mySet
  1. The Array is shown wrapped in square brackets, the Set is shown wrapped in curly braces. This is just a visual clue and doesn’t really mean anything. You cannot initialize a set using curly braces.
  2. All the supplied elements of the Array are listed, but the Set has removed the duplicate “dog” element. This did not cause an error or warning, it just happened quietly.

When initializing a Set, you must add : Set to distinguish it from an array initialization. In the example above, I did not specify the data type of the elements in the Set as the Swift compiler was able to infer this from the contents. But if initializing an empty array, the data type must be specified. To check how to do this, I option-clicked on mySet to see what the Swift compiler thought it was.

So mySet is actually Set<String>. This means that to create an empty Set, you need to use something like this:

var emptySetOfStrings: Set<String> = []
var emptySetOfInts: Set<Int> = []

Adding and removing elements:

If you have been using an Array to store unique values, then you have probably written code like this:

if !myArray.contains("cat") {

With Sets, you don’t have to care. Just use insert() and let the Set work out whether to add the item or not.

// goldfish added: {"hamster", "cat", "dog", "goldfish"}
// dog already there: {"hamster", "cat", "dog", "goldfish"}

Removing elements is also easier than in Arrays. For an Array, you first have to find the index of the element and remove it by index:

// myArray.remove("hamster") // will not compile
if let index = myArray.index(of: "hamster") {
   myArray.remove(at: index)

But in a Set, you can remove any element easily and trying to remove an element that doesn’t exist will fail without an error.

mySet.remove("hamster")		// returns "hamster"
mySet.remove("canary")		// returns nil

Converting between Sets and Arrays:

Sometimes you need to be able to switch between the two. My most recent example was when I wanted to store data from a Set in a plist. Sets are not property list types but Arrays are, so I converted the Set to an Array before storing it in the plist. When reading the data in from the plist, I converted it back to a Set.

let myArrayAsSet = Set(myArray)
let mySetAsArray = Array(mySet)

One useful side-effect of these easy conversions is the ability to ‘unique’ an Array in a single line. This may be inefficient for large arrays, but works very well for small ones. Just be careful if the order of the elements is important as you cannot guarantee the order of elements in a Set.

let myArrayUniqued = Array(Set(myArray))
// ["cat", "dog"]

Iterating over elements in a Set:

As with an Array, you can use a for element in set structure, or you can use enumerated(). But you cannot subscript a Set.

for animal in mySet {

for (index, animal) in mySet.enumerated() {
   print("\(index) = \(animal)")

// will not compile
//for index in 0 ..< mySet.count {
//   print("\(index) = \(mySet[index])")

Where Sets get really interesting:

Remember in school when you learnt about Venn diagrams with pretty interlocking circles? Sets can do the same things, although you will have to do your own pretty drawings :-)

let set1: Set = ["dog", "cat", "pig"]
let set2: Set = ["cow", "horse", "pig"]

let intersect = set1.intersection(set2)
// {"pig"}

let subtract = set1.subtracting(set2)
// {"cat", "dog"}

let union = set1.union(set2)
// {"pig", "cat", "dog", "cow" "horse"}

let xor = set1.symmetricDifference(set2)
// {"cat", "dog", "cow", "horse"}

In the code example above, we have two Sets of animals, with one animal in common.

  • intersection() lists the elements in common.
  • subtracting() lists the elements in one Set after removing all elements that are in the other.
  • union() joins all the elements without duplicates.
  • symmetricDifference() lists the elements that are in one or other of the Sets but not in both. (Swift 3 renamed this function from exclusiveOr())

Here is my best attempt at a pretty drawing to show how these go together:

The next fun trick is working out sub-sets, super-sets and disjoint sets.

let set1: Set = ["dog", "cat", "pig"]
let set2: Set = ["cow", "horse", "pig"]

let smallSet: Set = ["pig", "cow"]

smallSet.isSubset(of: set1)	// false
smallSet.isSubset(of: set2)	// true

smallSet is not a subset of set1 because it contains an element that is not in set1. smallSet is a subset of set2 because every element in smallSet is also in set2.

If you want to get technical, a Set should not be considered a subset of an identical Set. The default isSubset(of:) allows this, but you can use isStrictSubset(of:) if you prefer.

set1.isSubset(of: set1)		// true
set1.isStrictSubset(of: set1)	// false

Superset works just the same but in reverse so the diagram above explains it too:

let set1: Set = ["dog", "cat", "pig"]
let set2: Set = ["cow", "horse", "pig"]

let smallSet: Set = ["pig", "cow"]

set1.isSuperset(of: smallSet)	// false
set2.isSuperset(of: smallSet)	// true

set1.isSuperset(of: set1)		// true
set1.isStrictSuperset(of: set1)		// false

set1 is not a superset of smallSet because it does not contain every element in smallSet. set2 is a superset of smallSet because every element in smallSet is also in set2.

The isSuperset(of:) and isStrictSuperset(of:) functions allow or reject identical sets.

The final comparison tool that might be useful is isDisjoint(with:) which returns true only if the two Sets have no elements in common i.e. if there is no overlap in the circles.

let set1: Set = ["dog", "cat", "pig"]
let set2: Set = ["cow", "horse", "pig"]
let otherSet: Set = ["duck", "chicken"]

set1.isDisjoint(with: set2)		// false
set1.isDisjoint(with: otherSet)		// true

“pig” occurs in both set1 and set2 so they are not disjoint. otherSet and set1 have no matching entries so they are disjoint.

When should you use a Set?

  1. If you want the elements to be unique.
  2. If you want easy methods of comparing the contents of different collections.
  3. If you want to be able to remove elements easily.

When should you not use a Set?

  1. If you need the collection to be able to hold multiples of an element.
  2. If the order of the collection is important.

For more details on Sets, check out SwiftDoc.org.

Tags: swift   learning swift