Collections play a vital role in programming in any programming language. In Swift particularly, collections, or more precisely, sequences of elements that can be traversed many times according to the documentation, consist of three types; arrays, sets and dictionaries. Among all, the Array is the most often used collection type, with the other two having lower usage frequency.
In this post dictionaries will be left aside, as the focus is entirely on arrays and sets. Both of these types present several similarities, but also fundamental and radical differences. As a result, and based on the attributes and capabilities they offer, one might be more suitable to utilize over the other, but always depending on the use case. There is no better or worse collection type to use just out of the box. It always depends on the needs the collection must fulfill.
But in order to decide which one to use, it’s required to be aware of what’s different between the two. So, before we get into the details of how we handle them in common operations, let’s get to know what actually changes between arrays and sets.
Main differences between Array and Set
From a practical point of view, there are two key differences that actually dictate whether we need to use the one or the other:
- Sets do not accept duplicate values, while arrays do. If maintaining unique values is a must in your task, then Set is probably the best candidate collection to use, unless there are other reasons mandating to use an array.
- Elements in a Set do not have a specific order. If you initialize and print the elements of a set multiple times, each time they will appear in a different order. Since order is non-guaranteed, index-based reference of elements is actually meaningless; using an index to refer to a specific item into a Set is risky, as the order of elements can change after an addition or removal. If order of elements is important, then array is the way to go. The same if you plan to perform index-based manipulation of elements. Otherwise, using a Set is likely a better choice due to an efficiency-related reason, which is mentioned right next.
Performance-wise, Set is always a better choice because of its implementation details and the way it stores its elements in memory, which are required to conform to Hashable
protocol. Since it has to do with hash values internally, it provides a constant time for operations on elements (O(1)). That comes in contrast to arrays that store and access elements sequentially and have an average time of O(n).
Having said all that, let’s get to know how to handle the most common operations on both arrays and sets.
Initializing arrays and sets
Starting with the basics, both arrays and sets can be initialized in a quite similar fashion, but not exactly the same. We can either be specific and explicit, or let the compiler implicitly conclude the type of the contained elements. For instance:
1 2 3 4 |
var array: [Int] = [] var set: Set<Int> = [] |
See that the syntax is a bit different between the array and the set. In the former, we define the data type between brackets. In the latter, the data type is specified between angle brackets (the lower than and greater than symbols).
Arrays and sets can have initial values upon creation:
1 2 3 4 |
var array: [Int] = [0, 1, 2, 3, 4, 5] var set: Set<Int> = [0, 1, 2, 3, 4, 5] |
In this case, it’s not really necessary to include the Int
data type between the angle brackets, and simply keep the Set
keyword:
1 2 3 |
var set: Set = [0, 1, 2, 3, 4, 5] |
Most likely you know that we can omit to explicitly declare the elements’ data type and let the compiler implicitly determine it when there are initial values. To be specific, we can write this:
1 2 3 |
var array = [0, 1, 2, 3, 4, 5] |
That’s the short form of initializing an array. For sets it’s not the same; we still need to be clear when initializing a Set
. Yet, there is a short form here too. Instead of declaring the data type, we initialize a Set
passing as argument the collection of values:
1 2 3 |
var set = Set([0, 1, 2, 3, 4, 5]) |
Inserting elements to arrays and sets
Arrays and sets use different methods for adding new elements in the collection. In arrays there is the append(_:)
method:
1 2 3 4 |
array.append(10) // array: [0, 1, 2, 3, 4, 5, 10] |
The above adds the new value as the last element to the array. We can also insert a value to a specific index using the insert(_:at:)
method:
1 2 3 4 |
array.insert(10, at: 1) // array: [0, 10, 1, 2, 3, 4, 5] |
The new value is added at index 1, meaning the second position in the array.
Let’s go to Set
now, where there is the insert(_:)
instance method for adding new elements like so:
1 2 3 |
set.insert(10) |
But, unlike the append(_:)
or the insert(_:at:)
methods of Array
that do not return a value (they are void
methods), insert(_:)
returns a tuple with a boolean and the element we are trying to insert. More precisely:
- If the element does not exist in the set, then the first value in the tuple is
true
and the second is the newly inserted element. - If the element already exists in the set, then the first value is
false
and the second is the existing element.
Note: Remember that sets do not accept duplicate values (more on that in a while)!
For example:
1 2 3 4 5 6 7 8 9 10 |
let result = set.insert(10) // result: (inserted: true, memberAfterInsert: 10) print(result.inserted) // Output: true print(result.memberAfterInsert) // Output: 10 |
Note that it’s not mandatory to always handle the return value from the insert(_:)
method. It’s marked with the @discardableResult
attribute (see more here) so the compiler does not throw an error when left unhandled.
In order to be able to add new elements, arrays and sets must be initialized with the var
keyword as variables and not with the let
as constants. Both of them are value types and cannot be mutated (modified) if they are constants.
The following would make Xcode show an error:
1 2 3 4 5 6 7 8 9 |
let array: [Int] = [] array.append(10) // Error: Cannot use mutating member on immutable value: ‘array’ is a ‘let’ constant let set: Set<Int> = [] set.insert(10) // Error: Cannot use mutating member on immutable value: ‘set’ is a ‘let’ constant |
Iterating over elements
Going through the elements of collections is a pretty common task in every programming language. The most popular type of loop, for-in
, can be used in both arrays and sets, as demonstrated in the following examples:
1 2 3 4 5 6 7 8 9 10 11 |
for element in array { print(element) } // Output: 0, 1, 2, 3, 4, 5 for element in set { print(element) } // Output: 5, 0, 3, 1, 2, 4 |
Notice that the elements of the array are printed in order, but there’s no specific order in the set. That’s one of the fundamental differences as said in the beginning.
The forEach(_:)
higher order function can also be used for iteration:
1 2 3 4 5 6 7 8 9 10 11 |
array.forEach { print($0) } // Output: 0, 1, 2, 3, 4, 5 set.forEach { print($0) } // Output: 2, 1, 3, 4, 5, 0 |
However, the following will make the compiler show an error:
1 2 3 4 5 |
for i in 0..<set.count { print(set[i]) } |
The index-based iteration works perfectly in arrays, but not in sets, and the nature of the latter is the reason for that. Set
is an unordered collection type that stores unique values, with the order of elements not being guaranteed after they’ve been inserted to the set. So, it’s not possible to access a single element using a subscript just like we do in arrays.
Inserting duplicate values
Now that we’ve talked about how to append new elements to both types of collections and how to iterate over their elements, it’s worth focusing a bit on what happens when we attempt to add duplicate values. Let’s start with the array, since it’s the most common use case:
1 2 3 4 5 6 7 8 |
array.append(3) for element in array { print(element) } // Output: 0, 1, 2, 3, 4, 5, 3 |
See that the number 3 exists twice in the array, and in the expected indices. Now let’s try to do the same in a set:
1 2 3 4 5 6 7 8 9 10 |
let result = set.insert(3) print(result) // Output: (inserted: false, memberAfterInsert: 3) for element in set { print(element) } // Output: 1, 2, 0, 4, 3, 5 |
Notice here that the number 3 is not inserted to the set, because it already exists in it. The inserted
value of the result
is false
, indicating that insertion did not happen, while the memberAfterInsert
has the value 3; the existing value in the set. An additional proof of that is the loop, where numbers are printed in random order once again, however number 3 exists just once.
See that the compiler does not raise any error when we try to add a duplicate value to the set. It simply ignores the new insertion and returns a result tuple similar to the one shown above.
Note that even if we initialize a Set
with duplicate values, those will be actually inserted just once. For example:
1 2 3 4 5 6 7 8 |
var set: Set<Int> = [1, 2, 3, 3, 2, 1] for element in set { print(element) } // Output: 2, 1, 3 |
Getting the index of elements
In arrays there are various methods that return the index of elements. Let’s go through some of them:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// Get the first index of the element with value “2”. // The same value might exist in other positions of // the array, but this method returns the index of the // first occurrence found. let index array.firstIndex(of: 2) // Get the last index of element with value “4”. let index = array.lastIndex(of: 4) // Get the first index of element based on a condition. let index = array.firstIndex(where: { $0 >= 2 }) // Get the last index of element based on a condition. let index = array.lastIndex(where: { $0 >= 2 }) // Get the index of the element right after the given one. let nextIndex = array.index(after: 3) // Get the index of the element right before the given one. let previousIndex = array.index(before: 3) // Get the index in the specified distance from the given index as first argument. let index = array.index(array.startIndex, offsetBy: 3) |
In all the above examples, the index
returned value is an Array<Int>.Index
value and not an Int
.
On the other hand, one would expect that similar methods would (or should) not exist in Set
, considering that index is meaningless there. The order of the elements is not constant as they are not stored sequentially and it changes when modifications happen to the set. But, it provides some methods that return the index of an element which you should use with caution, if not at all. Always remember that an index returned at any given moment is valid for that moment only; there is no guarantee that the next time you’ll seek the index of the same element will have the same value as before.
So, the provided instance methods to get an index in a Set
are:
firstIndex(of:)
firstIndex(where:)
index(_:offsetBy:)
index(after:)
All of them return a Set<Int>.Index
value in the example demonstrated here. The value in the angle brackets matches the data type of the elements in the Set
every time. As said, be careful as using them in two different time instances might not return the same results.
Deleting elements
When it comes to delete items from arrays and sets, arrays provide a wider range of methods to manage that in comparison to sets. That’s because there are methods working with the index, something that’s not actually supported in sets.
Let’s see the most important (almost all) delete methods in arrays first:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Remove element at a a specific index. array.remove(at: 4) // Remove the first element from the array. array.removeFirst() // Remove the first two elements from the array. array.removeFirst(2) // Remove the last element from the array. array.removeLast() // Remove all elements in the array. array.removeAll() // Remove elements in a range. array.removeSubrange(1…4) // Remove elements based on a condition. array.removeAll(where: { $0 > 2 }) |
Note that all methods except for the last two return the removed item. It’s not mandatory to handle it, unless it’s necessary.
As far as it regards sets, see next that there are much less methods to remove elements:
1 2 3 4 5 6 7 8 9 10 11 |
// Remove a specific element from the set. set.remove(4) // Remove the first element in the set, regardless of // which is, given that order is not guaranteed in Set. set.removeFirst() // Remove all elements. set.removeAll() |
There is also a remove(at:)
method which is similar to the array’s remove(at:)
. It accepts the index of the element to delete as argument. However, I would suggest against using it, because the index of an element is not certain in Set, and the order changes with every new insertion or deletion. Be cautious if you’re planning on using it.
Conclusion
Getting to the end of this tutorial, the above is pretty much all you need to know in order to wisely choose between Array and Set, as well as the basic and most common operations you can perform in them. By having all that in mind it’s easy to discover other less used features or APIs not presented here. But all the essential information is now considered known, so go ahead and use collections thoughtfully from now on.
Thank you for reading!