Is there a reason why array[index] doesn't return an optional?

This was one of my first Swift radars, which was closed as "Behaves Correctly." It was also discussed in the dev forums. As noted by Dave Abrahams:

As to rationale, It’s easy and very common to have static knowledge that the index is in range.... In this case it’s much better to establish a precondition that the index is valid so that common use-cases don’t have to cope syntactically with a failure that can’t happen. Contrast this with dictionary indexing on keys, where it’s commonly not known whether the key is already in the dictionary.

As I've grown more experienced in Swift, I've come to agree. I do sometimes wish there were a "safe subscript" (like Mike Ash's) built-in, but I've come to agree that it shouldn't be the default.

Making it the default would make Arrays very difficult to work with, not just because of the required unwrap, but because the Index type would no longer be Int. It is required that subscript(Index) return Element (not Element?). This is why the Index of Dictionary isn't Key; it's DictionaryIndex<Key,Value>. Creating a special ArrayIndex would likely have a lot of annoying side-effects. (Maybe it would all work out in the end, but it's questionable whether it's worth it.)

The real lesson here is that you should avoid arbitrarily subscripting Arrays anyway. When practical, you should prefer to use it as a CollectionType. That means subscripting only with indexes that you fetched (with indexOf or indices for instance), and strongly favoring iteration (for-in, map) rather than subscripting. Use xs.first rather than xs[0]. If you treat it as a collection rather than an array, then you get the safety you're describing, while still leaving subscripts available when you need to solve special problems where you know the subscript is in range.

Here's an illustrative example. Consider this common loop, which you may think requires subscripting:

let xs = [1,2,3]

for i in 0..<xs.count {
    print("\(i): \(xs[i])")
}

We can do this a little better and not rely on our special knowledge of array indexing and make it work for all Collections:

for i in xs.indices {
    print("\(i): \(xs[i])")
}

But even that isn't necessary. We can do much better and make it work for all Sequences:

for (i, x) in xs.enumerate() {
    print("\(i): \(x)")
}

No subscript required.


Try to look at this example:

var arr: Array<Int?> = []
arr.append(1)
arr.append(nil)
arr.count == 2
let n = arr[1] // nil

I think that with this example is easy to understand the reason. Even though your index is valid, you still receive nil.

So, we know the type, but:

if let n = arr[1] {
    // how to know whether the index is out of range
    // or a valid nil value was returned?
}

Given the exhaustive discussion under user3441734:s answer (which we should probably have taken in chat...), I feel I must make clear the somewhat OK point that I think user3441734 was trying to make.

First of all, note that the question covers

  • What is the reason for Swift to let a an invalid-index situation behave just like that (throwing exception, out of bounds), instead of returning an optional?

Rob's answer is exhaustive and cover this very well, but I do think that user3441734:s answer at least deserve a status >= 0, as it does contain another reason as to why the hypothetical case "swift lets invalid-index return an optional" might not be a good idea. Note that this is by no means " ... the real reason why ... " swift don't let invalid-index return nil, but a point I think deserves to be >= 0-voted.

The question does not cover:

  • Whether it's possible to get an optional instead of an exception if the index is out of bounds.

The question author himself states the "This is easy to do with an extension method".


So, with this out of the air, lets have a look at user3441734:s answer, and try to make it a little bit more clear what he/she tried to point out to us.

We will analyse the following expression under the assumption that we are in a parallel Swift universe, where invalid-index situations (w.r.t. arrays) are treated as optionals and return nil.

// Lets assume the following is all in the scope of some function
var arr: Array<Int?> = []
arr.append(1)
arr.append(nil)
arr.append(3)
print("\(arr)") // Optional(1), nil, Optional(3)

// lets say the function randomIntegerTwoOrFour() -> Int returns, 
// randomly, either value 2 or value 4.
let ourMagicAndAtCompileTimeUnknownIndex = randomIntegerTwoOrFour()

// now, in our parallel Swift universe, say we want to
// use an if let clause on our array at our magic index
if let n = arr[ourMagicAndAtCompileTimeUnknownIndex] {
    // ...
}
else {
    // lets say we're not careful here, and think that we've
    // entered this clause because of a nil valued member of
    // our array. If we try to use our magic index for non-nil
    // assignment, how would parallel universe Swift 2 handle this?
    
    // We could of course try some downcasting to infer actual, 
    // (optional) type ourselves, but this is not very "Swifty" 
    // w.r.t. if let clauses.
}

// on the other hand, say we want to guard against invalid-index
// in our array, using a guard let clause 
guard let n = arr[ourMagicAndAtCompileTimeUnknownIndex] else {
    print("Out of bounds!") 
         // or are we ...? Careful parallel Swift universe programmer!
    
    // Naturally, we could (and should, in parallel Swift 2 universe), 
    // look closer at what type n is at this point, but this is also
    // not very "Swifty" in this context).

    return
}

To wrap it up, this is the point that I inferred from user3441734:s answer, and I think it deserve no less, and perhaps no more, than a score of 0, however, not to be down voted.

Tags:

Swift