Swift Ranges
A range is an interval of values and is defined by its lower and upper bounds.
Two range creators ..<
for half-open ranges that don;t include their upper bound, and ... for closed ranges that include both bounds
let singleDigitNumbers = 0..<10
Array(singleDigitNumbers)
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let lowercaseLetters = Character("a")...Character("z")
lowercaseLetters
// {lowerBound "a", upperBound "z"}
There's also prefix and postfix variants of these operators
let fromZero = 0... // PartialRangeFrom<Int>
let upToZ = ..<Character("z") // PartialRangeUpTo<Character>
There are five concrete implementations of ranges each captures constraints on the value. Most common two are Range
(a half-open range, ..<
) and ClosedRange
(created using ...
).
Both have a generic Bound
parameter: the only requirement is that Bound
must conform to Comparable
.
Meaning the above lowercaseLetters
expression is of type ClosedRange<Character>
.
- Only a half-open range
Range
can represent an empty interval (when the lower and uppoer bounds are equal) - Only a closed range
ClosedRange
can contain the maximum value of its element type (0...Int.max).
Countable ranges
Ranges can be looped over and treated as a collection, however, Character
does not conform to protocol Strideable
because of an issue with Unicode, thus character ranges are non-iterable.
In other words, the range must be Countable
in order for it to be iterated over. To iterate over the elements use stride(from:to:by)
and stride(from:through:by)
functions.
Partial ranges
Partial ranges are created with ...
or ..<
as a prefix or a postfix operator. They are called partial because there is only the starting bound. Thus there are three kinds
let fromA: PartialRangeFrom<Character> = Character("A")...
let throughZ: PartialRangeThrough<Character> = ...Character("Z")
let upTo100: PartialRangeUpTo<Int> = ..<100
When you iterate over a countable PartialRangeFrom
, iteration starts with the lower bound and repeatedly calls advanced(by: 1)
. If you use such a range in a for
loop, you must take care to add a break condition. PartialRangeThrough
and PartialRangeUpTo
aren't countable.
Expressions
All five ranges conform to RangeExpression
protocol, which computes a fully-specified range with given constraint. Here's it's simple implementation
public protocol RangeExpression {
associatedtype Bound: Comparable
func contains(_ element: Bound) -> Bool
func relative<C>(to collection: C) -> Range<Bound> where C: Collection, Self.Bound == C.Index
}
For partial expressions with a missing lower bound, the relative(to:)
method adds the collection's startIndex
as the lower bound. For partial ranges with a missing upper bound, the method will use the collection's endIndex
. Partial ranges enable a very compact xyntax for sclicing collections
let array = [1,2,3,4] // [1, 2, 3, 4]
array[2...] // [3, 4]
array[..<1] // [1]
array[1...2] // [2, 3]
array[...] // [1, 2, 3, 4]
type(of: array[...]) // ArraySlice<Int>.Type
Think of ArraySlice
as of a SQL table view. This way Swift reduces computational costs - array elements do not get copied.
Notably, ArraySlice
conforms to Collection
protocol, as Array
does, so both share same methods. If you'd need to convert the view into real array
Array(array[...]) // [1, 2, 3, 4]
This works because the corresponding subscript declaration in the Collection
protocol takes a RangeExpression
rather than one of the concrete range types.
This is implemented as a special case in the standard library as unbounded range.
In case of making your own functions take in range, it's really advisable to copy paste from the standard library. This way clients of my APIs are always happy!
Top comments (0)