count
returns an IndexDistance
which is the type describing
the distance between two collection indices. IndexDistance
is
required to be a SignedInteger
, but need not be an Int
and can
be different from Index
. Therefore it is not possible to create
the range 0..<count - 1
.
A solution is to use startIndex
and endIndex
instead of 0
and count
:
extension MutableCollection where Index == Int {
/// Shuffle the elements of `self` in-place.
mutating func shuffle() {
// empty and single-element collections don't shuffle
if count < 2 { return }
for i in startIndex ..< endIndex - 1 {
let j = Int(arc4random_uniform(UInt32(endIndex - i))) + i
if i != j {
swap(&self[i], &self[j])
}
}
}
}
Another advantage is that this also works correctly with array slices
(where the index of the first element is not necessarily zero).
Note that according to the new "Swift API Design Guidelines",
shuffle()
is the "proper" name for a mutating shuffle method,
and shuffled()
for the non-mutating counterpart which returns an array:
extension Collection {
/// Return a copy of `self` with its elements shuffled
func shuffled() -> [Iterator.Element] {
var list = Array(self)
list.shuffle()
return list
}
}
Update: A (even more general) Swift 3 version has been added to
How do I shuffle an array in Swift? in the meantime.
For Swift 4 (Xcode 9) one has to replace the call to the swap()
function by a call to the swapAt()
method of the collection.
Also the restriction on the Index
type is no longer needed:
extension MutableCollection {
/// Shuffle the elements of `self` in-place.
mutating func shuffle() {
for i in indices.dropLast() {
let diff = distance(from: i, to: endIndex)
let j = index(i, offsetBy: numericCast(arc4random_uniform(numericCast(diff))))
swapAt(i, j)
}
}
}
See SE-0173 Add MutableCollection.swapAt(_:_:)
for more information about swapAt
.
As of Swift 4.2 (Xcode 10, currently in beta), with the implementation of
SE-0202 Random Unification,
shuffle()
and shuffled()
are part of the Swift standard library.
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…