Safe (bounds-checked) array lookup in Swift, through optional bindings?

If I have an array in Swift, and try to access an index that is out of bounds, there is an unsurprising runtime error:

var str = ["Apple", "Banana", "Coconut"]

str[0] // "Apple"
str[3] // EXC_BAD_INSTRUCTION

However, I would have thought with all the optional chaining and safety that Swift brings, it would be trivial to do something like:

let theIndex = 3
if let nonexistent = str[theIndex] { // Bounds check + Lookup
    print(nonexistent)
    ...do other things with nonexistent...
}

Instead of:

let theIndex = 3
if (theIndex < str.count) {         // Bounds check
    let nonexistent = str[theIndex] // Lookup
    print(nonexistent)   
    ...do other things with nonexistent... 
}

But this is not the case - I have to use the ol' if statement to check and ensure the index is less than str.count.

I tried adding my own subscript() implementation, but I'm not sure how to pass the call to the original implementation, or to access the items (index-based) without using subscript notation:

extension Array {
    subscript(var index: Int) -> AnyObject? {
        if index >= self.count {
            NSLog("Womp!")
            return nil
        }
        return ... // What?
    }
}

Solution 1:

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:

Swift 3.2 and newer

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
    }
}

Swift 3.0 and 3.1

extension Collection where Indices.Iterator.Element == Index {

    /// Returns the element at the specified index if it is within bounds, otherwise nil.
    subscript (safe index: Index) -> Generator.Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

Credit to Hamish for coming up with the solution for Swift 3.

Swift 2

extension CollectionType {

    /// Returns the element at the specified index if it is within bounds, otherwise nil.
    subscript (safe index: Index) -> Generator.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)
    }
}

Solution 2:

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?

Solution 3:

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
            }
        }
    }
}