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 Collection
s (Range
s 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 ArraySlice
s and other SubSequence
types. This was another motivation to post a solution.
Example usage:
let check = digits.isIndexValid(index: index)
For Collection
s 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 Array
s 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 Array
s 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 Array
s, 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 Collection
s e.g. LazyCollection
s. 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
Collection
s, 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 XCFramework
s (but be careful with the ObjC runtime compatibility for < iOS 13).