NEW: Nominations are now open for the 2019 Swift Community Awards! >>

Why using isEmpty is faster than checking count == 0

...and why you can't read letters from a string using integers.

Paul Hudson       @twostraws

If you want to check that an array, set, string, or other collection type is empty, you might write code like this:

let name = ""

if name.count == 0 {
    print("You're anonymous!")
}

However, that code is better expressed like this:

if name.isEmpty {
    print("You're anonymous!")
}

Using isEmpty is both clearer to read and faster to run.

How Swift strings are built

To understand why isEmpty is faster than count == 0 for strings, we need to dive into how strings work in Swift.

Swift strings are complex collections of characters, where multiple symbols might combine together to a form a single letter visible to users. For example, an emoji as simple as the British flag is actually made up of two distinct characters: “G” and “B”. Not those literal letters, of course, but Unicode symbols that, when put side by side, automatically become the British flag.

You can see this in action in the screenshot below: it stores regional indicator symbol letter G in one string, regional indicator symbol letter B in a second string, and merges them together in a third string. As you can see, individually they are shown as “G” and “B” with a dashed line around them, but together they automatically become the British flag.

Look at the count values for each of those strings – all three are 1. So, from the perspective of raw characters this thing is two characters, but from a user’s perspective it’s just one: the British flag - they don’t want to divide the flag in half by accident. Swift is designed to stop us from breaking up Unicode strings like this by accident, so it also considers emoji to be just one character.

To make things more confusing, behind the scenes each of those special letters “G” and “B” can be represented as two values in UTF-16, or as four values in UTF-8.

Indexing into strings

As a result of this complexity, it’s not possible to read individual characters in a string using an integer, which means this kind of code won’t compile:

let name = "Guybrush Threepwood"
print(name[0])

That’s not because it’s impossible to make such code work. In fact, we can add a trivial extension to strings for just that purpose:

extension String {
    subscript(i: Int) -> Character {
        return self[index(startIndex, offsetBy: i)]
    }
}

However, things aren’t quite so simple. Because each character in a Swift string might be stored as one value, two values, four values, or potentially dozens (see https://www.zalgotextgenerator.com for extreme examples), Swift can’t know ahead of time where character 5 is going to be – our extension starts at the first letter and counts through by i characters until it finds the one you asked for.

This becomes problematic because you might try writing something like this to print out every other character from a string:

for i in stride(from: 0, to: name.count, by: 2) {
    print(name[i])
}

We say that a loop like that is O(n), or order N, which means “the length of the string linearly controls the speed of the code.” So, a 1-character string might take 1 second to work with, a 2-character string might take 2 seconds, a 3-character string might take 3 seconds, and so on – it’s a linear relationship.

However, our little string subscript extension is also an O(n) operation, because it counts through the string letter by letter. As a result, we’ve now got one O(n) operation inside another, which becomes an O(n²) operation – using the example above, that would mean a 2-character string would take 4 seconds (2²), a 3-character string 9 seconds (3²), a 10-character string 100 seconds (10²), and so on.

So, even though our code looks like it’s going to be relatively fast, it is in fact likely to be extremely slow.

Back to isEmpty

Now that you understand how Swift strings work internally, let’s circle back to isEmpty vs count == 0.

As you’ve seen, Swift’s strings mask all sorts of complexity: one character visible to users might be a dozen underlying values in the string itself. So, when we read the size of a string using count, Swift needs go through all the characters it has to figure out just how long the string is: it starts at the beginning, and counts through to the end.

This means reading the count of a string is an O(n) operation: if you have an empty string it’s basically instant, but if you have the complete works of Shakespeare it will take some time to calculate.

In comparison, using isEmpty can return true or false after one simple check: does the start index of my string equal the end index of my string? If so, the string is empty and we’re done – there’s no need to count through all the characters.

Now, this particular problem doesn’t apply to arrays, sets, or dictionaries, only strings, so you could if you wanted use count == 0 elsewhere and isEmpty only for strings. However, I’d caution against that for two reasons:

  1. isEmpty is still clearer than checking for a specific value, and often conveying your intent with code is half the battle.
  2. You can more easily avoid using count == 0 with strings, because you can remove all instances from your code.

Fortunately, both SwiftLint and SwiftFormat can take care of this for you, because they have opt-in rules that can detect exactly this situation.

Where next?

There’s no doubt that Swift strings can be tricky little things, but hopefully now you have new-found respect for how much work they are doing on your behalf.

If you’d like to learn more about how to use Swift strings, there’s a whole section of my Swift knowledge base dedicated to strings – there are lots of common problems solved there, all fully updated for the latest version of Swift.

And if you have further questions about Swift strings, feel free to message me on Twitter – I’m @twostraws there.

SAVE 20% ON iOS CONF SG The largest iOS conference in Southeast Asia is back in Singapore for the 5th time in January 2020, now with two days of workshops plus two days of talks on SwiftUI, Combine, GraphQL, and more! Save a massive 20% on your tickets by clicking on this link.

MASTER SWIFT NOW
Buy Testing Swift Buy Practical iOS 12 Buy Pro Swift Buy Swift Design Patterns Buy Swift Coding Challenges Buy Server-Side Swift (Vapor Edition) Buy Server-Side Swift (Kitura Edition) Buy Hacking with macOS Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with Swift Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

About the author

Paul Hudson is the creator of Hacking with Swift, the most comprehensive series of Swift books in the world. He's also the editor of Swift Developer News, the maintainer of the Swift Knowledge Base, and a speaker at Swift events around the world. If you're curious you can learn more here.

Was this page useful? Let us know!

Average rating: 4.7/5