[2018/05/22 追記]
Swift 4.2からは、Range
の型パラメーター(Bound
)によってCountable
かどうかが区別できるようになるため、Countable
系のRange
は無くなりシンプルになります。
Swift 4.1で導入されたConditional Conformanceのインパクト - Qiita
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 2.2:
それでは、Swift 3.0のRange周りについて、説明していきます。
Swift 3.0のRange周り解説
次のように、Half-Open(..<)
/Closed(...)
x Comparable
/Strideable
のマトリックスで、Range
・ClosedRange
・CountableRange
・CountableClosedRange
という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)のみ、ランダムアクセス可能なコレクションであり、Sequence・Collectionの有する便利なコレクション走査メソッド(for-in
操作・map
・filter
)を扱えます。
for i in 0..<3 {
print(i)
}
Strideable
なものはInt
のRangeなどです。
そうではない、つまりCountable
では無い例としては、Float
やCharacter
のRangeが挙げられます。
Float
もStrideable
ではあるものの、刻みが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: 範囲を示すことなどに、用途が限られる-
Double
・Character
などのRange
-
Half-Open(..<)
/Closed(...)
が分かれている理由
Swift 2.2では両方ともRange
でした。
さらに詳しく書くと、Closed(...)
と書いても、それは表記の違いだけで、Half-Open(..<)
として扱われていました。
次の評価も、true
となってしました。
(0..<2) == (0...1)
ただ、問題があって、例えば0以上の整数すべてのレンジを表現したい時に0...Int.max
が0..<Int.maxより1多い何か
みたいな感じに解釈され、結果overflow系のコンパイルエラーが発生してしまっていました。
それでは、switch
分内のcase
で0以上の整数すべてを受けたい時など困る、ということでClosedInterval
・OpenInterval
というマッチ用ともいえる型がありました。
次のコードは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(...)
とに明確に分けられました。そして、ClosedInterval
・OpenInterval
は不要になったので、無くなりました。
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
解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型
ちゃんと理解していれば大したことないですが、そうで無いと書き換え方に悩むかもしれませんね。