Swift プログラミング
基本構文
Swift の列挙型で、値付き列挙型をジェネリックで定義してみたところ、ビルド時にエラーが発生しました。
決定的な解決策はまだ見つかっていないのですが、幾つかの回避策があったので記しておきます。
Swift の列挙型でジェネリックを使ってビルドエラーになった場合の回避策
Swift言語 の列挙型 で、値付き列挙型をジェネリック で定義してみたところ、ビルド時にエラーが発生しました。
決定的な解決策はまだ見つかっていないのですが、幾つかの回避策があったので記しておきます。
今回のお話はXcode 6.1 で発生したものなのですが、これがSwift言語 の仕様なのか不具合なのかはよく分かりません。ただし少なくとも今のところは、列挙型 でジェネリック を使う時には、少し気を遣わないといけない様子です。
発生するエラー
まず、定義した列挙型 は次のような感じです。
Swift Compiler Error
Command failed due to signal: Segmentation fault: 11
コンパイラが強制終了されたというエラーメッセージなんですけど、ビルドログを見るともう少し具体的なエラーメッセージが表示されています。
error: unimplemented IR generation feature non-fixed multi-payload enum layout
エラーメッセージを見る限り、固定されていない複数の値を持つ列挙型で何か実装が不足していることが問題となっているようです。このエラーメッセージに出てくるIR generation
というものが何を意味しているかはわかりません。
エラーが発生するコード
ジェネリックな値を採る列挙子がひとつでもある場合
このエラーは、複数のケースが値付き列挙子になっていて、そのうちのどれか一つでもジェネリック型 になっていると発生するようです。
public enum Result<T> {
case Succeeded(T)
case Failed(String)
}
値付き列挙子がひとつだけの場合
ちなみにジェネリック型 を使っていても、次のようにひとつのケースだけが値付き列挙子になっている分には問題ないようです。
public enum Result<T> {
case OK
case Retry
case Error(T)
}
すべての列挙子で同じジェネリック型を採る場合
2つ以上(全部を含む)が値付き列挙子になっている場合は、それらが同じジェネリック型であってもエラーになります。
public enum Result<T> {
case OK(T)
case Error(T)
}
それぞれの列挙子が採る型が明確な場合
なお、複数の値付き列挙子が存在していても、それらが明確な型になっている場合は問題ありませんでした。
public enum Result<T> {
case Succeeded(Int)
case Failed(String)
列挙型 でジェネリック を使えるようにするには…
複数のケースを値付きにしたい場合は、ジェネリックを使わない方が無難かも
複数のケースを値付きにして、そこでジェネリック型 を使いたい場合は、取り得るデータ型を少なくともObjective-C 互換のプロトコルに制限する必要があるようでした。
Objective-C 互換の代表的なプロトコルといえばAnyObjectプロトコル なので、次のように列挙型 を定義すると、ビルドエラーが発生しなくなります。
public enum Result<T:AnyObject> {
case Succeeded(T)
case Failed(String)
ただし、このようにすると case 文でジェネリック型を持つ列挙子を判定するコードを記載したときに、Xcode 6.1 では次のような理由不明のコンパイルエラーが発生する様子でした。
Command /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc failed with exit code 1
併せて、必ずしも自由に値を持てなくなるところにも注意が必要です。
値がAnyObjectプロトコル に制限されるため、Int型 やenum型 、String型 などのような純粋なSwift オブジェクトは、原則として持てなくなります。
値としてSwift言語 の数値リテラルや文字列リテラルを指定することはできますが、実際にはそれぞれNSNumber型 とNSString型 として保持されることになります。
ジェネリックを使う場合は、値付き列挙子を1つだけにする
いちばん安定感があるのが、列挙型 でジェネリック を使う場合は、値を採る列挙子をひとつだけにする方法です。
public enum Result<T> {
case Succeeded(T)
case Failed
case Processing
}
このようにすることで、任意の型を自由に扱うことができます。
ジェネリックを使わずにAny で対応する
Swift言語 ではAny型 を使用することで、任意の型を扱えます。これを使って、ジェネリックではなくAny型 で値を持つ方法もあります。
public enum Result {
case Succeeded(Any)
case Failed(String)
}
この方法なら、複数の列挙子で値を採る場合でも、任意の型の値を持てる列挙子を定義できます。
ただし、この方法を使った場合はプログラミング内では常にAny型 の値として扱われるため、どの型の値が入っているかを判定して処理しないといけないところがとても厄介です。
目的の型を返すクロージャーを列挙子で持つ
ジェネリックを使った列挙型で複数の列挙子で値を採る場合でも、採る値の型がクロージャーであれば問題ないようです。
public enum Result<T> {
case Succeeded(@autoclosure ()->T)
case Failed(String)
}
このとき@autoclosureディレクティブ を使用すれば、通常の値のように渡してあげるだけで自動的にクロージャーにしてくれるので使い勝手が良くなります。
保持する値はあくまでもクロージャーなので、目的の値を取得するときには、列挙子から値を取得して、それをクロージャーとして実行する必要があります。
switch result {
case let .Succeeded(closure):
let value = closure()
}
考察
状況によってどれが適切かは違ってくるでしょうけれど、とりあえず感じたところとしては、基本的にはジェネリックな型を扱う列挙型では『値を採る列挙子はひとつだけ持てる』つもりでいると、列挙型を設計しやすいかもしれませんね。
複数の列挙子で値を持ちたい場合は、値の型が明確になるように定義するか、またはクラスや構造体でラップしてあげるのが良いかもしれません。
クロージャーを使う方法もそれほど負担はないので、ジェネリックを使って、かつ複数の列挙子で値を扱いたい場合は『クロージャーを使うものだ』と割り切ってしまえば、悩まずに済むようにも思えました。