Swift プログラミング
基本構文
Swift の列挙型 (enum) では列挙子に Int などの Raw 値を設定してプログラムで使えるようになっています。
そんな Raw 値には独自のクラスも使用できます。
Swift の列挙型で独自クラスを Raw 値に使う
Swift言語 のenum型 で使えるRaw 値について
Swift言語 の列挙型 では、各列挙子にInt型 などのRaw 値を割り当てて、その値を使ってプログラムを制御できるようになっています。
このRaw
値は、Objective-C言語
でもNS_ENUM
を使って整数型を指定できるようになっていましたが、Swift言語
ではDouble型
やString型
のような、リテラルで値を初期化できるクラスも指定できます。
独自のクラスでも条件を満たせばenum型 のRaw 値として利用できます。
独自クラスをRaw 値として使うための条件
独自のクラスをenum型 のRaw 値として使うためには、クラスが次の条件を満たす必要があります。
- インスタンスをリテラルから変換できること。
- Equatable プロトコルに準拠していること。
列挙子のRaw 値として使えるクラスを定義する
それでは、実際に列挙子のRaw 値として使えるクラスの定義を行ってみます。
とても簡単な例ですが、次のようにInt型 の値を保持する変数value を持ったクラス「MyLiteral」を用意して、これを列挙型のRaw 値として使えるように調整します。
class MyLiteral {
var value:Int
}
なんらかのリテラルから変換できるようにする
独自クラスを列挙型のRaw として扱えるようにするために、まずはなんらかのリテラルを独自クラスに変換できる必要があります。
リテラルというのは、たとえば整数値であれば100
といったテキストですし、小数点数であれば10.5
といったテキスト、文字列もリテラルで"TEXT"
といったテキストになります。
今回はこのうちの整数値リテラルからの変換に対応するクラスにしてみることにします。
整数値リテラルから変換できるクラスにするには、IntegerLiteralConvertibleプロトコル に準拠させます。
このプロトコルに準拠するには、引数にIntegerLiteralType型 の値を取るイニシャライザを実装する必要があります。
class MyLiteral : IntegerLiteralConvertible {
var value:Int
required init(integerLiteral value: IntegerLiteralType) {
self.value = value
}
}
今回は単純に、渡された整数値リテラルをインスタンス変数に格納するだけの処理になっていますが、ここで自由に処理をして、インスタンスを初期化するようにします。
たとえば受け取った数値を ID と見立てて、何かデータを揃えるみたいな処理でも良いかもしれません。
ちなみに、リテラルからの変換に対応しなかった場合は、列挙型の宣言のところで次のようなビルドエラーが発生します。
Raw type 'MyLiteral' is not convertible from any literal
Equatableプロトコル に準拠させる
また、列挙型のRaw として使うためには、Equatableプロトコル に準拠させて、等号による一致判定ができるようにする必要があります。
クラスの宣言部分にEquatableプロトコル を追加したら、このプロトコルで要求される実装は==演算子 なので、クラス定義の外側で、プロトコルが求める演算子を実装します。
class MyLiteral : IntegerLiteralConvertible, Equatable {
var value:Int
required init(integerLiteral value: IntegerLiteralType) {
self.value = value
}
}
func ==(lhs: MyLiteral, rhs: MyLiteral) -> Bool {
return lhs.value == rhs.value
}
このようにすることで、独自クラス同士で一致判定ができるようになりました。
なお、このプロトコルを実装しないと、ビルド時に次のエラーが表示されます。
RawRepresentable 'init' cannot be synthesized because raw type 'MyLiteral' is not Equatable
独自クラスを列挙子として使う
このように準備ができたら、作成した独自クラスを列挙型のRaw 値として利用できます。
enum MyEnum : MyLiteral {
case once = 1
case twice = 2
}
列挙型を定義する際の注意点としては、想定されるケースを定義するときに、値をリテラルで与える必要があるところです。
Raw 値として、インスタンスそのものは指定できないところに注意が必要です。たとえば「case once = MyLiteral(1)」のようにインスタンスを作って指定しようとすると、次のようなコンパイルエラーになります。
Raw value for enum case must be a literal
case 文で使う
列挙型のRaw として独自クラスを使っても、列挙型の扱い方は普段と何も変わりません。
いつものように列挙型の変数を定義して、case 文では列挙子を使って場合分けをするだけです。
let times = MyEnum.twice
switch times {
case .once:
println("ONCE")
case .twice:
println("TWICE")
}
独自クラスを使ったときの特徴としては、列挙型の変数からrawValueプロパティ を使ってインスタンスを取り出せるところです。
let raw:MyLiteral = times.rawValue
取り出されたインスタンスはもちろん、今回の場合はMyLiteralクラス
なので、そのクラスが持っているプロパティやメソッドに自由にアクセスできます。
note
MyLiteral.twice.rawValue のように、列挙子から直接インスタンスを取り出すことも可能です。
rawValueプロパティ では値を複製する
このように定義した列挙子を扱う上で注意したいのが、rawValueプロパティ にアクセスした時、列挙子のインスタンスが値として複製されるところです。
このときの複製は、代入演算子による複製ではなく、列挙子に割り当てられたリテラルを使ってインスタンスを構築することによって行われます。
もう少し踏み込んで見てみると、実際には列挙型の値はリテラルで管理されていて、そのrawValueプロパティ を参照したときに初めて、そのリテラル値を使ってインスタンスが生成されることになるようでした。
そのため、rawValueプロパティ を参照すると、参照した回数だけ変換イニシャライザが呼び出されるところに注意が必要です。
rawValueプロパティ を複数回利用する場合は、重たい変換処理を避けたり、あらかじめ別の変数に受けて使用するのが良さそうです。
Raw 値から列挙子を取得する
Raw 値として指定したクラスのインスタンスから、列挙子を取得することも可能です。
たとえば、列挙子MyEnum
のRaw
値として使っているMyLiteral型
のインスタンスが、変数times
に格納されていたとき、次のようにしてそれに該当する列挙子を取得できます。
let value:MyEnum? = MyEnum(rawValue: times)
このようにすることで、インスタンスが列挙子に該当する場合に、その値が変数に格納されます。該当しない場合はnil
が得られます。
また、この方法では、引数の変数rawValue にリテラル値を指定することもできるようです。その場合は、渡したリテラル値から変換イニシャライザでインスタンスを作成した上で、該当するものがあるかが判定されます。
このとき該当するかを調べるために、列挙型が想定する列挙子を順番に、変換イニシャライザを使ってリテラルからインスタンスに変換して、与えられたRaw 値のインスタンスと==演算子 演算子で比較することになるようです。変換イニシャライザが重い処理をしている場合は注意が必要そうです。
独自クラスを列挙型で使う価値について
クラスと列挙子を関連づける必要性は…
もし「列挙子の値によってクラスのインスタンスを作り分ける」みたいな場面であれば、値を渡してその先でインスタンスを作るくらいなら、最初からインスタンスを作ってそれを case 分岐でも使う、みたいな方法が採れるかもしれません。
ただ、クラスであれば型や継承関係を判定する機能がSwift言語 には用意されているので、わざわざインスタンスを列挙子に関連づける必要性もないかもしれません。
複数のパターンを制御するには良いかも?
たとえば選択パターンが A, B, C とあったとします。このとき、各タイプのそれぞれで、背景画像はこれ、パーサーはこれ、通知メッセージはこれ、などといったインスタンスが揃えられていたとします。
そのようなとき、列挙型のRaw 値として使えるクラスを定義して、インスタンスにこれらの情報をまとめておけば、パターンを case 文で判断しつつ、各タイプ用の素材を取得するのが簡単になるかもしれません。
ただしこの場合だと、わざわざ列挙型を使って分岐しなくても、インスタンスひとつで分岐せずに処理が済みそうにも思えます。分岐が必要な場合でも、むしろクラスがpatternプロパティ とかを持って、そこで単純なパターンを返せば済むとも言えそうです。
ただ、そのような方法をとった場合、パターンの情報はインスタンス間で独立しているため、パターンが重複しないようにインスタンス構築時に注意してコーディングをしたりする必要が出てきそうです。
それよりは、列挙型としてまず A, B, C を用意して、独自クラスのイニシャライザで「A ならこれ」「B ならこれ」というように初期化をした方が、パターンを管理しやすい可能性も考えられます。
とはいえ、列挙子の定義と、イニシャライザ内の処理の両方で A, B, C というパターンを二重に管理する手間が発生するため、そこまで賢い方法とは呼べない感は残ります。
生成するインスタンスの範囲を管理するのに便利かも?
もうひとつ考えられる利点は、インスタンスの値が存在する範囲をコンパイラで制限できることもあるかもしれません。
Swift言語
では、switch
文で想定漏れのケースが存在するとコンパイルエラーになるという性質があるので、生成するインスタンスの想定される範囲が限られる場合は、敢えて列挙型で列挙子とインスタンスの対応付けを行っておくと、想定外のインスタンスを生成するのを抑制できるかもしれません。
しかし逆に言うと、想定される場面が増えて列挙子を増やせば、方々で使用しているswitch
が軒並み想定不足でエラーになるため、わざわざこのような制限をクラスにかけておかなくても、適切な修正を漏れなく行えそうなため、この方法はあまり価値はないかもしれません。
初期化を遅らせる効果
上記で触れたように、クラスのインスタンスが生成されるのはrawValueプロパティ にアクセスしたときです。
そのため、どのインスタンスを使うかを決めてから実際にそれを使うまでに時間差がある場合には、ひとまず列挙子で持ち回っておいて、必要なタイミングでrawValueプロパティ からインスタンスを生成するという方法はあり得ます。
ただ、構造体であれば受け渡しの度に値のコピーが発生するため効果はあるかもしれませんが、クラスの場合は参照で受け渡しが行われるため、通常はこのような努力をする必要はなさそうです。
この方法の唯一利点を挙げるとすれば、rawValueプロパティ にアクセスしたときにインスタンスが生成されるため、ある意味、必要時にインスタンスのスナップショットが作れるといった効能はあるかもしれません。
マルチスレッドで値を持ち回る場合は、あるスレッドで編集中に別のスレッドで読んだときに発生するバグの危険がつきまといますが、リテラルであれば編集という状態は発生しないため、値をロックせずに持ち回って、必要な時だけスナップショットを作る(そのスレッドだけで使うインスタンスを変換イニシャライザで作る)ということも行いやすくなるかもしれません。
ただ、このような場面を考慮した場合に、そもそもの列挙型の特徴である「取り得る値の制限」が必要になるかは疑問です。
このような目的のためにrawValueプロパティ
で初期化を遅らせるくらいなら、別のSwift言語
の機能、たとえばlazy
プロパティを使ったり、そもそもインスタンス生成用のメソッドをenum
に直接実装するなどした方が、よっぽど建設的にも思えます。
列挙型自身で持てるメソッドとプロパティとの兼ね合い…
Swift言語 の列挙型は、それ自身にメソッドやプロパティを定義できるようになっています。
プロパティについては、それ専用に値を保持すること(ストアドプロパティ)はできず、自分自身の値から何かを作って返すこと(コンピューテッドプロパティ)しかできませんが、それでも必要に応じてなんらかの値を自由に返すことができます。
今回のrawValueプロパティ も同じことが言えて、このプロパティにアクセスされたときに、自分自身のリテラル値からRaw 値に指定された型のインスタンスを生成する、コンピューテッドプロパティとしての振る舞いをしています。
こう考えると、わざわざ小難しくRaw 値で使えるクラスを作らなくても、単純に列挙型にメソッドやプロパティを実装して、目的の値を返す方がシンプルで良いかもしれません。
note
列挙型に実装したメソッドやプロパティで自分自身の値を取得するには
self
を使います。このときself
の型は、列挙型そのものの型になります。