Add MutableCollection.swapAt(_:_:)
01 何が問題だったのか
Swift には従来、2つの値を入れ替えるためのフリー関数 swap(_:_:) が用意されていました。swap の主な用途はコレクション内の2要素の入れ替えであり、標準ライブラリの sort の実装などで次のように使われてきました。
while hi != lo {
swap(&elements[lo], &elements[hi])
// ...
}
しかし、Swift が進めている Law of Exclusivity(排他性の法則)のもとでは、この書き方は違法になります。同じ変数(ここでは elements)を、同一の関数呼び出しに対して2つの異なる inout 引数として同時に渡すことができなくなるためです。つまり、swap のもっとも典型的な使い方そのものが、将来的に Swift コードとして受け付けられなくなる状況でした。
一方で、単なる2つの独立した変数の入れ替えであれば、そもそも関数を使う必要はなく、タプル代入で書くのが推奨されるスタイルです。
var a = 0
var b = 1
// swap(&a, &b) の代わりに
(a, b) = (b, a)
したがって、swap の本来のユースケースである「コレクション内の2要素の入れ替え」を、排他性に違反しない形で表現する新しい API が必要でした。
02 どのように解決されるのか
MutableCollection プロトコルに、2つのインデックスを受け取って対応する要素を入れ替えるメソッド swapAt(_:_:) が追加されました。
protocol MutableCollection {
/// インデックス i と j の位置にある値を交換します。
///
/// i と j が等しい場合は何もしません。
public mutating func swapAt(_ i: Index, _ j: Index)
}
これにより、従来 swap(&elements[lo], &elements[hi]) と書いていた処理は、単一のコレクション elements に対するメソッド呼び出しとして表現できるようになり、排他性の法則にも違反しません。
var numbers = [1, 2, 3, 4, 5]
numbers.swapAt(0, 4)
// numbers == [5, 2, 3, 4, 1]
while hi != lo {
elements.swapAt(lo, hi)
// ...
}
同一要素の交換は許容される
既存の swap(_:_:) は、同じ要素を自分自身と入れ替えようとすると実装上の理由から fatalError で停止する仕様でした。そのため、呼び出し側が事前にインデックスが一致していないかをチェックする必要がありました。同一要素の交換は sort のフェンスポストバグなど論理エラーのことも多い一方、shuffle の実装などでは自然に発生しうる正当なケースです。
swapAt(_:_:) ではこの事前条件は取り除かれ、i と j が同じインデックスのときは単に何もしないという素直な挙動になっています。
既存の swap の扱い
フリー関数の swap(_:_:) は非推奨(deprecated)化され、将来のバージョンで削除される予定です。コレクションの2要素を入れ替えている既存コードは、swapAt(_:_:) への移行が必要です。
ただし、ムーブオンリー型の導入など将来の言語機能と組み合わせた特殊な状況では swap が性能上有利になる可能性があるため、関数自体は当面残されます。