Hatena::ブログ(Diary)

((プログラミング | 形式) 言語) について書く日記

2010-11-09

JavaとScalaとC#のジェネリクス機能比較表

JavaScalaC#ジェネリクスは、いずれも継承を持ったオブジェクト指向言語においてParametric Polymorphismを実現するための手段であり、それぞれ異なった特性を持っている。というわけで、それぞれの言語においてジェネリクスがどのようにサポートされているかを比較した表を用意してみた。後で気が向いたら、各項目の説明を追加するかも。

Java(5.0以降)ScalaC#(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の順番だと思う。

  • 全ての型のサブタイプ

読んで字のごとく、全ての型のサブタイプであるような型が明示的に書けるか否か。参照型に限定すれば、JavaC#ではnull typeがそれに該当するが、どちらの言語でもnull typeを明示的に書く手段は存在しない。Scalaでは、たとえば次のような形で、Nothingという全ての型のサブタイプである型を明示的に書くことができる。

def error(message: String): Nothing = throw new RuntimeException(message)

これは、後述するdefinition-site varianceと組み合わせたときに真価を発揮する。

  • definition-site variance

JavaでもC#でもScalaでも、デフォルトでは、ジェネリックな型の型パラメータ同士に継承関係があってもサブタイプ関係は成立しない。たとえば、Javaclass 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のスーパータイプでないのでエラー

JavaScalaでは、既存の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#ではバイトコードレベルでジェネリックスに関する情報を持っており、実行時にもその情報を参照できるため、JavaScalaで発生する問題(の多く)は発生しない。

先に書いたように、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絡みの話だが、JavaScalaでは、コンパイル時に型パラメータの情報が消去されるため、次のように異なる型パラメータで具体化した複数のインタフェースを同時に継承することはできない。

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> { }

JavaScalaではこのようなコードはコンパイルを通らない。

class Tuple<T1, T2> { }
class Tuple<T1, T2, T3> { } // エラー。型パラメータが違うだけで、上と同じ名前
class Tuple<T1, T2, T3, T4> { } // 同じくエラー

JavaC#ジェネリクスでは、あるジェネリックなクラスやインタフェースが別の型をパラメータに取る、という事は記述できるが、その型パラメータがさらに別の型を取る、と言ったことは記述できない。たとえば、以下のようなコードをJavaC#で書くことはできない。

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)

*1:全ての型についてin/out指定ができるわけではない。コメント欄参照

*2:注意して欲しいのは、ジェネリックなクラスから型パラメータの情報が消去されるわけではない点。たとえば、List<T>を表すClassオブジェクトからは、一つの型パラメータを取るという情報がちゃんと取得できる

NyaRuRuNyaRuRu 2010/11/09 20:58 C# Generics は .NET Generics のサブセットなので,.NET 界隈の人はあんまり C# Generics を言語特性とは思っていないような印象がありますね.
C# の特徴として挙げられている「型パラメータの上限」や「definition-site variance」にしても,そもそも .NET では処理系が型検査の一環としてこれらをチェックしながら実行するものなので,(.NET の中だけで見ると) "C# (だけ) の特性" とは見られていないように思います.
当然,実行時に生成されたコードや,他の言語で書かれたコードも,処理系による同じ型検査が行われます.
いっそのこと『JavaとScalaと.NETのジェネリクス機能比較表』とした方が,.NET を使っている人には伝わりやすいかもしれません.

> definition-site variance
> C# 4.0では、型を定義するときに、G<out T>のようにすると、(略)

補足ですが,.NET Generics での Variance サポートは,Generic Interface と Generic Delegate に限定されています.
任意の Generic Type で使えるわけではありません.
C# も同じ制限に従います.

> 型パラメータの下限
> Javaはまだしも、C# 4.0でこの機能が無いのはかなり不思議な事ではある。

私の誤解かもしれませんが,以下の話と同じ内容でしょうか?
http://d.hatena.ne.jp/NyaRuRu/20080510/p1


> 同じジェネリッククラスを複数同時に継承する

この話を思い出しました.
http://d.hatena.ne.jp/NyaRuRu/20080913/p1

Visual C# 2010 で以下のコードのコンパイルが通ることと,実行結果が 1 になるのが仕様通りなのかどうかは,最近の仕様をきちんと追っていないのでよく分からないですが.
using System;
using System.Text;

public interface ICountable<out T>
{
 int Count { get; }
}

public class X : ICountable<string>, ICountable<StringBuilder>
{
 int ICountable<string>.Count
 {
  get { return 1; }
 }
 int ICountable<StringBuilder>.Count
 {
  get { return 2; }
 }
}

public static class Program
{
 static void Main(string[] args)
 {
  var x = new X();
  var countable = x as ICountable<object>;
  Console.WriteLine(countable.Count);
 }
}

結果: 1

kmizushimakmizushima 2010/11/09 21:21 ありがとうございます。C#に関してはあまり詳しく無いので、補足してくださるのは助かります。以下、コメントです。

> C# Generics は .NET Generics のサブセットなので,.NET 界隈の人はあんまり C# Generics を言語特性とは思っていないような印象がありますね.
C# の特徴として挙げられている「型パラメータの上限」や「definition-site variance」にしても,そもそも .NET では処理系が型検査の一環としてこれらをチェックしながら実行するものなので,(.NET の中だけで見ると) "C# (だけ) の特性" とは見られていないように思います.

私が勘違いしているかもしれないのですが、C#のGenericsに関するコンパイル時の型チェックはあくまでC#コンパイラが行っているものであって、.NETのランタイムは関わってませんよね?.NETのランタイム自体がジェネリクスをサポートしている、という事自体は知っていたのですが、それとC#コンパイラによる型チェックは重複しているものはあっても、一応別のものだと認識していたので、このような書き方になりました。

> 当然,実行時に生成されたコードや,他の言語で書かれたコードも,処理系による同じ型検査が行われます.
いっそのこと『JavaとScalaと.NETのジェネリクス機能比較表』とした方が,.NET を使っている人には伝わりやすいかもしれません.

たとえば、varianceについて言えば.NET自体がそれをサポートしていても、言語(たとえばC#)がそれに対応していないと使えないわけで、.NETのジェネリクスと言ってしまうのはちょっと違うのかなと思いました。

>> definition-site variance
>> C# 4.0では、型を定義するときに、G<out T>のようにすると、(略)

>補足ですが,.NET Generics での Variance サポートは,Generic Interface と Generic Delegate に限定されています.
任意の Generic Type で使えるわけではありません.
C# も同じ制限に従います.

補足ありがとうございます。言い訳しておくと、それについては、一応、関連する部分の言語仕様を流し読みくらいはしたので知ってはいました。ただ、あえて言わなくても別に良いかなくらいに思っていましたが、誤解を招くかもしれませんね。脚注に追記しておきます。


>> 型パラメータの下限
>> Javaはまだしも、C# 4.0でこの機能が無いのはかなり不思議な事ではある。

>私の誤解かもしれませんが,以下の話と同じ内容でしょうか?
>http://d.hatena.ne.jp/NyaRuRu/20080510/p1

こちらの方は、ざっと読んだ限り、型パラメータから継承したいという話に読めます。今回言及したのはそれとはまた別の話で、whereのところで、ある型パラメータが何かを継承している、という制約だけでなく、ある型パラメータが別の型パラメータのスーパークラスである、という制約も書きたい、ということです。この機能がある無しは、immutableなコレクションライブラリを作りたいとして、その使い勝手に影響するので、無いと不便だなあという話です。


> この話を思い出しました.
http://d.hatena.ne.jp/NyaRuRu/20080913/p1
なるほど。これに関してはあまり深く考えてなかったのですが、言われてみると面倒な問題ですね。

NyaRuRuNyaRuRu 2010/11/09 22:18 > 私が勘違いしているかもしれないのですが、C#のGenericsに関するコンパイル時の型チェックはあくまでC#コンパイラが行っているものであって、.NETのランタイムは関わってませんよね?

そうだと思います.

> たとえば、varianceについて言えば.NET自体がそれをサポートしていても、言語(たとえばC#)がそれに対応していないと使えないわけで、.NETのジェネリクスと言ってしまうのはちょっと違うのかなと思いました。

実際,C# 4 で .NET Generics と C# Generics の相違はすぐに思いつかないぐらいになってしまったので,今ある C# Generics の制限は基本的に .NET Generics 由来,ということは書かれていても良いかなと思った次第です.

>> 型パラメータの下限
> こちらの方は、ざっと読んだ限り、型パラメータから継承したいという話に読めます。今回言及したのはそれとはまた別の話で、whereのところで、ある型パラメータが何かを継承している、という制約だけでなく、ある型パラメータが別の型パラメータのスーパークラスである、という制約も書きたい、ということです。

なるほど了解しました.
であれば「C# 4.0でこの機能が無いのはかなり不思議な事ではある。」の答えは,.NET Generics の制約,ということになるかと思います.

kmizushimakmizushima 2010/11/09 22:30 > 実際,C# 4 で .NET Generics と C# Generics の相違はすぐに思いつかないぐらいになってしまったので,今ある C# Generics の制限は基本的に .NET Generics 由来,ということは書かれていても良いかなと思った次第です.

ふつーに言語処理系を実装する事を考えたときに、下位レイヤーが該当機能をサポートしていない事が上位レイヤーでその機能を実装しない理由になるというのはなんか奇妙に感じるのですが、C#(言語)レイヤーでサポートされている型システムは全て.NET Genericsでサポートされていなければならない、という設計ポリシーで言語仕様が設計されている、という事なのでしょうか?

NyaRuRuNyaRuRu 2010/11/09 23:02 ああ,もちろん設計ポリシーに関しては明言されているのを見たわけではありません.これは今まで見てきた流れからの想像です.
また,「型システム全て」ではなくて「Generics 機構」に関する話です.実際,Extension Method みたいなのもある種の型システムの範疇で,かつ下位レイヤーのサポートを必要としないものだと思いますが,ここでは「型システム全て」について述べているわけではありません.

例えば,C# が以下のような構文で Generics の「型パラメータの下限」をサポートするとします.

public class G<A>{
 public void foo<B>(B b) where A : B {
 }
}

このとき,戦略は大まかに以下の 3 つでしょうか.少なくとも Generics に関しては,(B) も (C) も選びそうにない,というのが私が C# の言語設計に抱いている印象です.

(A) 下位レイヤーを拡張して,他の言語や下位レイヤーからも foo を参照可能にしつつ,実行時検証は下位レイヤーにまかせる.(コンパイル時検証はもちろん独立して行う)
(B) 他の言語や下位レイヤーから foo を参照可能にしつつ,C# コンパイラが独自の実行時検証コードを出力する.(コンパイル時検証はもちろん独立して行う)
(C) 他の言語や下位レイヤーから foo を参照不能にする.外部から参照不能ということを利用して,コンパイル時検証で全てのパスを検証する.(必要なら実行時検証も)

NyaRuRuNyaRuRu 2010/11/09 23:05 ちなみに,(B) のケースでは「他の静的型付け言語がこのメソッドを正しくコンパイル時検証できないことを甘受する」も入ります.

kmizushimakmizushima 2010/11/10 08:27 なるほど。.NET上で動作する以上、他の(ジェネリクスをサポートしている)言語からもC#と同様にジェネリックなクラスやメソッドにアクセスしたりコンパイル時に検証できるべき、という設計ポリシーではないか、という話だと理解しましたが、合っているでしょうか。

ちなみに、EPFLやMS Researchの人たちによる、C# Genericsにおけるvarianceの提案 http://research.microsoft.com/en-us/um/people/akenn/generics/aug2006.pdf では、C# Genericsにおいて「型パラメータの下限」をサポートできるようにしよう、という話がなされていて、実際に.NET上で(B)に近いポリシーで実験的に実装がされたようです。で、この研究と実際のC# 4.0のGenericsがどのような関係にあるのかはよく知らないのですが(スライド最後のページを見ると、実際に製品チームとの関わりはあるように見えますが)、実験的とはいえ、実際に実装できたのに、何故現実のC# 4.0ではサポートされてないのかなーというのがそもそも疑問なのでした。

kmizushimakmizushima 2010/11/10 10:04 > ちなみに,(B) のケースでは「他の静的型付け言語がこのメソッドを正しくコンパイル時検証できないことを甘受する」も入ります.

についてリプライするのを忘れていたので、これについても。そもそも、「型パラメータの下限」+varianceサポートが無いとうまく書けないコードがある以上、同等の機能をサポートしていない他の言語からそれを参照するときに、コンパイル時検証が正しく行かない事があり得る、というのは.NETかどうかに関わらず、必然です。仮に、そのようなことが、NET上で動作する言語の設計ポリシーとして許容されないとしたら、今後、型システム上の重要な発展があったとして、それを取り込む事が阻害されることにすらなりかねないのではないかなあと個人的には思ったりします。

kmizushimakmizushima 2010/11/10 10:12 誤解が無いように追記すると、現行のC#の仕様がこうである、ということにどうしても異議をとなえたい、というわけではないです。たとえばScalaのvarianceサポートと比べて不便なのは間違いないですが、使い物にならないというわけでもないですし。ただ、せっかくvarianceサポートを追加したのにその利点を最大限に引き出すために必要な機能が実装されていないのはなんか勿体無いなあ、とは思いますが。

NyaRuRuNyaRuRu 2010/11/11 04:28 > 実際に実装できたのに、何故現実のC# 4.0ではサポートされてないのかなーというのがそもそも疑問なのでした。

資料の方ありがとうございます.aug2006.pdf をざっと見てみましたが,確かにそういう内容が書いてありますね.
ただこれ,実装できたというのは「C# だけで閉じた場合のコンパイル時検証」+「実行時型検証を行うためのコード生成」というところまでという印象を受けるのですが,実際どうなんでしょうかねぇ.
製品化されなかった理由は聞いてみないと分からないかもしれませんが,製品グループは,単に実装ができるというだけでなく,以下のようなことまで気にする必要があり,そう話は単純でもないように思います.
・その C# 用の追加情報に関するバイナリ表現・規格化・バージョニングをどうするか
・その C# 用の追加情報を,.NET Framework の標準ライブラリでどこまでサポートするか.過去の .NET Framework で動かした場合の挙動をどうするか
・その C# 用の追加情報を,C# 以外の .NET Generics 対応言語でサポートするか

ちなみに,処理系が Generics-aware である .NET では,Reflection API を通じた型に対する操作が実行時にも色々できます.
例えば,以下のような型が定義されているとします.

---------------------------
public class G1<A, B> where A : B /* A についての制約 */ {}
public class G2<A, B> where B : A /* B についての制約 */ {}
public class G1X<A> {
 // 以下の Nested Type H の Arity は 2 であることに注意.
 // C# のシンタックスから受ける印象に反して,実際には A も型引数に含まれている.
 // G1 同様,IL 的には A に対する制約を書くことができるので,ここでの where A : B は許される.
 // ただし C# の構文的にはコンパイルエラーになるので,実際に IL で書く必要がある.
 // (仮に H が Generic Method の場合,C# のシンタックスどおり H の型引数は B のみとなり,
 // A に対する制約である where A : B は書けなくなる.つまり,以前のコメントで書いた
 // G<A>.foo<B>(B b) where A : B が不可能という点に変わりはない.
 // 以下の H は,ネストした Generic Type に関する C# のシンタックス上のトリックと言える)
 public class H<B> where A : B /* A についての制約 */ {}
}
public class G2X<A> {
 public class H<B> where B : A /* B についての制約 */ {}
}
---------------------------

こういう型定義さえ何らかの方法で与えられれば,Generics 非対応の言語から Generics 向けに書かれたライブラリを利用するのは不可能ではありません.
以下ような処理の結果を考えるときに,G1, G2, G1X, G2X がどんな言語で書かれたかは,普通は気にしません.

using System;
using System.IO;

public static class Program
{
 static void TryMakeType(Type target, Type a, Type b) {
  try {
   var t = target.MakeGenericType(a, b);
   Console.WriteLine("{0}[{1}, {2}] OK", target, a, b);
  } catch (Exception e) {
   Console.WriteLine("{0}[{1}, {2}] Failed", target, a, b);
   Console.WriteLine(" " + e.Message);
  }
 }

 static void Main(string[] args) {
  TryMakeType(Type.GetType("G1`2"), typeof(Stream), typeof(MemoryStream));
  TryMakeType(Type.GetType("G1`2"), typeof(MemoryStream), typeof(Stream));
  TryMakeType(Type.GetType("G2`2"), typeof(Stream), typeof(MemoryStream));
  TryMakeType(Type.GetType("G2`2"), typeof(MemoryStream), typeof(Stream));
  TryMakeType(Type.GetType("G1X`1+H`1"), typeof(Stream), typeof(MemoryStream));
  TryMakeType(Type.GetType("G1X`1+H`1"), typeof(MemoryStream), typeof(Stream));
  TryMakeType(Type.GetType("G2X`1+H`1"), typeof(Stream), typeof(MemoryStream));
  TryMakeType(Type.GetType("G2X`1+H`1"), typeof(MemoryStream), typeof(Stream));
 }
}

== 結果 ==
G1`2[A,B][System.IO.Stream, System.IO.MemoryStream] Failed
 GenericArguments[0], 'System.IO.Stream', on 'G1`2[A,B]' violates the constraint of type 'A'.
G1`2[A,B][System.IO.MemoryStream, System.IO.Stream] OK
G2`2[A,B][System.IO.Stream, System.IO.MemoryStream] OK
G2`2[A,B][System.IO.MemoryStream, System.IO.Stream] Failed
 GenericArguments[1], 'System.IO.Stream', on 'G2`2[A,B]' violates the constraint of type 'B'.
G1X`1+H`1[A,B][System.IO.Stream, System.IO.MemoryStream] Failed
 GenericArguments[0], 'System.IO.Stream', on 'G1X`1+H`1[A,B]' violates the constraint of type 'A'.
G1X`1+H`1[A,B][System.IO.MemoryStream, System.IO.Stream] OK
G2X`1+H`1[A,B][System.IO.Stream, System.IO.MemoryStream] OK
G2X`1+H`1[A,B][System.IO.MemoryStream, System.IO.Stream] Failed
 GenericArguments[1], 'System.IO.Stream', on 'G2X`1+H`1[A,B]' violates the constraint of type 'B'.

このように,型の制約は処理系レベルで検証されています.

しかし,例えば C# が,自分自身ではコンパイラやランタイムライブラリに依存した独自の型システムを使いつつ,その型システムの上に構築された型を外部には標準規格の制約のみで公開し始めると,この手の Reflection API を通じた型操作の安心感が一気に薄れる恐れがあるように思います.ここで言う安心感とは,Variance を含めてその型システムが標準規格化され,かつバイナリ表現が何年も維持され続けてきたという安心感です.
仮に C# が,独自の属性などを利用して型パラメータの下限を表現し始めると,isinst opcode や Type.IsAssignableFrom といった下位レイヤーの仕組みが false-positive を起こし始めます.
(Variance を理解しない言語から .NET Generics を見たときのコンパイル時検証が不完全という問題は,単に上位レイヤーで false-negative が起きるというだけで,実行時に Reflection API を通じて下位レイヤーに問い合わせれば正しい情報は得られていました.下位レイヤーが false-positive を返すようになるケースとはだいぶ事情が異なると思います)
http://msdn.microsoft.com/ja-jp/library/system.reflection.emit.opcodes.isinst.aspx
http://msdn.microsoft.com/en-us/library/system.type.isassignablefrom.aspx

もちろん,こういったことが C# の改良の足かせになっているのは「勿体無い」ということには,強く同意するところですが.

通りがかり通りがかり 2010/12/24 00:10 「型パラメータの下限」についてですが、use-site variance の項で出てきた下記のコードがJavaではこれに相当するような気がしますが、別の話でしょうか?

ArrayList<? super Number> nums2 = objs; //contravariantなArrayList

kmizushimakmizushima 2010/12/31 17:56 はい。別の話です。「型パラメータの下限」というのは、Javaで仮に書けたとするなら、

class ImmutableList<+A> {
<B super A> ImmutableList<B> prepend(B element) {
...
}
}

に相当するようなものです。

通りがかり通りがかり 2011/01/16 23:25 > はい。別の話です。

ご回答ありがとうございます。
なるほど、了解しました。

まあ use-site variance の Java に限って言えば、上限に比べて使いどころはだいぶ少なそうですね。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証