enumの列挙子を配列で取得するのをprotocolで

  • 29
    Like
  • 0
    Comment
More than 1 year has passed since last update.

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との差分

  • EnumEnumerabletypealiasassociatedtype に変わっています。
  • anygenerator()関数が deprecated になったので、 AnyGeneratorのイニシャライザに変わっています。
  • EnumerateSequenceのinitdeprecated になったので、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値が振られるので、
withUnsafePointerUnsafePointer を用いてhash値からcaseを復元します。
復元したcaseの hashValue と、復元する時に投げた n の値を見て、一致していたら next を返し、一致しなければ nil を返してgenerateを止めます。
nextを返した後は、nの値をインクリメントします。

例えば、

enum SomeTypes {
    case A, B, C, D
}

とした時は、 next.hashValuen の値の遷移は以下のようになります。

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!

参考


  1. また、将来的に、Swift3.0でAnyGeneratorがAnyIteratorに変わる可能性があるので、近くなったらそれに合わせて更新します。 

  2. http://stackoverflow.com/a/24562707 AssociatedValueを持たなければ、enumはデフォルトでHashableに適合していると言及されている。
    enum Piyo { case A } としたときに、Piyo.A.hashValueを呼び出せるので、Hashableに適合しているのは確かである