Access Slice element with slice index

Hello Swift Community,

When working with collections in Swift, accessing elements via indices in slices can be quite counterintuitive, especially for those new to the language or working in generics or with Data. The Collection.subscript(position:) method accesses an element at a specified position, but when it comes to slices, things get a bit tricky.

Examples Demonstrating the Issue:

1. Slicing a Slice

let data = [1, 2, 3]
print(data[1...][1...])  // Expecting [3], but outputs [2, 3]

The output is [2, 3] because the indices of the slice match those of the original array.

2. Enumerating over a Slice

let array = [10, 20, 30, 40, 50]
let slice = array[1...3]

for (index, element) in slice.enumerated() {
    print("Index: \(index), Element: \(element)")
    print(slice[index])  // index out of range
}

This code snippet fails as the index is the index for EnumeratedSequence.

Workaround & Discussion

These index disparities can be handled by manually adjusting with startIndex, but this approach is cumbersome and error-prone:

print(slice[slice.startIndex + index])  // Adjust with startIndex

This approach loses the simplicity and clarity that accessing elements by index usually has in many other programming contexts (like C, where element access is often just pointer arithmetic).

Additionally, when working with generics, such as in the function:

func foo<T>(collection: Collection<T>)

or when dealing with structures like Data, where the slice type is the same as the original type, these index adjustments can become pervasive throughout the code.

Potential Solution?

A straightforward solution could involve modifying the Slice and other structures to adjust the indices internally. For example:

struct Slice<Base> where Base: Collection, Base.Index == Int {
    let base: Base
    let startIndex: Base.Index
    
    subscript(position: Base.Index) -> Base.Element {
        base[startIndex + position]
    }
}

While this solves the problem, it would indeed be a source-breaking change. I am curious about how you handle this issue or if a proposal to adjust slicing behavior might be worth considering for future versions of Swift.

Looking forward to your insights and discussions on this topic. How do you manage slice indices, and what improvements would you suggest?

1 Like

FWIW, my personal solution is to extend RandomAccessCollection like this:

extension RandomAccessCollection {
  @inlinable
  public func index(forRelativeIndex relativeIndex: Int) -> Self.Index {
    return self.index(self.startIndex, offsetBy: relativeIndex)
  }

  @inlinable
  public subscript(relativeIndex relativeIndex: Int) -> Self.Element {
    return self[self.index(forRelativeIndex: relativeIndex)]
  }
}
4 Likes

This is the issue, here:

The first value in the tuples returned by .enumerated() is not an index, but an offset. It always counts up from 0, regardless of the indices of the collection. This happens to work exactly how Array indices work, but it breaks in the general case, e.g. Dictionary, which is indexed by non-numeric indices of type Dictionary.Index.

The correct way to iterate over indices and elements together, is by zipping them:

for (index, element) in zip(slice.indices, slice) { ... }

The resulting indices will always be correct and usuable with the subscript operator, whether the collection is a simple Array, a slice, or some type with exotic indices, like , String, Dictionary or Set.

See: Blog/Proper Array Iteration.md at master · amomchilov/Blog · GitHub

5 Likes