3/2〜4の3日間、渋谷にて開催されている「try! Swift」のセッションで紹介されたSwiftの「Type Erasure(型消し)」についての理解を深めるため、内容を順を追って記述していきます。
Javaで書けること
以下のようなコードをJavaでは書くことが可能です。
interface BaseInterface<T> {
void hoge(T action);
}
class Foo implements BaseInterface<String> {
@Override
public void hoge(String action) {
System.out.println("action = " + action);
}
}
class Bar implements BaseInterface<Integer> {
@Override
public void hoge(Integer action) {
System.out.println("action = " + action);
}
}
......
void fuga(BaseInterface<?> param) {
// something
}
.......
Foo foo = new Foo();
Bar bar = new Bar();
fuga(foo);
fuga(bar);
Swiftに移植してみよう(失敗例)
では、同様のコードをSwiftでも書いてみましょう。
protocol BaseProtocol {
typealias T
func hoge(action: T)
}
class Foo: BaseProtocol {
func hoge(action: String) {
print("action = \(action)\n")
}
}
class Bar: BaseProtocol {
func hoge(action: Int) {
print("action = \(acton)\n")
}
}
// この関数定義でコンパイルエラー
func fuga(param: BaseProtocol) {
// something
}
let foo = Foo()
let bar = Bar()
fuga(foo) // fuga()の宣言失敗しているのでそもそも呼べない
fuga(bar) // 同上
上記のコードは、コメントに示したところでコンパイルエラーとなります。
これはSwiftの言語仕様の問題です。SwiftのprotocolはJavaのinterfaceと違い、クラス実装時の制約であり型ではないためです。
その制約を一部解消する方法を紹介したセッションが「平常心で型を消し去る」でした。しかし、前述のJavaのようなことは(少なくとも現時点のSwiftでは)できないことも説明されていました。
Type Erasure
ラッパークラスを作成することで「型を消す=(型制約の有用性を維持しつつ、同じprotocolで別の型のオブジェクトを仲間として取り扱う)」というアプローチが説明された。それが以下のようなものである。
class AnyBaz<T>: BaseProtocol {
let _hoge: T -> Void // T型を受け取る関数、戻り値なし(Voidを返す)
// 初期化の引数で渡されたオブジェクトの型をUとして、
// Uの中のT型とこのクラスの宣言で指定したT型が同一
// とする制約をつける
required init<U: BaseProtocol where U.T == T>(_ baz: U) {
_hoge = baz.hoge
}
func hoge(action: T) {
_hoge(action)
}
}
let foo = AnyBaz(Foo()) // fooはAnyBaz<String>型
let bar = AnyBaz(Bar()) // barはAnyBaz<Int>型
解決しない問題
ただし、この”Type Erasure”のテクニックを用いても、Javaでできた以下のようなことはできません。
// この関数定義でコンパイルエラー
func fuga(param: AnyBaz) {
// something
}
fuga(foo) // fuga()の宣言失敗しているのでそもそも呼べない
fuga(bar) // 同上
たとえ以下のような継承関係があるクラスであったとしても、
class X {
// something
}
class Y: X {
// something
}
class Z: X {
// something
}
func fuga(param: AnyBaz<X>) {
// something
}
fuga()の呼び出しに渡せる引数は、AnyBaz<X>のインスタンスでなければならず、AnyBaz<Y>やAnyBaz<Z>のインスタンスは渡せません。このこと自体はJavaも同様で、以下の場合list2はコンパイルエラーを起こします。ジェネリクスの制限としては妥当だと考えられます。
class X {
}
class Y extends X {
}
List<X> list1 = new ArrayList<X>();
List<X> list2 = new ArrayList<Y>();
しかし、”Type Erasure”では解決できないことがあるという事実は変わらないままです。
Type Erasureは正しいアプローチか?
Type Erasureによって問題を解決できるシーンは当然あると思います。
しかし他の言語からの移植時などに、その言語でのやり方すべてをそのまま持ってこようとするのは良いアプローチではなく、そのための方法として利用するものではないとの理解をしました。
簡単に持ってこられるものは持ってくるが、そうじゃないところはアプローチを変えるべきでしょう。おそらくis-a関係(継承など)ではなくhas-a関係(移譲)を用いて検討し直した方がよい場合が多いのではないかと。