...and why you can't read letters from a string using integers.
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.
SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
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.
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.
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:
isEmpty
is still clearer than checking for a specific value, and often conveying your intent with code is half the battle.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.
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 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Link copied to your pasteboard.