181

In Swift, is there any way to check if an index exists in an array without a fatal error being thrown?

I was hoping I could do something like this:

let arr: [String] = ["foo", "bar"]
let str: String? = arr[1]
if let str2 = arr[2] as String? {
    // this wouldn't run
    println(str2)
} else {
    // this would be run
}

But I get

fatal error: Array index out of range

Pang
  • 9,564
  • 146
  • 81
  • 122
chrishale
  • 2,436
  • 2
  • 18
  • 19

12 Answers12

550

An elegant way in Swift:

let isIndexValid = array.indices.contains(index)
Manuel
  • 14,274
  • 6
  • 57
  • 130
  • 17
    In real life, it shouldn't matter but time-complexity-wise wouldn't it be much better to use `index < array.count`? – funct7 Sep 03 '16 at 08:15
  • @WoominJoshPark Your solution does not account for cases where the index value is messed up, e.g. 'index=-5' would always return true, so you'd have to test against both boundaries. Then you end up with two conditions: 'if index < array.count && index >= 0'. Then you realize that it's a more error prone code because a typo in numerical comparative conditions is a common issue. Timewise you are right, but as always you have to compromise between efficiency and simple code/error mitigation/comfort. – Manuel Sep 03 '16 at 08:30
  • 21
    In case you wonder what the speed difference is, I measured it with Xcode's tools and it's negligible. https://gist.github.com/masonmark/a79bfa1204c957043687f4bdaef0c2ad – Mason Sep 18 '16 at 10:59
  • @Mason Thanks, I was curious about that but never got around doing it. And I like your class name. – Manuel Sep 18 '16 at 11:01
  • 9
    Just to add another point on why this is right: if you apply the same logic when working on an `ArraySlice`, the first index won't be 0, so doing `index >= 0` won't be a good-enough check. `.indices` instead works in any case. – DeFrenZ Apr 20 '17 at 10:35
  • 3
    I love Swift for this. Here's my use case: building shitty JSON structure for service so had to do this: "attendee3" : names.indices.contains(2) ? names[2] : "" – Lee Probert Aug 01 '17 at 19:44
  • 6
    This is a good answer. To answer @funct7's concern about time complexity, the type returned by the indices method for Array is a range, not an array of integers. Thus, the time complexity is O(1), just as it would be when manually checking against the upper and lower bound. – Chris Laurel Aug 23 '19 at 18:43
  • See @eonist answer below, it takes this one more step and it's an important step. – Andy Weinstein May 13 '20 at 17:02
  • To the point of @funct7's comment about negative index values, it's not that uncommon to have sentinel values like -1 or NSNotFound. For example: https://developer.apple.com/documentation/appkit/nstableview/1535010-selectedrow – Quinn Taylor Sep 08 '20 at 12:47
  • This cant be used in an if statement, – dub Sep 23 '20 at 00:53
  • 1
    @dub Your comment is incorrect, it can of course be used in an if statement. – Manuel Sep 24 '20 at 04:36
84

Type extension:

extension Collection {

    subscript(optional i: Index) -> Iterator.Element? {
        return self.indices.contains(i) ? self[i] : nil
    }

}

Using this you get an optional value back when adding the keyword optional to your index which means your program doesn't crash even if the index is out of range. In your example:

let arr = ["foo", "bar"]
let str1 = arr[optional: 1] // --> str1 is now Optional("bar")
if let str2 = arr[optional: 2] {
    print(str2) // --> this still wouldn't run
} else {
    print("No string found at that index") // --> this would be printed
}
Benno Kress
  • 2,637
  • 27
  • 39
38

Just check if the index is less than the array size:

if 2 < arr.count {
    ...
} else {
    ...
}
Antonio
  • 71,651
  • 11
  • 148
  • 165
  • 4
    What if the array size is unknown? – Nathan McKaskle Dec 16 '15 at 14:37
  • 10
    @NathanMcKaskle The array always knows how many elements it contains, so the size cannot be unknown – Antonio Dec 16 '15 at 18:40
  • 20
    Sorry I wasn't thinking there. Morning coffee was still entering bloodstream. – Nathan McKaskle Dec 16 '15 at 19:22
  • 5
    @NathanMcKaskle no worries... sometimes that happens to me, even after several coffees ;-) – Antonio Dec 16 '15 at 21:33
  • In some ways, this is the best answer, because it runs in O(1) instead of O(n). – ScottyBlades Nov 22 '19 at 04:08
  • This answer is incorrect. The question is `check if an index exists`. The number of elements has nothing to do with whether a specific index exists. – Manuel Apr 26 '20 at 00:32
  • @Manuel please elaborate a little more. Array have sequential indexes, they are incremental and not sparse, so if an array has n items, it's guaranteed that it has indexes from 0 to n-1. You can argue that there are other ways (even more explicit, like `Range`'s `contains()`), but that doesn't invalidate the fact that indexes are sequential. – Antonio Apr 26 '20 at 09:59
  • This logic doesn’t work for slices of an array, which technically are also arrays, but only slices of it. https://developer.apple.com/documentation/swift/arrayslice – Manuel Apr 26 '20 at 12:21
  • Well a slice is actually a different type - you can't, for example, pass an array to a function expecting a slice, and vice-versa. It's perfectly legit to say that this answer won't work for slices, but on the other hand it always works with pure arrays. – Antonio Apr 26 '20 at 16:29
  • @Antonio by definition, an `ArraySlice` is a child of `Array`. The question mentions "array" without giving a specific type, other than a "something like this" example. So the answer should cover the parent type `Array` and be valid for all child types of `Array`, which this answer is not. – Manuel Apr 27 '20 at 17:49
  • What if your value (2 in your example) is in fact negative? – Paul Bruneau Dec 24 '20 at 14:40
  • In that case it would return the wrong result. A check would be needed to throw an exception if the index is negative – Antonio Dec 28 '20 at 21:55
  • This answer works well for me for checking sequential index existence. Thanks – RobbB Jan 03 '21 at 22:14
21

Add some extension sugar:

extension Collection {
  subscript(safe index: Index) -> Iterator.Element? {
    guard indices.contains(index) else { return nil }
    return self[index]
  }
}
if let item = ["a", "b", "c", "d"][safe: 3] { print(item) } // Output: "d"
// or with guard:
guard let anotherItem = ["a", "b", "c", "d"][safe: 3] else { return }
print(anotherItem) // "d"

Enhances readability when doing if let style coding in conjunction with arrays

Sentry.co
  • 5,355
  • 43
  • 38
  • 3
    honestly, this is the most swifty way to do it with maximum readability and clarity – barndog Feb 14 '18 at 09:08
  • 2
    @barndog Love it too. I usually add this as Sugar in any project i Start. Added guard example as well. Credit to the swift slack community for coming up with this one. – Sentry.co Feb 26 '18 at 12:22
  • 1
    good solution...but Output will print "d" in example you are giving... – jayant rawat Feb 15 '20 at 06:21
  • 1
    This should be part of the Swift language. The issue of index out of range is no less problematic than nil values. I would even suggest using similar syntax: maybe something like: myArray[?3]. If myArray is optional, you would get myArray?[?3] – Andy Weinstein May 13 '20 at 17:01
10

the best way.

  let reqIndex = array.indices.contains(index)
  print(reqIndex)
Manish Kumar
  • 997
  • 2
  • 13
  • 30
8

Swift 4 extension:

For me i prefer like method.

// MARK: - Extension Collection

extension Collection {

    /// Get at index object
    ///
    /// - Parameter index: Index of object
    /// - Returns: Element at index or nil
    func get(at index: Index) -> Iterator.Element? {
        return self.indices.contains(index) ? self[index] : nil
    }
}

Thanks to @Benno Kress

YanSte
  • 10,661
  • 3
  • 57
  • 53
6

You can rewrite this in a safer way to check the size of the array, and use a ternary conditional:

if let str2 = (arr.count > 2 ? arr[2] : nil) as String?
Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • 2
    What Antonio suggests is much more transparent (and efficient) Where the ternary would be appropriate is if you had a readily available value to use and eliminate the if altogether, just leaving a let statement. – David Berry Sep 22 '14 at 15:43
  • 2
    @David What Antonio suggests requires two `if` statements instead of one `if` statement in the original code. My code replaces the second `if` with a conditional operator, letting you keep a single `else` instead of forcing two separate `else` blocks. – Sergey Kalinichenko Sep 22 '14 at 15:51
  • 2
    I'm not seeing two if statements in his code, put let str2 = arr[1] in for his ellipsis and it's done. If you replace the OP if let statement with antonio's if statement, and move the assignment inside (or not, since the only usage of str2 is to print it there's no need at all, just put the dereference inline in the println. Not to mention that the ternary operator is just an obscure (to some:)) if/else. If arr. count > 2, then arr[2] can never be nil, so why map it to String? just so you can apply yet another if statement. – David Berry Sep 22 '14 at 21:53
  • 1
    @David The entire `if` from OP's question will end up inside the "then" branch of Antonio's answer, so there would be two nested `if`s. I am viewing OPs code as a small example, so I am assuming that he would still want an `if`. I agree with you that in his example `if` is not necessary. But then again, the entire statement is pointless, because OP knows that the array does not have enough length, and that none of its elements is `nil`, so he could remove the `if` and keep only its `else` block. – Sergey Kalinichenko Sep 23 '14 at 00:42
  • 1
    No it wouldn't. Antonio's if replaces the OP's if statement. Since the array type is [String] you know that it can never contain a nil, so don't need to check anything beyond the length. – David Berry Sep 23 '14 at 01:56
  • @David If you allow that OP knows that all elements are of type `String`, you must also allow that he knows that the length of the array is 2, making the rest of the exercise pointless. That is what I was trying to say in my previous comment. – Sergey Kalinichenko Sep 23 '14 at 02:07
  • Fails if the index you are checking is negative – Paul Bruneau Dec 24 '20 at 14:41
  • @PaulBruneau Except that... `2` can't be negative, can it? – Sergey Kalinichenko Dec 24 '20 at 14:48
  • No, 2 can’t be negative. But often in programming we will use these things called “variables” which can often be negative, so pretend instead of “2” it was a thing called “i” that could be any value. – Paul Bruneau Dec 25 '20 at 23:02
  • @PaulBruneau The answer is formulated in the same terms as the question. The question doesn't ask about an index supplied through a variable; it asks specifically about the index of 2, so the answer isn't going fancy on the condition either. There this thing in people's heads called "brain" which often helps them generalize helpful answers - say, by extending `arr.count > 2` to `arr.count > i && i >= 0` or something to that effect. – Sergey Kalinichenko Dec 26 '20 at 00:08
  • History will judge which of us is being obtuse – Paul Bruneau Dec 27 '20 at 01:41
2
extension Array {
    func isValidIndex(_ index : Int) -> Bool {
        return index < self.count
    }
}

let array = ["a","b","c","d"]

func testArrayIndex(_ index : Int) {

    guard array.isValidIndex(index) else {
        print("Handle array index Out of bounds here")
        return
    }

}

It's work for me to handle indexOutOfBounds.

Pratik Sodha
  • 3,679
  • 2
  • 19
  • 38
2

Swift 4 and 5 extension:

As for me I think this is the safest solution:

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

Example:

let array = ["foo", "bar"]
if let str = array[safe: 1] {
    print(str) // "bar"
} else {
    print("index out of range")
}
RunesReader
  • 7,137
  • 1
  • 14
  • 12
1

Asserting if an array index exist:

This methodology is great if you don't want to add extension sugar:

let arr = [1,2,3]
if let fourthItem = (3 < arr.count ?  arr[3] : nil ) {
     Swift.print("fourthItem:  \(fourthItem)")
}else if let thirdItem = (2 < arr.count ?  arr[2] : nil) {
     Swift.print("thirdItem:  \(thirdItem)")
}
//Output: thirdItem: 3
Sentry.co
  • 5,355
  • 43
  • 38
1

I believe the existing answers could be improved further because this function could be needed in multiple places within a codebase (code smell when repeating common operations). So thought of adding my own implementation, with reasoning for why I considered this approach (efficiency is an important part of good API design, and should be preferred where possible as long as readability is not too badly affected). In addition to enforcing good Object-Oriented design with a method on the type itself, I think Protocol Extensions are great and we can make the existing answers even more Swifty. Limiting the extension is great because you don’t create code you don’t use. Making the code cleaner and extensible can often make maintenance easier, but there are trade-offs (succinctness being the one I thought of first).

So, you can note that if you'd ONLY like to use the extension idea for reusability but prefer the contains method referenced above you can rework this answer. I have tried to make this answer flexible for different uses.

TL;DR

You can use a more efficient algorithm (Space and Time) and make it extensible using a protocol extension with generic constraints:

extension Collection where Element: Numeric { // Constrain only to numerical collections i.e Int, CGFloat, Double and NSNumber
  func isIndexValid(index: Index) -> Bool {
    return self.endIndex > index && self.startIndex <= index
  }
}

// Usage

let checkOne = digits.isIndexValid(index: index)
let checkTwo = [1,2,3].isIndexValid(index: 2)

Deep Dive

Efficiency

@Manuel's answer is indeed very elegant but it uses an additional layer of indirection (see here). The indices property acts like a CountableRange<Int> under the hood created from the startIndex and endIndex without reason for this problem (marginally higher Space Complexity, especially if the String is long). That being said, the Time Complexity should be around the same as a direct comparison between the endIndex and startIndex properties because N = 2 even though contains(_:) is O(N) for Collections (Ranges only have two properties for the start and end indices).

For the best Space and Time Complexity, more extensibility and only marginally longer code, I would recommend using the following:

extension Collection {
  func isIndexValid(index: Index) -> Bool {
    return self.endIndex > index && self.startIndex <= index
  }
}

Note here how I've used startIndex instead of 0 - this is to support ArraySlices and other SubSequence types. This was another motivation to post a solution.

Example usage:

let check = digits.isIndexValid(index: index)

For Collections in general, it's pretty hard to create an invalid Index by design in Swift because Apple has restricted the initializers for associatedtype Index on Collection - ones can only be created from an existing valid Collection.Index (like startIndex).

That being said, it's common to use raw Int indices for Arrays because there are many instances when you need to check random Array indices. So you may want to limit the method to fewer structs...

Limit Method Scope

You will notice that this solution works across all Collection types (extensibility), but you can restrict this to Arrays only if you want to limit the scope for your particular app (for example, if you don't want the added String method because you don't need it).

extension Array {
  func isIndexValid(index: Index) -> Bool {
    return self.endIndex > index && self.startIndex <= index
  }
}

For Arrays, you don't need to use an Index type explicitly:

let check = [1,2,3].isIndexValid(index: 2)

Feel free to adapt the code here for your own use cases, there are many types of other Collections e.g. LazyCollections. You can also use generic constraints, for example:

extension Collection where Element: Numeric {
  func isIndexValid(index: Index) -> Bool {
    return self.endIndex > index && self.startIndex <= index
  }
}

This limits the scope to Numeric Collections, but you can use String explicitly as well conversely. Again it's better to limit the function to what you specifically use to avoid code creep.

Referencing the method across different modules

The compiler already applies multiple optimizations to prevent generics from being a problem in general, but these don't apply when the code is being called from a separate module. For cases like that, using @inlinable can give you interesting performance boosts at the cost of an increased framework binary size. In general, if you're really into improving performance and want to encapsulate the function in a separate Xcode target for good SOC, you can try:

extension Collection where Element: Numeric {
  // Add this signature to the public header of the extensions module as well.
  @inlinable public func isIndexValid(index: Index) -> Bool {
    return self.endIndex > index && self.startIndex <= index
  }
}

I can recommend trying out a modular codebase structure, I think it helps to ensure Single Responsibility (and SOLID) in projects for common operations. We can try following the steps here and that is where we can use this optimisation (sparingly though). It's OK to use the attribute for this function because the compiler operation only adds one extra line of code per call site but it can improve performance further since a method is not added to the call stack (so doesn’t need to be tracked). This is useful if you need bleeding-edge speed, and you don’t mind small binary size increases. (-: Or maybe try out the new XCFrameworks (but be careful with the ObjC runtime compatibility for < iOS 13).

Pranav Kasetti
  • 8,770
  • 2
  • 50
  • 71
-1

I think we should add this extension to every project in Swift

extension Collection {
    @inlinable func isValid(position: Self.Index) -> Bool {
        return (startIndex..<endIndex) ~= position
    }
    
    @inlinable func isValid(bounds: Range<Self.Index>) -> Bool {
        return (startIndex..<endIndex) ~= bounds.upperBound
    }
    
    @inlinable subscript(safe position: Self.Index) -> Self.Element? {
        guard isValid(position: position) else { return nil }
        return self[position]
    }
    
    @inlinable subscript(safe bounds: Range<Self.Index>) -> Self.SubSequence? {
        guard isValid(bounds: bounds) else { return nil }
        return self[bounds]
    }
}

extension MutableCollection {
    @inlinable subscript(safe position: Self.Index) -> Self.Element? {
        get {
            guard isValid(position: position) else { return nil }
            return self[position]
        }
        set {
            guard isValid(position: position), let newValue = newValue else { return }
            self[position] = newValue
        }
    }
    @inlinable subscript(safe bounds: Range<Self.Index>) -> Self.SubSequence? {
        get {
            guard isValid(bounds: bounds) else { return nil }
            return self[bounds]
        }
        set {
            guard isValid(bounds: bounds), let newValue = newValue else { return }
            self[bounds] = newValue
        }
    }
}

note that my isValid(position:) and isValid(bounds:) functions is of a complexity O(1), unlike most of the answers below, which uses the contains(_:) method which is of a complexity O(n)


Example usage:

let arr = ["a","b"]
print(arr[safe: 2] ?? "nil") // output: nil
print(arr[safe: 1..<2] ?? "nil") // output: nil

var arr2 = ["a", "b"]
arr2[safe: 2] = "c"
print(arr2[safe: 2] ?? "nil") // output: nil
arr2[safe: 1..<2] = ["c","d"]
print(arr[safe: 1..<2] ?? "nil") // output: nil
Mendy
  • 176
  • 2
  • 8