RangeSet と IndexSet の相互変換
RangeSet/IndexSet Conversion
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
SE-0270 で標準ライブラリに導入された RangeSet は、コレクションのインデックスからなる「連続していない範囲の集合」を表す型です。Bound が Comparable であれば任意のインデックス型で使えます。
一方、Foundation には以前から IndexSet という型があり、Objective-C 由来の API などでインデックス集合を受け渡すために使われてきました。IndexSet は Objective-C コレクションの制約に合わせ、非負の整数インデックスのみを扱える専用の型です。
両者は目的が近いものの、それぞれの API はもう一方の型を直接受け付けません。そのため、たとえば次のような場面で開発者が困っていました。
- 新しい Swift API を
RangeSet<Int>ベースで設計しつつ、内部実装でIndexSetを要求する既存の Objective-C API を呼び出したい場合 - 既存の SDK の
IndexSetベース API と、自分のコードで使っているRangeSet<Int>の値とを行き来させたい場合
これらは Foundation 側に変換用のイニシャライザを提供すれば、正しさと性能の両面で安全に橋渡しできます。
02 どのように解決されるのか
Foundation に、IndexSet と RangeSet<Int> を相互に変換するためのイニシャライザが追加されます。FoundationPreview 0.4 以降で利用できます。
extension IndexSet {
@available(FoundationPreview 0.4, *)
public init?(integersIn indices: RangeSet<Int>)
}
extension RangeSet<Int> {
@available(FoundationPreview 0.4, *)
public init(_ indices: IndexSet)
}
RangeSet<Int> から IndexSet への変換は failable イニシャライザになっています。IndexSet は非負の整数しか保持できませんが、その制約を型システムで静的に表現することはできないため、実行時にチェックが必要です。負のインデックスを含む RangeSet が渡されたときには、クラッシュさせるのではなく nil を返す方針が採られました。これは、変換イニシャライザが Swift 側の API から渡された RangeSet を Objective-C 側の API に橋渡しする目的で使われることが多く、入力が呼び出し元から来る不定値であるケースが想定されるためです。確実に有効なインデックスしか来ないことが分かっている場合は force-unwrap で表明し、そうでない場合は何もしない、ログを出す、別の処理に切り替えるなど、呼び出し側で挙動を選べるようになっています。
IndexSet から RangeSet<Int> への変換は、IndexSet の値域は常に有効なので、失敗しない通常のイニシャライザです。
たとえば、RangeSet<Int> を受け取る Swift API を、内部で既存の IndexSet ベースの Objective-C API(UICollectionView.reloadSections(_:) など)に橋渡ししたい場合は、次のように書けます。
extension UICollectionView {
func reloadSections(_ sections: RangeSet<Int>) {
guard let indexSet = IndexSet(integersIn: sections) else {
fatalError("Invalid section numbers passed to reloadSections(_:). Sections must be non-negative integers")
}
self.reloadSections(indexSet) // 既存の ObjC API の呼び出し
}
}
引数ラベルは、IndexSet 側が integersIn:、RangeSet 側はラベルなしになっています。これは既存の API のスタイルに合わせたものです。IndexSet には init(integersIn: Range<Int>) のように integersIn: ラベルを使うイニシャライザがすでにあり、RangeSet には init(_ range: Range<Bound>) や init(_ ranges: some Sequence<Range<Bound>>) のようにラベルなしのイニシャライザがあるので、それぞれの慣習に揃える形になっています。