JavaとScalaとC#のジェネリクスは、いずれも継承を持ったオブジェクト指向言語においてParametric Polymorphismを実現するための手段であり、それぞれ異なった特性を持っている。というわけで、それぞれの言語においてジェネリクスがどのようにサポートされているかを比較した表を用意してみた。後で気が向いたら、各項目の説明を追加するかも。
Java(5.0以降) | Scala | C#(4.0) | |
---|---|---|---|
ジェネリックなクラス | ○ | ○ | ○ |
ジェネリックなメソッド | ○ | ○ | ○ |
型パラメータの上限 | ○ | ○ | ○ |
型パラメータの下限 | × | ○ | × |
型パラメータの推論 | ○ | ○ | ○ |
全ての型のサブタイプ(ScalaにおけるNothing) | × | ○ | × |
definition-site variance | × | ○ | ○ |
use-site variance | ○(Wildcard) | ○(Existential Type) | × |
実行時における型パラメータの扱い | 消去(Erasure) | 消去(Erasure) | 取り出せる |
型パラメータをnewする | × | △(ClassManifestで擬似的に実現可能) | ○ |
同じジェネリッククラスを複数同時に継承する | × | × | ○ |
同じ名前で型パラメータの個数が異なるジェネリッククラスを複数作作成する | × | × | ○ |
高階のジェネリクス(Type Constructor Polymorphism) | × | ○ | × |
ジェネリックな型エイリアス | × | ○ | × |
- ジェネリックなクラス
これをサポートしているのは前提なので、当たり前の事ながらどの言語でも使える。
Javaの場合、
class MyList<T> { }
C#の場合、
class MyList<T> { }
Scalaの場合、
class MyList[T] { }
と、細かい構文の違いはあるが大体似たり寄ったり。
- ジェネリックなメソッド
これも同様にサポートしていて当たり前の機能。
Javaの場合、
<T> T id(T arg) { return arg; }
C#の場合、
T id<T>(T arg) { return arg; }
Scalaの場合、
def id[T](arg: T): T = arg
のような感じになる。Javaの場合、型パラメータの宣言が出てくる場所がちょっとキモくて嫌な感じ。
- 型パラメータの上限
型パラメータがある特定の型を継承していなければならない(サブタイプでなければならない)、という制約をユーザが指定する機能。C++のテンプレートのように、実際に展開してみて型が合わなければエラーという種類の言語であれば良いのだけど、展開する前に型チェックをやる仕組みのOOP言語ではこの機能が無いと、特定の型で定義されるメソッドを型パラメータに対して呼び出せないので、つらい。
Javaだと、
<T extends Comparable<T>> T max(T a, T b) { return a.compareTo(b) < 0 ? b : a; }
C#だと、
T max<T>(T a, T b) where T:IComparable<T> { return a.CompareTo(b) < 0 ? b : a; }
Scalaだと、
def max[T <: Ordered[T]](a: T, b: T) = if(a.compare(b) < 0) b else a
となる。
- 型パラメータの下限
型パラメータがある型のスーパータイプで無ければならない、という制約をユーザが書ける機能。対称性を考えると、この機能も導入されていてしかるべきだと思うのだが、不思議なことにJavaにもC#にもこの機能は存在しない。Javaはまだしも、C# 4.0でこの機能が無いのはかなり不思議な事ではある。後述するdefinition-site varianceと組み合わせるときにこの機能が無いと不便。
Scalaでは、たとえば
class G[A] { def foo[B >: A](b: B) = b }
のようにすると、型BはAのスーパータイプでなければならない、という制約を記述することができる。
- 型パラメータの推論
ジェネリックなメソッドを呼び出すときに、どの型パラメータで呼び出すかを明示しなくても、処理系が引数などから型パラメータを推論する機能。たとえば、
Javaでは、
<T> T id(T arg) { return arg; }
のようにして定義したメソッドを、Tにどの型を渡すかを記述することなしに、単にid("foo")
などのように記述できる。この場合、引数から処理系はT=Stringであることを推論する。
どこまで型パラメータを推論してくれるか、は言語によって差があるが、強さ順に並べると、大体、Scala > C# > Javaの順番だと思う。
- 全ての型のサブタイプ
読んで字のごとく、全ての型のサブタイプであるような型が明示的に書けるか否か。参照型に限定すれば、JavaやC#ではnull typeがそれに該当するが、どちらの言語でもnull typeを明示的に書く手段は存在しない。Scalaでは、たとえば次のような形で、Nothingという全ての型のサブタイプである型を明示的に書くことができる。
def error(message: String): Nothing = throw new RuntimeException(message)
これは、後述するdefinition-site varianceと組み合わせたときに真価を発揮する。
- definition-site variance
JavaでもC#でもScalaでも、デフォルトでは、ジェネリックな型の型パラメータ同士に継承関係があってもサブタイプ関係は成立しない。たとえば、Javaでclass G<T>
があったとして、G<String>
はG<Object>
のサブタイプではないし、逆も成立しない。G<A>
とG<B>>
に代入互換性があるのは、AとBが同じ型のときだけである。
これは、そのような関係を許すと型安全性をぶち壊すからなのだが、この制限がうっとおしいときもある。この制限を取っ払うための機能がdefinition-site varianceである。
C# 4.0では、型を定義するときに、G<out T>のようにすると、
interface G<out T> { } ... G<string> g1 = ...; G<object> g2 = g1; // OK
のように、G<A>がG<B>のサブタイプのとき、G<B>型にG<A>型の値を代入できるようになる(covariant)。
一方、G<in T>のようにすると、
interface G<in T> { } ... G<object> g1 = ...; G<string> g2 = g1; // OK
のように、G<B>がG<A>のサブタイプのとき、G<A>型にG<B<型の値を代入できるようになる(contravariant)*1。
Scalaのそれも基本的にC#のものと同様で(ただし、C#にある制限は存在しない)、G<out T>に相当するのがG[+T]で、G<in T>に相当するのが、G[-T]になる。もちろん、このような注釈を付加したときに型安全性が保たれるかどうかはちゃんと型チェッカがチェックしてくれて、型安全性が保てないような場合にin/outや+/-を入れるとコンパイルエラーになる。
- use-site variance
definition-site varianceはそれはそれで便利なのだが、型の定義時にcovariantかcontravariantを指定しなければいけないのが不便なケースがある。たとえば、mutableなコレクションクラスである定義時にはjava.util.ArrayListなどは、クラスの定義時にはcovariantでもcontravariantにもできない(詳細は割愛)。しかし、そのようなクラスであっても一時的にcovariant/contravariantなものとして扱いたい、という場合がある。これを実現するのがuse-site varianceであり、読んで字のごとく、型を利用する側に注釈を付加するものである。
Javaでは、たとえば次のようにすると、(co/contra)variantなArrayListを実現できる。
ArrayList<Integer> ints = new ArrayList<Integer>(); ints.add(1); ArrayList<? extends Number> nums = ints; //covariantなArrayList ArrayList<Double> doubles = new ArrayList<Double>(); doubles.add(1.5); nums = doubles; //OK ArrayList<Object> objs = new ArrayList<Object>(); ArrayList<? super Number> nums2 = objs; //contravariantなArrayList nums2 = ints; //IntegerはNumberのスーパータイプではないのでエラー
Scalaではこれと同等のコードを次のようにして記述できる。
val ints = new ArrayList[java.lang.Integer] ints.add(1) var nums: ArrayList[_ <: Number] = ints; //covariantなArrayList val doubles = new ArrayList[java.lang.Double] nums = doubles //OK objs = new ArrayList[Any] val nums2: ArrayList[_ >: Number] = objs //contravariantなArrayList nums2 = ints //IntegerはNumberのスーパータイプでないのでエラー
- 実行時における型パラメータの扱い
JavaやScalaでは、既存のJVM(ジェネリクスに関する事を知らない)に変更を加えないでGenericsを扱うために、コンパイル時に、適用された型パラメータの情報を消去してしまうErasureという手法を取っている。たとえば、Javaで
List<String> strs = new ArrayList<String>(); strs.add("Foo"); String foo = strs.get(0); System.out.println(foo);
と書いた場合、<String>>の部分の情報はコンパイル時に削除されてしまい、単に
List strs = new ArrayList(); strs.add("Foo"); String foo = (String)strs.get(0); //キャストが挿入される
と書いたのと同じ意味になってしまう。この点は、必ずしも問題になるわけではないが、リフレクションなどの、実行時に型情報を得るAPIを使う場合、型パラメータの情報を得ることができないため、困ったことになることがある*2。また、この(Erasureの)せいで、Javaのジェネリクスにはいくつかの制限がある。
一方、C#ではバイトコードレベルでジェネリックスに関する情報を持っており、実行時にもその情報を参照できるため、JavaやScalaで発生する問題(の多く)は発生しない。
- 型パラメータをnewする
先に書いたように、Javaのジェネリクスでは、型パラメータに関する情報がコンパイル時に消去されてしまう。そのため、ある型パラメータTについて、その型Tのインスタンスをnewする次のようなコードは、実行時にどの型をインスタンス化すればいいかわからず、コンパイルを通らない。
<T> T newInstance() { return new T(); } // エラー
Scalaでは、implicit parameterとClassManifestという機能を使うことで、制限はあるものの似たようなことができる:
def newInstance[T:ClassManifest] = implicitly[ClassManifest[T]].erasure.newInstance().asInsta
nceOf[T]
一方、C#では、型パラメータTが0引数のコンストラクタを持っているという制約を付けることで、同様のコードをコンパイルすることができる。
T NewInstance<T>() where T:new() // OK { return new T(); }
- 同じジェネリッククラスを複数同時に継承する
引き続いて、Erasure絡みの話だが、JavaやScalaでは、コンパイル時に型パラメータの情報が消去されるため、次のように異なる型パラメータで具体化した複数のインタフェースを同時に継承することはできない。
interface G<T> { } class H implements G<String>, G<Integer> {} //エラー
一方、C#にはそのような制限は無い。
interface G<T> { } class H : G<string>, G<int> { } // OK
- 同じ名前で型パラメータの個数が異なるジェネリッククラスを複数作作成する
C#では、同じ名前のクラスでも型パラメータの個数が違うクラスは別のクラスとして扱われるので(完全に別なのかは実は詳しく知らないので、誰か教えてください><)、次のように、型パラメータの個数が違うが同じ役割を持ったクラスを同じ名前で作成することができる。
class Tuple<T1, T2> { } class Tuple<T1, T2, T3> { } class Tuple<T1, T2, T3, T4> { }
JavaやScalaではこのようなコードはコンパイルを通らない。
class Tuple<T1, T2> { } class Tuple<T1, T2, T3> { } // エラー。型パラメータが違うだけで、上と同じ名前 class Tuple<T1, T2, T3, T4> { } // 同じくエラー
- 高階のジェネリクス(Type Constructor Polymorphism)
JavaやC#のジェネリクスでは、あるジェネリックなクラスやインタフェースが別の型をパラメータに取る、という事は記述できるが、その型パラメータがさらに別の型を取る、と言ったことは記述できない。たとえば、以下のようなコードをJavaやC#で書くことはできない。
class AbstractCollection<Collection<X>, T> { //filterだけ実装すれば、rejectの型も勝手にconcreteなコレクション型になるようにしたい…が、そもそも型パラメータが型パラメータを取るような事を書けない Collection<T> filter(Predicate<T> p) { ... } Collection<T> reject(Predicate<T> p) { return filter(x => !p(x)); } } class ConcreteCollection<T> : AbstractCollection<ConcreteCollection, T> { ConcreteCollection<T> filter(Predicate<T> p) { ... //filterだけ実装すればOK } }
一方、Scalaでは次のようにして、ある型パラメータがさらに別の型パラメータを取る、つまり型パラメータそのものがジェネリックなクラスであるような場合を記述できる。
class AbstractCollection[Collection[X], T]{ //filterだけ実装すれば、rejectの型も勝手にconcreteなコレクション型になるようにしたい def filter(p: T => Boolean): Collection[T] = { ... } def reject(p: T => Boolean): Collection[T] = filter(x => !p(x)) } class ConcreteCollection[T] extends AbstractCollection[ConcreteCollection, T] { def filter(p: T => Boolean): ConcreteCollection[T] = { ... //filterだけ実装すればOK } }
このような機能を、Type Constructor Polymorphismと(Scalaでは)呼んでいる(型理論的にこの機能を指す、広く使われている用語は知らないので、誰か教えてください><)。Scala 2.8のコレクションライブラリでは、この機能無しにScala 2.8のコレクションライブラリは成り立たないといってもいいくらいにこの機能が多用されている。
- ジェネリックな型エイリアス
Scalaではtype宣言によって型エイリアスを作成することが出来る(エイリアス自体を他のコンパイル単位から再利用することもできる)が、それに加えて、型パラメータを取るようなエイリアスを作成することもできる。たとえば、次のようにして、キーがStringであるようなMapのエイリアスStringMapを作成および利用することができる。
type StringMap[V] = Map[String, V] val m: StringMap[Int] = Map("x" -> 50, "y" -> 100)