Performance, functional programming and collections in Swift

Functional programming is often done in Swift and so easy that it could easily hit performance. Iterating over large collections and performing actions like filter or map is common and should be used wisely.

Performance is often decreasing when these methods are combined, like a filter followed by first. A list of best practices.

Prefer contains over first(where:) != nil

Verifying if an object exists in a collection can be done in multiple ways. Make sure to use contains for best performance.

Best practice

let numbers = [0, 1, 2, 3]
numbers.contains(1)

Bad practice

let numbers = [0, 1, 2, 3]
numbers.filter { number in number == 1 }.isEmpty == false
numbers.first(where: { number in number == 1 }) != nil

Prefer checking isEmpty over comparing count to zero.

The best way to describe the reasoning for this is by quoting the isEmpty documentation.

When you need to check whether your collection is empty, use the isEmpty property instead of checking that the count property is equal to zero. For collections that don’t conform to RandomAccessCollection, accessing the count property iterates through the elements of the collection.

Best practice

let numbers = []
numbers.isEmpty

Bad practice

let numbers = []
numbers.count == 0

Checking for an empty String with isEmpty

A String in Swift can be seen as a collection of Characters. This means that the above performance practice counts for strings as well and isEmpty should be used to check for an empty string.

Best practice

myString.isEmpty

Bad practice

myString == ""
myString.count == 0

Get the first object matching a condition

Iterating over a collection to get the first object matching a given condition can be done using a filter following by first, but best practice is to use first(where:) instead.

The latter stops iterating over the collection as soon as the condition is met for the first time. The filter would continue iterating over the whole collection, even though it might already hit an object matching the condition.

Obviously, the same counts for last(where:).

Best practice

let numbers = [3, 7, 4, -2, 9, -6, 10, 1]
let firstNegative = numbers.first(where: { $0 < 0 })

Bad practice

let numbers = [3, 7, 4, -2, 9, -6, 10, 1]
let firstNegative = numbers.filter { $0 < 0 }.first

Getting the minimum or maximum element in a collection

The minimum element in a collection can be found after sorting it and selecting the first object. The same counts for the maximum element. This, however, requires to sort the whole array first. Using the min() and max() methods is best practice in this case.

Best practice

let numbers = [0, 4, 2, 8]
let minNumber = numbers.min()
let maxNumber = numbers.max()

Bad practice

let numbers = [0, 4, 2, 8]
let minNumber = numbers.sorted().first
let maxNumber = numbers.sorted().last

Making sure all objects match a condition

It’s easy to use a filter combined with isEmpty to verify that all objects in a collection match a given condition. With SE-0207 introduced in Swift 4.2 we can now use allSatisfy(_:) to verify that every element of a collection matches a given condition.

Best practice

let numbers = [0, 2, 4, 6]
let allEven = numbers.allSatisfy { $0 % 2 == 0 }

Bad practice

let numbers = [0, 2, 4, 6]
let allEven = numbers.filter { $0 % 2 != 0 }.isEmpty

Using SwiftLint to ensure best practices

Most of these examples are implemented in SwiftLint and help you to ensure that these best practices are used.


Also published on Medium.