Edited at

列挙型でジェネリックを使ってビルドエラーになった場合の回避策

More than 5 years have passed since last update.


目的

値付き列挙型をジェネリックで定義してみたところ、ビルド時にエラーが発生しました。決定的な解決策はまだ見つかっていないのですが、幾つかの回避策を記します。


発生したエラー

値付き列挙型をジェネリックを使って定義したところ、Xcode 6.1 で次のエラーが Issue Navigator に報告されました。


Swift Compiler Error

Command failed due to signal: Segmentation fault: 11


ビルドログを見るともう少し具体的なエラーメッセージが表示されています。


error: unimplemented IR generation feature non-fixed multi-payload enum layout


このエラーメッセージが Issue Navigator に表示されることもあります。


エラーになるコード

次のような列挙型を定義したときに、このビルドエラーが発生しました。


複数の列挙子で値を採り、そのうちのひとつ以上がジェネリック型な場合

複数のケースが値付き列挙子になっていて、そのうちのどれか一つでもジェネリック型になっているときに、今回のエラーが発生しました。

public enum Result<T> {

case Succeeded(T)
case Failed(String)
}


すべての列挙子で同じジェネリック型を採る場合も同様

2つ以上(全部を含む)が値付き列挙子になっている場合、それらが同じジェネリック型であってもエラーになります。

public enum Result<T> {

case OK(T)
case Error(T)
}


ジェネリックな値付き列挙型でエラーにしないためには

次のようなコードであれば、ビルドエラーにはならないようです。


1. 値付き列挙子をひとつだけにする

ひとつのケースだけが値付き列挙子になっている分には問題ないようです。

public enum Result<T> {

case OK
case Retry
case Error(T)
}


2. それぞれの列挙子が採る型を明確にする

ジェネリックとは違ってくるのですが、複数の値付き列挙子が存在していても、それらが明確な型になっている場合は問題ないようです。

public enum Result<T> {

case Succeeded(Int)
case Failed(String)
}

そこまでジェネリックである必要がなければ、既存のクラスや独自クラスを使って型を明示指定することでエラーを回避できそうです。

なお、列挙型のメソッドやプロパティでジェネリック型を使う分には問題ないようです。


3. 列挙子が採る型を AnyObject に制限する(注意事項あり)

取り得るデータ型を少なくとも Objective-C 互換のプロトコルに制限するとビルドエラーが発生しなくなるようです。

そこで、扱う型を AnyObject に限定します。

public enum Result<T:AnyObject> {

case Succeeded(T)
case Failed(String)
}

ただし、このようにすると case 文でジェネリック型を持つ列挙子を判定するコードを記載したときに、次のような理由不明のコンパイルエラーが発生する様子でした。


Command /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc failed with exit code 1


必ずしも自由に値を持てなくなるところにも注意が必要です。

値が AnyObject 型に制限されるため、Int 型 や enum 型 、String 型 などのような純粋な Swift オブジェクトは、原則として持てなくなります。

値として 数値リテラル文字列リテラル を指定することはできますが、実際にはそれぞれ NSNumber 型と NSString 型として保持されることになります。


4. ジェネリックを使わずに Any で対応する

これまでの 2.3. を合わせたような方法ですが、列挙子が採る型を Any と明示することで、ジェネリックを使わずに、純粋な Swift オブジェクトも扱えるようになります。

public enum Result {

case Succeeded(Any)
case Failed(String)
}

この方法なら、複数の列挙子で値を採る場合でも、任意の型の値を持てる列挙子を定義できます。

ただし、この方法を使った場合はプログラミング内では常に Any 型の値として扱われるため、どの型の値が入っているかを判定して処理しないといけないところが とても厄介 です。


5. 列挙子が採る値をクロージャーで指定する

ジェネリックを使った列挙型で複数の列挙子で値を採る場合でも、採る値の型がクロージャーであれば問題ないようです。

ここで @autoclosure を使えば、値を渡すだけで自動的にクロージャーとして包んでくれるので、普段どおりの構文で列挙子に値を設定できます。

public enum Result<T> {

case Succeeded(@autoclosure ()->T)
case Failed(String)
}

ただし、保持する値はあくまでもクロージャーなので、目的の値を取得するときには、列挙子から値を取得して、それをクロージャーとして実行する必要があります。

switch result {

case let .Succeeded(closure):

let value = closure()
}


まとめ

状況によってどれが適切かは違ってくるでしょうけれど、とりあえず感じたところとしては、基本的には『ジェネリックな型を扱う列挙型では、値を採る列挙子はひとつだけ持てる』つもりでいると、列挙型を設計しやすいかもしれません。

複数の列挙子で値を持ちたい場合は、値の型が明確になるように定義するか、またはクラスや構造体でラップしてあげるのが良いかもしれません。

クロージャーを使う方法もそれほど負担はないので、ジェネリックを使って、かつ複数の列挙子で値を扱いたい場合は『クロージャーを使うものだ』と割り切ってしまえば、悩まずに済むようにも思えました。