Safe (bounds-checked) array lookup in Swift, through optional bindings?
If you really want this behavior, it smells like you want a Dictionary instead of an Array. Dictionaries return nil
when accessing missing keys, which makes sense because it's much harder to know if a key is present in a dictionary since those keys can be anything, where in an array the key must in a range of: 0
to count
. And it's incredibly common to iterate over this range, where you can be absolutely sure have a real value on each iteration of a loop.
I think the reason it doesn't work this way is a design choice made by the Swift developers. Take your example:
var fruits: [String] = ["Apple", "Banana", "Coconut"]
var str: String = "I ate a \( fruits[0] )"
If you already know the index exists, as you do in most cases where you use an array, this code is great. However, if accessing a subscript could possibly return nil
then you have changed the return type of Array
's subscript
method to be an optional. This changes your code to:
var fruits: [String] = ["Apple", "Banana", "Coconut"]
var str: String = "I ate a \( fruits[0]! )"
// ^ Added
Which means you would need to unwrap an optional every time you iterated through an array, or did anything else with a known index, just because rarely you might access an out of bounds index. The Swift designers opted for less unwrapping of optionals, at the expense of a runtime exception when accessing out of bounds indexes. And a crash is preferable to a logic error caused by a nil
you didn't expect in your data somewhere.
And I agree with them. So you won't be changing the default Array
implementation because you would break all the code that expects a non-optional values from arrays.
Instead, you could subclass Array
, and override subscript
to return an optional. Or, more practically, you could extend Array
with a non-subscript method that does this.
extension Array {
// Safely lookup an index that might be out of bounds,
// returning nil if it does not exist
func get(index: Int) -> T? {
if 0 <= index && index < count {
return self[index]
} else {
return nil
}
}
}
var fruits: [String] = ["Apple", "Banana", "Coconut"]
if let fruit = fruits.get(1) {
print("I ate a \( fruit )")
// I ate a Banana
}
if let fruit = fruits.get(3) {
print("I ate a \( fruit )")
// never runs, get returned nil
}
Swift 3 Update
func get(index: Int) ->
T?
needs to be replaced by func get(index: Int) ->
Element?
To build on Nikita Kukushkin's answer, sometimes you need to safely assign to array indexes as well as read from them, i.e.
myArray[safe: badIndex] = newValue
So here is an update to Nikita's answer (Swift 3.2) that also allows safely writing to mutable array indexes, by adding the safe: parameter name.
extension Collection {
/// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript(safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
extension MutableCollection {
subscript(safe index: Index) -> Element? {
get {
return indices.contains(index) ? self[index] : nil
}
set(newValue) {
if let newValue = newValue, indices.contains(index) {
self[index] = newValue
}
}
}
}
extension Array {
subscript (safe index: Index) -> Element? {
0 <= index && index < count ? self[index] : nil
}
}
- O(1) performance
- type safe
- correctly deals with Optionals for [MyType?] (returns MyType??, that can be unwrapped on both levels)
- does not lead to problems for Sets
- concise code
Here are some tests I ran for you:
let itms: [Int?] = [0, nil]
let a = itms[safe: 0] // 0 : Int??
a ?? 5 // 0 : Int?
let b = itms[safe: 1] // nil : Int??
b ?? 5 // nil : Int? (`b` contains a value and that value is `nil`)
let c = itms[safe: 2] // nil : Int??
c ?? 5 // 5 : Int?
Alex's answer has good advice and solution for the question, however, I've happened to stumble on a nicer way of implementing this functionality:
extension Collection {
/// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
Example
let array = [1, 2, 3]
for index in -20...20 {
if let item = array[safe: index] {
print(item)
}
}