Swift 3のRange徹底解説

  • 90
    いいね
  • 0
    コメント

Swift 3のRange周りはけっこうややこしいので、まとめてみました。

Range周りのややこしさはこのあたりに起因していると思っています。

  • Swift 3のRangeの仕様自体少し複雑
    • 現状の言語制約・型安全性など踏まえて妥当な複雑性だと思っていますが
  • Swift 2.2から大きく変更
    • Swift 2.2: Range<T>ClosedInterval<T>OpenInterval<T>
    • Swift 3.0: Range<T>ClosedRange<T>CountableRange<T>CountableClosedRange<T>
    • 再整理された感じなので、単純な置き換えでは無く、新たな理解が必要な部分も多いです

それでは、Swift 3.0のRange周りについて、説明していきます。

Swift 3.0のRange周り解説

次のように、Half-Open(..<)/Closed(...) x Comparable/Strideableのマトリックスで、RangeClosedRangeCountableRangeCountableClosedRangeという4種の型があります。

要素 Half-Open(..<) Closed(...)
Comparableのみ Range ClosedRange
Strideable(Int刻み) CountableRange CountableClosedRange

Half-Open(..<)/Closed(...)は、次のようにendの要素を含むか否かが区別されます。こちらはSwiftを普通に書いているとお馴染みだと思います。

  • Half-Open(..<): 例: 0..<3[0, 1, 2]相当
  • Closed(...): 例: 0...3[0, 1, 2, 3]相当

コレクション要素がComparable/Strideable(Int刻み)とは、次のように区別されます。

  • Comparable: 比較演算のみ可能 → 非CountableなRange
  • Strideable(Int刻み): Int刻みでの前後走査も可能 → CountableなRange

Comparable/Strideable(Int刻み)について

Comparable/Strideable(Int刻み)についてはもう少し詳しく説明します。

CountableなRange(要素がStrideableなRange)のみ、ランダムアクセス可能なコレクションであり、SequenceCollectionの有する便利なコレクション走査メソッド(for-in操作・mapfilter)を扱えます。

for i in 0..<3 {
    print(i)
}

StrideableなものはIntのRangeなどです。

そうではない、つまりCountableでは無い例としては、FloatCharacterのRangeが挙げられます。
FloatStrideableではあるものの、刻みがFloatなので、Countableでは無いです。

DoubleのRangeをシーケンス操作する次のコードを書くと、Type 'Range<Double>' does not conform to protocol 'Sequence'というコンパイルエラーが発生します。

for i in 0.0..<3.0 {
    print(i)
}

つまり、0.0の「次の値」が不定ということです。0.1かもしれないし、1.0かもしれないし、それはFloatの仕様では確定されません。
ちなみに、次のように刻みを明示(この場合0.1)すれば、イテレート操作可能となります。

for i in stride(from: 0.0, to: 3.0, by: 0.1) {
    print(i)
}

ただ、これはSequence型に準拠したStrideTo型で、Range系の型とは似て非なるものです。

また、Character型についても次のように書くと、同じコンパイルエラーが出ます。

for c in Character("a")...Character("z") {
    print(c)
}

"a"のつぎはASCIIコード的に"b"では?と思うかもしれません。

確かに、例えばRubyの場合は、次のコードを書くと、

("a".."z").each { |c| p c }

このように出力されます。

a
b
c
...

Swiftの場合は、Stringが厳密な実装(ちょっとこの表現難しいですが)になっていて、単純に"a"の次は"b"とみなさないような仕様になっています。
近々String周りも解説記事書きたいと思っていますが、とりあえず今気になる方は Strings in Swift 3 – Ole Begemann などご参照ください。

ちなみに次のように書くと、Swiftでも出来ます( ´・‿・`)
これはASCIIコードと明示してイテレートしてるので、厳密で良いと思っています。

extension Character
{
    init?(asciiCode: UInt32) {
        guard let scalar = UnicodeScalar(asciiCode) else {
            return nil
        }
        self = Character(scalar)
    }
    func asciiCode() -> UInt32 {
        let characterString = String(self)
        let scalars = characterString.unicodeScalars

        return scalars[scalars.startIndex].value
    }
}

for code in Character("a").asciiCode()...Character("z").asciiCode() {
    print(Character(asciiCode: code)!)
}

以上、簡潔にまとめると、次のようになります。

  • CountableなRange: コレクション系操作が可能
    • IntなどのRange
  • CountableなRange: 範囲を示すことなどに、用途が限られる
    • DoubleCharacterなどのRange

Half-Open(..<)/Closed(...)が分かれている理由

Swift 2.2では両方ともRangeでした。
さらに詳しく書くと、Closed(...)と書いても、それは表記の違いだけで、Half-Open(..<)として扱われていました。

次の評価も、trueとなってしました。

(0..<2) == (0...1)

ただ、問題があって、例えば0以上の整数すべてのレンジを表現したい時に0...Int.max0..<Int.maxより1多い何かみたいな感じに解釈され、結果overflow系のコンパイルエラーが発生してしまっていました。

それでは、switch分内のcaseで0以上の整数すべてを受けたい時など困る、ということでClosedIntervalOpenIntervalというマッチ用ともいえる型がありました。

次のコードはSwift 3.0では動かずSwift 2.2など用のコードですが、as ClosedIntervalを外すとエラーになります。

let value = 5
switch value {
    case 0...Int.max as ClosedInterval:
        print("zero or positive")
    default:
        print("default")
}

(こちらから確認できます: http://swiftlang.ng.bluemix.net/#/repl/584bbfe334efab1c6b31befb)

これがイマイチなどの理由があって、レンジ系全般がHalf-Open(..<)/Closed(...)とに明確に分けられました。そして、ClosedIntervalOpenIntervalは不要になったので、無くなりました。

Swift 3.0についてまとめると、次の通りです。

  • Half-Open(..<)のみ、空のRangeを表現出来る(5..<5など)
  • Closed(...)のみ、その要素型のmax値を含めた書き方が出来る(0...Int.maxなど)

Countableとそうでないものが別物の型になっている理由

本当は次のようなコードで、Range型は共通として、ジェネリクスで要素がどのプロトコルに準拠しているかなどで表現したいということみたいですが、Swift 3ではサポート外でコンパイルエラーになってしまいます。

extension Range: RandomAccessCollection
    where Bound: Strideable, Bound.Stride: SignedInteger
{    
}

Swift 4のメインゴールの1つであるConditional conformancesが実現出来れば、型を分ける必要はなくなり、次の2つに集約されシンプルになるはずです。

  • Range
  • ClosedRange

これによってまた仕様が変わるのか?と思うかもしれませんが、そうではあるものの概念的にはほぼ一緒な上で扱いやすくなるので悪影響は少ないと思います。Countableかどうかで型を明示している箇所でちょっと調整が必要になる程度でしょう(この型の明示が必要な場面があまり思いつきませんが)。

Swift 3対応にあたって

今までRangeだったのが、Half-Open(..<)/Closed(...)という2つの型に分かれたのが少し厄介です。
今までRangeを引数としていたところで、この2つの型の両対応が出来なくなってしまいました。

解1: オーバーロード

これが現状一番素直な方法に思います。

例えば、SwiftのStringもそうなっています:
String - Swift Standard Library | Apple Developer Documentation

Screen Shot 2016-09-28 at 14.35.28.png

解2: 呼び出し側で書き換え

Swift 2.2と全く同じ挙動になるように、呼び出し側をHalf-Open(..<)に変換することで安全に対応出来そうです。既存コードを手っ取り早く直したい場合などに良いと思います。
ただし、CountableなRangeでしかこの対応は出来ません。

func doSomething2<T>(for range: CountableRange<T>) {
        for number in range {
            print(number)
        }
}

// 既存コードがこの時、`Closed`か同化が非合致でコンパイルエラーになる
// doSomething2(for: 0...10)) 

// 対応1
doSomething2(for: CountableRange(0...10)) // → `0..<11`に変換される
// 対応2
doSomething2(for: 0..<11))

解3: ジェネリクスでメソッド定義をがんばる

Swift 3 migration pitfalls — Codelleに載っていました。
(そのサイトでは、IterableRange.Iterator.Element: Intだったのを汎用的に: Strideに変えました)

CountableなRangeに対しての対応例:

func doSomething<IterableRange>(for range: IterableRange)
    where IterableRange: Sequence, IterableRange.Iterator.Element: Strideable {
        for number in range {
            print(number)
        }
}

Indexがsuccessor()predecessor()advancedBy(_:)advancedBy(_:limit:)distanceTo(_:)などを持たなくなった

最後に、ちょっとここまでの話と変わりますが、Range周りの変更として、もう1つ紹介します。

コレクション系の型には、そのそれぞれの要素が属する場所(インデックス)を示すIndex型があります。Index自身が走査系のメソッドを持っていましたが、それがそのIndexのオーナーであるコレクションに移りました。

例として、Swift 2.2以前のコードが色々載っているString.Indexを使った文字列処理 - Qiita から一部コードをお借りして、書き換え例を載せておきます。

Swift 2.2:

let str = "ABC"
let idx0 = str.startIndex
let idx1 = idx0.successor()
let idx2 = idx1.successor()
str[idx0] // => 'A' Character型
str[idx1] // => 'B' Character型
str[idx2] // => 'C' Character型

Swift 3.0:

let str = "ABC"
let idx0 = str.startIndex
let idx1 = str.index(after: idx0)
let idx2 = str.index(after: idx1)
str[idx0] // => 'A' Character型
str[idx1] // => 'B' Character型
str[idx2] // => 'C' Character型

ちゃんと理解していれば大したことないですが、そうで無いと書き換え方に悩むかもしれませんね。

参考