What goes wrong
A string with count characters has valid positions numbered 0 to count - 1. Anything at or beyond count is out of bounds. In many languages that gives an exception you can catch. In Swift, reading a position that doesn't exist stops the program at once. There is no error to handle, so the only protection is to compute the position correctly.
With a string, the position is a String.Index that you build from startIndex and an offset. The index endIndex is special: it marks the spot just after the last character. It is a valid index to compute, but there is no character there to read.
let car = "ford"
print(car[car.index(car.startIndex, offsetBy: 3)])
print(car[car.index(car.startIndex, offsetBy: 4)])
The first print shows d, the character at offset 3. The second uses offset 4, which is car.endIndex, so the program stops with a fatal error:
Fatal error: String index is out of bounds
An offset beyond the end, such as 9, fails too, already when index(_:offsetBy:) computes it. A negative offset from startIndex is also invalid. Converting the string to an array, as in Array(car)[4], doesn't help: reading a position past the end of an array also stops the program.
The exercise
The code should print the last character of message, which is s:
let message = "I love pancakes"
print(message[message.index(message.startIndex, offsetBy: message.count)])
It offsets startIndex by message.count, which is 15. The valid offsets for this string are 0 to 14, so 15 is exactly endIndex, one past the last character. The program stops instead of printing.
The fix is a single number. The last character is at offset count - 1:
let message = "I love pancakes"
print(message[message.index(message.startIndex, offsetBy: message.count - 1)])
s
Safer ways to get there
You can often avoid computing the offset yourself:
message.lastreturns an optionalCharacter?, which isnilfor an empty string instead of crashing.message.index(before: message.endIndex)steps back one character from the end. It is only valid when the string isn't empty.index(_:offsetBy:limitedBy:)returns an optional index,nilwhen the offset would pass the limit:
let message = "I love pancakes"
let limit = message.endIndex
if let idx = message.index(message.startIndex, offsetBy: 50, limitedBy: limit) {
print(message[idx])
} else {
print("offset is past the end")
}
offset is past the end
Be careful with that last form: an offset equal to count does produce an index, namely endIndex, so it is not nil, and reading a character through it still crashes. The limit only protects the index computation, not the read.
When you know the string is non-empty, last! and the offset count - 1 give the same character.
Common mistakes
- Using
countas the last offset. The last character is atcount - 1. - Taking the last character of an empty string. With
countequal to 0,count - 1is -1, which is invalid. CheckisEmptyfirst, or uselast. - Reading at
endIndex. It is a boundary, not a character. Use it as the end of a range such asstart..<endIndex, but never subscript with it. - Counting from 1. The 5th character is at offset 4.
Key takeaways
- Valid offsets run from 0 to
count - 1. - Reading an out-of-bounds position stops the program with a fatal error. It can't be caught.
- Use
last,firstandprefixorsuffixwhen the string might be short.
