Swift Digest

RangeSetIndexSet の相互変換

RangeSet/IndexSet Conversion

Proposal
SF-0005
Authors
Jeremy Schonfeld
Review Manager
Charles Hu
Status
Accepted

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

SE-0270 で標準ライブラリに導入された RangeSet は、コレクションのインデックスからなる「連続していない範囲の集合」を表す型です。BoundComparable であれば任意のインデックス型で使えます。

一方、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 に、IndexSetRangeSet<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>>) のようにラベルなしのイニシャライザがあるので、それぞれの慣習に揃える形になっています。