Array.Index

Hi,

Pardon my ignorance, I haven't used Array.Index and just used it.
I am encountering Index out of range error when I didn't expect it

My Expectation

  • In the following code I thought i would become nil

Actual Result

  • In the following code i was 2

Question

  • What am I missing?
  • Is it safe to use Array.Index, I feel like the same checking needs to be done manually or am I using the API incorrectly?

Code

Given below is the code based on the documentation code (slightly modified):

Refer: index(_:offsetBy:limitedBy:) | Apple Developer Documentation

let numbers = [10, 20, 30, 40, 50]
if let i = numbers.index(numbers.startIndex,
                         offsetBy: 5,
                         limitedBy: numbers.endIndex) {
    print(numbers[i]) //Fatal error: Index out of range
}

endIndex is not exactly what you expect it to be - it's past the end (endIndex | Apple Developer Documentation). And index method you are using expects limitedBy parameter to be a valid array index so it could use it as a fallback. So you need to pass numbers.index(before: numbers.endIndex) instead.

1 Like

Thanks a lot @vns I was breaking my head over it.

I see the usefulness partly but seems very verbose are there scenarios where this proves better than manual checking?

It's more useful for generic functions that take arbitrary collections where the index type isn't necessarily an integer, unlike Array.

4 Likes

Thanks a lot @David_Smith I didn't beyond integer index.. wow!

I see your point now, yeah non-integer indexes my logic of manually checking wouldn't be possible, thanks!!

For integer indexes is it ok to check to use manual checking? (as shown below)

This uses numbers.startIndex and numbers.endIndex

let numbers = [10, 20, 30, 40, 50]
let offset = 5

if offset < numbers.endIndex {
    let index = offset + numbers.startIndex
    print(numbers[index])
}

Seems fine to me. Array also always has a startIndex of 0, so you can simplify further. ArraySlice is the one that might not start at 0.

1 Like

yeah I didn't even have to add numbers.startIndex .. haha

Yes, I almost never use these methods on Arrays, since it much simpler to express it via simple arithmetic. When it comes to String for example, or working with generic collections, it's a powerful API on collection, and you it's good you are getting more familiar with it anyway.

1 Like

Thanks a lot @vns and @David_Smith was a good learning for me!!

Better check that index first. :slight_smile:

let numbers = [10, 20, 30, 40, 50]
let offset = 5

let index = offset + numbers.startIndex

if index >= numbers.startIndex && index < numbers.endIndex {
    print(numbers[index])
}

The docs here could definitely be clearer! The examples certainly suggest that you can use endIndex as the limit and get back an index that’s safe to use for subscripting, when in fact you can’t. Looks like there’s a bug tracking this: [SR-10487] Misleading about the swift doc. · Issue #52887 · apple/swift · GitHub.

3 Likes

Thanks a lot @hisekaldma I totally agree, the example in the documentation could be clearer.

I understood it completely wrong initially, I am glad that there is an issue tracking it, hope the documentation gets updated with a clearer example.

Kinda missed that this is from the documentation…

Just to colour in some reasoning behind this, which also helps with remembering it:

Setting the endIndex to "one past the end" lets you write code that doesn't need to have a special case for empty collections. std::end does the same in C++

If we instead define endIndex to be "the last element valid instead", then it breaks down for empty collections: because they don't have any elements to point to, you'd need to implement a special-case for it, which must break the rule.

You could design this with 3 possible values (that I can think of), which all suck:

  1. 0: in that case, then you can't distinguish between "has one element, at index 0", or "has no elements"
  2. -1: now you have cases where you end index can be smaller than your start index
  3. Some other sentinel like Int.min

In any case, you need to litter you code with special cases for empty collections.

With the current design, startIndex..<endIndex is always valid for Array.

8 Likes

Thanks a lot @AlexanderM that was clear explanation.

The part that tripped me was the documentation index(_:offsetBy:limitedBy:) | Apple Developer Documentation

In my humble opinion by using limitedBy: numbers.endIndex is not a good idea

Given below is from the documentation, just changing the offset would cause it to crash.

let numbers = [10, 20, 30, 40, 50]
if let i = numbers.index(numbers.startIndex,
                         offsetBy: 4,
                         limitedBy: numbers.endIndex) {
    print(numbers[i])
}

i just really really wish that doccomment had an example of the boundary condition, that shows you get nil when you pass 5 for the offsetBy, because i remember being confused about the same thing when i was first starting to use Swift.

the documentation for String.index(_:offsetBy:limitedBy:) has the same issue, it gives examples for offsetBy: 4 and offsetBy: 6, but not 5 for some reason.

2 Likes

Thanks a lot @taylorswift you are spot on!

Yes I agree, they should have clearly explained it with 5 stating that it would be accessing beyond the bounds if limitedBy: numbers.endIndex is used

An alternative way to think about it is that the design is consistent with (or an example of) preferring half-open interval over closed-interval.

1 Like