Swift3.0版はこちらです。
(2016/03/22): Swift2.2対応しました。1
Swift2.1.1までの書き方も残しています。
なぜ
enum に宣言した全てのcase
がほしい時に、
enum SomeTypes {
case A, B, C, D
var cases: [SomeTypes] {
return [.A, .B, .C, .D]
}
}
としてしまうのは、後にcase
が増えた時に漏れる可能性もあるし、他にも同じことをやろうと思った時に書き方が冗長的になってしまうので、protocolを定義してみました。
実装
Swift2.2 での書き方
public protocol EnumEnumerable {
associatedtype Case = Self
}
public extension EnumEnumerable where Case: Hashable {
private static var generator: AnyGenerator<Case> {
var n = 0
return AnyGenerator {
defer { n += 1 }
let next = withUnsafePointer(&n) { UnsafePointer<Case>($0).memory }
return next.hashValue == n ? next : nil
}
}
@warn_unused_result
public static func enumerate() -> EnumerateSequence<AnySequence<Case>> {
return AnySequence(generator).enumerate()
}
public static var cases: [Case] {
return Array(generator)
}
public static var count: Int {
return cases.count
}
}
Swift2.1.1との差分
-
EnumEnumerable
の typealias が associatedtype に変わっています。 -
anygenerator()
関数が deprecated になったので、AnyGenerator
のイニシャライザに変わっています。 - EnumerateSequenceの
init
が deprecated になったので、AnySequence(generator).enumerate()
に変わっています。
Swift2.1.1までの書き方
public protocol EnumEnumerable {
typealias Case = Self
}
public extension EnumEnumerable where Case: Hashable {
private static var generator: AnyGenerator<Case> {
var n = 0
return anyGenerator {
defer { n += 1 }
let next = withUnsafePointer(&n) { UnsafePointer<Case>($0).memory }
return next.hashValue == n ? next : nil
}
}
@warn_unused_result
public static func enumerate() -> EnumerateSequence<AnySequence<Case>> {
return EnumerateSequence(AnySequence(generator))
}
public static var cases: [Case] {
return Array(generator)
}
public static var count: Int {
return cases.count
}
}
※以下の解説はSwift2.1.1時点のものです。Swift2.2以降で若干コードに差異があります。
ポイントとなるのは generator() の中身で、
参考にした情報2曰く、enumはデフォルトでHashableに適合していて、caseを割り当てた順に、0, 1, 2, ...
とhash値が振られるので、
withUnsafePointer と UnsafePointer を用いてhash値からcaseを復元します。
復元したcaseの hashValue と、復元する時に投げた n の値を見て、一致していたら next を返し、一致しなければ nil を返してgenerateを止めます。
nextを返した後は、nの値をインクリメントします。
例えば、
enum SomeTypes {
case A, B, C, D
}
とした時は、 next.hashValue と n の値の遷移は以下のようになります。
next.hashValue: 0
n: 0
// return A
----
next.hashValue: 1
n: 1
// return B
----
next.hashValue: 2
n: 2
// return C
----
next.hashValue: 3
n: 3
// return D
----
next.hashValue: 0
n: 4
// return nil
// =>ここで異なる値になるので、nextとしてnilを返すことで、正しく全てのcaseが取得できる。
----
hashValueが0→1→2→3→0
となるのはおそらくこの場合の割当が、A,B,C,Dで4つなので、n % 4
としてhashValueが与えられるからだと思ってます。ちょっと自信ない。。
(この例で、nに10を入れてnext.hashValue
を見ると、 2 になっているので間違ってはなさそう。)
あとは、enumerate()で列挙する場合と、当初の目的である配列で取得する場合とを用意します。
尚、enumerate()は、 SequenceType の
extension SequenceType {
@warn_unused_result
public func enumerate() -> EnumerateSequence<Self>
}
と同様に、 EnumSequence として返すようにしています。indexが不要の場合は、 AnySequence で返せば良いです。
@warn_unused_result
static func enumerate() -> AnySequence<Case> {
return AnySequence(generator())
}
使い方
enumに EnumEnumerable を適合するだけです。
enum Hoge: Int, EnumEnumerable
でも、
enum Fuga: String, EnumEnumerable
でも、
enum Piyo: EnumEnumerable
でも、
問題なく使用できます。
enum Hoge: Int, EnumEnumerable {
case A, B, C, D
}
enum Fuga: String, EnumEnumerable {
case A, B, C, D, E
}
enum Piyo: EnumEnumerable {
case A, B, C, D, E, F
}
Hoge.count // 4
Fuga.cases // [Fuga.A, Fuga.B, Fuga.C, Fuga.D, Fuga.E]
Piyo.enumerate().forEach { print($0.0,$0.1) }
/*
0 A
1 B
2 C
3 D
4 E
5 F
*/
注意
デフォルトでenumは Hashable に適合してはいるものの、以下のように Associated Valueを持つcase
を宣言してしまうと、それが無効になるので、上記の列挙方法は使えなくなります。
// これはできない
enum SomeTypes: EnumEnumerable {
case A(String)
case B(Int)
}
//----
SomeTypes.cases // error!
参考
-
また、将来的に、Swift3.0でAnyGeneratorがAnyIteratorに変わる可能性があるので、近くなったらそれに合わせて更新します。 ↩
-
http://stackoverflow.com/a/24562707 AssociatedValueを持たなければ、enumはデフォルトでHashableに適合していると言及されている。
enum Piyo { case A }
としたときに、Piyo.A.hashValue
を呼び出せるので、Hashableに適合しているのは確かである ↩