コンピュータプログラミングにおいて、型バリアンスとは、複合型(例List[Int]:)のサブタイプと、その構成要素のサブタイプ(例:)との間の関係を指します。言語で選択されたバリアンスによって、例えば、のリストとのリスト、あるいはを返す関数とを返す関数Intとの間の関係が決まります。CatAnimalCatAnimal
型Catが のサブタイプである場合Animal、型の式は、型の式が使用される場所であればどこでも置換可能である必要があります。型コンストラクタの変性に応じて、単純型のサブタイプ関係は、それぞれの複合型に対して保持、反転、または無視される場合があります。たとえば、多くのプログラミング言語では、リスト型コンストラクタが共変であるため、「Cat のリスト」は「Animal のリスト」のサブタイプになります。これは、単純型のサブタイプ関係が複合型に対して保持されることを意味します。一方、「Animal から String への関数」は、関数型コンストラクタがパラメータ型に対して反変であるため、「Cat から String への関数」のサブタイプになります。ここでは、単純型のサブタイプ関係が複合型に対して反転されます。CatAnimal
プログラミング言語の設計者は、配列、継承、汎用データ型などの言語機能の型規則を考案する際に、型の不変性を考慮します。型コンストラクタを不変ではなく共変または反変にすることで、より多くのプログラムが型が正しく記述されていると認識されるようになります。一方で、プログラマーは反変性を直感的ではないと感じることが多く、実行時型エラーを回避するために型の不変性を正確に追跡すると、複雑な型規則につながる可能性があります。
型システムをシンプルに保ち、有用なプログラムを可能にするために、言語は、型コンストラクタを可変とみなしても安全であっても不変として扱うこともあれば、型安全性に違反する可能性があるにもかかわらず共変として扱うこともあります。
これらの用語は、圏論における共変関手と反変関手の概念に由来する。圏を考えてみよう。オブジェクトが型であり、射が部分型関係 ≤ を表す。(これは、任意の半順序集合をカテゴリとみなす方法の一例です。)例えば、関数型コンストラクタは 2 つの型pとrを受け取り、新しい型p → rを作成します。つまり、オブジェクトを受け取ります。オブジェクトへ関数型のサブタイピング規則により、この操作は最初のパラメータの ≤ を反転させ、2 番目のパラメータの ≤ を保持するため、最初のパラメータでは反変関数であり、2 番目のパラメータでは共変関数となります。
とAをB型とし、 は型引数 を持つ型コンストラクタI<U>の適用を表すとする。プログラミング言語の型システムにおいて、型コンストラクタの型付け規則は次のようになる。IUI
A ≤ Bの場合、 ;I<A> ≤ I<B>A ≤ B:の場合、 ;I<B> ≤ I<A>A ≤ B(つまり、の場合、 );[注1 ]I<A> ≡ I<B>この記事では、これが一般的な型コンストラクタにどのように適用されるかを考察する。
例えば、C#では、 Catがのサブタイプである場合Animal、次のようになります。
IEnumerable<Cat>は のサブタイプです。は に対して共変であるため、サブタイプ化は保持されます。IEnumerable<Animal>IEnumerable<T>TAction<Animal>は のサブタイプです。は に対して反変であるため、サブタイプ化は逆になります。Action<Cat>Action<T>TIList<Cat>IList<Animal>IList<T>TC# のジェネリック インターフェイスのバリアンスは、その型パラメーター (0 個以上) に (out共変) または(反変) 属性を配置することによって宣言されます。 [ 1 ] : 144上記のインターフェイスは、、、およびとして宣言されます。複数の型パラメーターを持つ型は、各型パラメーターに異なるバリアンスを指定できます。たとえば、デリゲート型は、型の反変入力パラメーターと型の共変戻り値を持つ関数を表します。[ 2 ] [ 1 ] : 145コンパイラは、すべての型が注釈と一貫して定義および使用されているかどうかをチェックし、そうでない場合はコンパイル エラーを通知します。inIEnumerable<outT>Action<inT>IList<T>Func<inT,outTResult>TTResult
インターフェースのバリアンスの型規則は、型の安全性を保証します。たとえば、 は、型の引数を期待する第一級関数を表します。[ 1 ] : 144また、あらゆる種類の動物を処理できる関数は、猫しか処理できない関数の代わりに常に使用できます。Action<T>T
読み取り専用データ型(ソース)は共変である可能性があり、書き込み専用データ型(シンク)は反変である可能性があります。ソースとシンクの両方として機能する可変データ型は不変であるべきです。この一般的な現象を説明するために、配列型を考えてみましょう。型に対して、型を「動物の配列」にすることができます。この例では、この配列は要素の読み取りと書き込みの両方をサポートしています。AnimalAnimal[]
私たちはこれを以下のいずれかの方法で扱うことができます。
Cat[]Animal[]Animal[]Cat[]Animal[]Cat[]Cat[]Animal[]型エラーを回避したい場合、3番目の選択肢のみが安全です。明らかに、すべてのを として扱うことはできません。配列から読み込むクライアントは を期待しますが、 にはたとえば が含まれる可能性があるからです。したがって、反変ルールは安全ではありません。Animal[]Cat[]CatAnimal[]Dog
逆に、を として扱うことはできません。を に格納することは常に可能であるべきです。共変配列の場合、バッキングストアが実際には猫の配列である可能性があるため、これが安全であるとは保証できません。したがって、共変ルールも安全ではありません。配列コンストラクタは不変である必要があります。これは可変配列の場合のみ問題となることに注意してください。共変ルールは不変(読み取り専用)配列では安全です。同様に、反変ルールは書き込み専用配列では安全です。Cat[]Animal[]DogAnimal[]
JavaやC#の初期バージョンには、ジェネリクス(パラメトリック多相性とも呼ばれる)は含まれていませんでした。このような環境では、配列を不変にしてしまうと、有用な多相性プログラムを作成できなくなります。
例えば、配列をシャッフルする関数や、要素に対して . メソッドを使用して 2 つの配列の等価性をテストする関数を作成することを考えてみましょう。Object実装は配列に格納されている要素の正確な型に依存しないため、すべての型の配列で動作する単一の関数を作成することが可能です。次のような型の関数は簡単に実装できます。equals
boolean equalArrays ( Object [] a1 , Object [] a2 ); void shuffleArray ( Object [] a );しかし、配列の型が不変であるとみなされた場合、これらの関数は、まさに 型の配列に対してのみ呼び出すことができる。例えば、文字列の配列をシャッフルすることはできない。Object[]
したがって、JavaとC#はどちらも配列型を共変的に扱います。たとえば、Javaでははのサブタイプであり、C#でははのサブタイプです。String[]Object[]string[]object[]
前述のように、共変配列は配列への書き込みで問題を引き起こします。Java [ 3 ] : 126と C# は、配列オブジェクトの作成時に各オブジェクトに型をマークすることでこの問題に対処します。値が配列に格納されるたびに、実行環境は値の実行時型が配列の実行時型と等しいかどうかを確認します。不一致がある場合は、(Java) [ 3 ] : 126または(C#) がスローされます。ArrayStoreExceptionArrayTypeMismatchException
// a は String の単一要素配列ですString [] a = new String [ 1 ] ;// b は Object の配列ですObject [] b = a ;// b に整数 (int) を代入します。これは、b が実際にObject の配列であれば可能ですが、実際には String の配列であるため、実行時に java.lang.ArrayStoreException が発生します。b [ 0 ] = 1 ;上記の例では、配列(b)からの読み取りは安全に行えます。問題となるのは、配列への書き込みを試みる場合のみです。
このアプローチの欠点の1つは、より厳密な型システムであればコンパイル時に検出できたはずの実行時エラーが発生する可能性があることです。また、配列への書き込みごとに実行時チェックが追加で必要となるため、パフォーマンスが低下します。
ジェネリクスの追加により、Java [ 3 ] : 126–129と C# では、共変性に頼らずにこの種の多相関数を記述する方法が提供されるようになりました。配列比較関数とシャッフル関数には、パラメータ化された型を指定できます。
<T> boolean equalArrays ( T [ ] a1 , T [] a2 ) ; <T> void shuffleArray ( T [ ] a ) ;あるいは、C# メソッドがコレクションに読み取り専用でアクセスするように強制するには、配列を渡す代わりにインターフェースを使用できます。IEnumerable<object>object[]
第一級関数を持つ言語には、「Cat を引数として受け取り、Animal を返す関数」(OCaml構文またはC#構文で記述)のような関数型があります。cat->animalFunc<Cat,Animal>
これらの言語では、ある関数型が別の関数型のサブタイプである場合、つまり、異なる型の関数を期待するコンテキストで、ある型の関数を安全に使用できる場合も指定する必要があります。関数f が g よりも一般的な型の引数を受け取り、g よりも具体的な型を返す場合、関数 f を関数gの代わりに使用しても安全です。たとえば、型、 、 の関数は、が期待される場所であればどこでも使用できます。(これは、通信の堅牢性の原則「受け入れるものには寛容に、生成するものには保守的に」と比較できます。)一般的なルールは次のとおりです。animal->catcat->catanimal->animalcat->animal
推論規則表記を用いると、同じ規則は次のように記述できます。
言い換えれば、→型コンストラクタは、パラメータ(入力)型に対して反変であり、戻り値(出力)型に対して共変である。この規則は、最初にジョン・C・レイノルズによって正式に述べられ[ 4 ]、ルカ・カルデッリの論文でさらに普及した[ 5 ]。
関数を引数として取る関数を扱う場合、このルールは複数回適用できます。たとえば、このルールを2回適用すると、次のことがわかります。もしつまり、そのタイプはの位置において共変である複雑な型の場合、特定の型特殊化が型安全であるか否かを頭の中でたどるのは混乱を招く可能性がありますが、どの位置が共変でどの位置が反変であるかを計算するのは簡単です。位置は、偶数個の矢印が適用される左側にある場合に共変です。
オブジェクト指向プログラミング(OOP)言語では、サブクラスがスーパークラスのメソッドをオーバーライドする場合、コンパイラはオーバーライドするメソッドが正しい型を持っているかどうかをチェックする必要があります。一部の言語では、型がスーパークラスの型と完全に一致する必要がある(不変性)一方で、オーバーライドするメソッドが「より適切な」型を持つことも型安全です。関数型の通常のサブタイピング規則によれば、これはオーバーライドするメソッドがより具体的な型を返し(戻り値の型共変性)、より一般的な引数を受け入れる(引数型の反変性)必要があることを意味します。統一モデリング言語(UML)表記では、可能性は次のようになります(クラスBはスーパークラスであるクラスAを継承するサブクラスです)。
具体的な例として、動物シェルターをモデル化するクラスを作成するとします。は のサブクラスであり、 は ( Java 構文を使用して)基底クラスであると仮定します。CatAnimal

クラスAnimalShelter {Animal getAnimalForAdoption ( ) { // ... } void putAnimal ( Animal ) { //... } }さて、問題は、をサブクラス化する場合、とにどのような型を与えることができるかということです。AnimalSheltergetAnimalForAdoptionputAnimal
共変戻り値型を許容する言語では、派生クラスはメソッドをオーバーライドして、より具体的な型を返すことができます。getAnimalForAdoption

class CatShelter extends AnimalShelter {Cat getAnimalForAdoption () { return new Cat (); } }主流のオブジェクト指向プログラミング言語の中で、Java、C++、C#(バージョン9.0以降[ 6 ])は共変戻り値型をサポートしています。共変戻り値型の追加は、1998年に標準化委員会によって承認されたC++言語の最初の変更の1つでした[ 7 ]。ScalaとD も共変戻り値型をサポートしています。
同様に、オーバーライドするメソッドが基底クラスのメソッドよりも一般的な引数を受け入れることを許可しても、型安全性は保たれます。

class CatShelter extends AnimalShelter { void putAnimal ( Object animal ) { // ... } }実際にこれを許可するオブジェクト指向言語はごく少数です(例えば、mypyで型チェックを行うPythonなど)。C++、Java、およびオーバーロードやシャドウイングをサポートするその他のほとんどの言語では、これはオーバーロードまたはシャドウイングされた名前を持つメソッドとして解釈されます。
しかし、Sather は共変性と反変性の両方をサポートしていました。オーバーライドされたメソッドの呼び出し規約は、出力パラメータと戻り値に関しては共変性であり、通常のパラメータ(モードが の場合)に関しては反変性です。
EiffelやDart [ 8 ]といった主流のプログラミング言語では、オーバーライドするメソッドのパラメータが、スーパークラスのメソッドよりも具体的な型を持つことが許されています(パラメータ型の共変性)。したがって、基底クラスのメソッドをオーバーライドする以下のDartコードは、型チェックに合格します。putAnimal

class CatShelter extends AnimalShelter {void putAnimal ( covariant Cat animal ) { // ... } }これは型安全ではありません。を にアップキャストすることで、犬を猫シェルターに入れようとすることができます。これはパラメータの制約を満たさず、実行時エラーになります。型安全性の欠如(Eiffel コミュニティでは「catcall 問題」として知られており、「cat」または「CAT」は変更された可用性または型です)は長年の問題でした。長年にわたり、グローバル静的解析、ローカル静的解析、および新しい言語機能のさまざまな組み合わせがこれを解決するために提案され、[ 9 ] [ 10 ]これらは一部の Eiffel コンパイラで実装されています。CatShelterAnimalShelterCatShelter
型安全性の問題にもかかわらず、Eiffel の設計者は、現実世界の要件をモデル化するために共変パラメータ型が重要だと考えている。[ 10 ]猫シェルターは一般的な現象を示している。それは一種の動物シェルターだが、追加の制約があり、これをモデル化するために継承と制限付きパラメータ型を使用するのは妥当と思われる。この継承の使用を提案するにあたり、Eiffel の設計者は、サブクラスのオブジェクトは常にスーパークラスのオブジェクトよりも制約が少ないべきであるというLiskov 置換原則を拒否している。
メソッドパラメータの共変性を許容する主流言語のもう1つの例は、PHPのクラスコンストラクタに関するものです。次の例では、__construct() メソッドは、メソッドパラメータが親クラスのメソッドパラメータと共変であるにもかかわらず、受け入れられます。このメソッドが __construct() 以外のものであれば、エラーが発生します。
インターフェースAnimalInterface {}interface DogInterface extends AnimalInterface {}class Dog implements DogInterface {}class Pet { public function __construct ( AnimalInterface $animal ) {} }class PetDog extends Pet { public function __construct ( DogInterface $dog ) { parent :: __construct ( $dog ); } }共変パラメータが役立つもう1つの例は、いわゆるバイナリメソッドです。バイナリメソッドとは、パラメータがメソッド呼び出し対象のオブジェクトと同じ型であることが期待されるメソッドのことです。例えば、`a` メソッドは、`b` が何らかの順序で `a`より前か後かをチェックしますが、例えば2つの有理数を比較する方法は、2つの文字列を比較する方法とは異なります。バイナリメソッドのその他の一般的な例としては、等価性テスト、算術演算、部分集合や和集合などの集合演算が挙げられます。compareToa.compareTo(b)ab
Javaの旧バージョンでは、比較メソッドはインターフェースとして指定されていました。Comparable
インターフェースComparable {int compareTo ( Object o ); }この方法の欠点は、メソッドが型の引数を取るように指定されていることです。一般的な実装では、まずこの引数をダウンキャストします(期待される型でない場合はエラーをスローします)。Object
class RationalNumber implements Comparable { int numerator ; int denominator ; // ... public int compareTo ( Object other ) { RationalNumber otherNum = ( RationalNumber ) other ; return Integer . compare ( numerator * otherNum . denominator , otherNum . numerator * denominator ); } }共変パラメータを持つ言語では、引数に目的の型を直接指定することで、型キャストを隠蔽できます。(もちろん、例えば に対して が呼び出された場合、実行時エラーが発生します。)compareToRationalNumbercompareToString
その他の言語機能を用いることで、リスコフの置換可能性を維持しながら、共変パラメータの明らかな利点を提供することができる。
ジェネリクス(別名パラメトリック多相性)と境界量化を備えた言語では、前述の例を型安全な方法で記述できます。[ 11 ]を定義する代わりに、パラメータ化されたクラスを定義します。(この方法の欠点の1つは、基底クラスの実装者が、サブクラスでどの型を特殊化する必要があるかを予測する必要があることです。)AnimalShelterShelter<T>
class Shelter < T extends Animal > {T getAnimalForAdoption () { // ... }void putAnimal ( T動物) { // ... } }class CatShelter extends Shelter < Cat > {Cat getAnimalForAdoption () { // ... }void putAnimal (猫の動物) { // ... } }同様に、Javaの最近のバージョンではインターフェースがパラメータ化されており、型安全な方法でダウンキャストを省略できるようになっています。Comparable
class RationalNumber implements Comparable < RationalNumber > {int numerator ; int denominator ; // ... public int compareTo ( RationalNumber otherNum ) { return Integer.compare ( numerator * otherNum.denominator , otherNum.numerator * denominator ) ; } }もう一つ役立つ言語機能は、多重ディスパッチです。バイナリメソッドの記述が難しい理由の一つは、 のような呼び出しでは、 の正しい実装を選択するにはと の両方の実行時型に依存するのに対し、従来のオブジェクト指向言語では の実行時型のみを考慮する必要があるからです。共通リスプオブジェクトシステム(CLOS)スタイルの多重ディスパッチを備えた言語では、比較メソッドを、両方の引数をメソッド選択に使用する汎用関数として記述できます。a.compareTo(b)compareToaba
ジュゼッペ・カスタニャ[ 12 ]は、多重ディスパッチを持つ型付き言語では、汎用関数がディスパッチを制御するパラメータと、制御しない「余剰」パラメータを持つことができると指摘した。メソッド選択ルールは適用可能な最も具体的なメソッドを選択するため、メソッドが別のメソッドをオーバーライドする場合、オーバーライドするメソッドは制御パラメータに対してより具体的な型を持つことになる。一方、型安全性を確保するためには、言語は余剰パラメータが少なくとも同じくらい一般的であることを要求する必要がある。前述の用語を用いると、実行時メソッド選択に使用される型は共変であり、メソッドの実行時メソッド選択に使用されない型は反変である。Javaのような従来の単一ディスパッチ言語もこのルールに従う。メソッド選択には1つの引数(隠し引数としてメソッドに渡されるレシーバオブジェクト)のみを使用し、実際、オーバーライドするメソッド内の型はスーパークラス内よりも特殊化されている。thisthis
カスターニャは、共変パラメータ型が優れている例(特にバイナリメソッド)は、共変性を持つ多重ディスパッチを用いて処理すべきだと提唱している。しかし、ほとんどのプログラミング言語は多重ディスパッチをサポートしていない。
以下の表は、上記で説明した言語におけるメソッドのオーバーライドに関する規則をまとめたものです。
ジェネリクス(別名パラメトリック多相性)をサポートするプログラミング言語では、プログラマは新しいコンストラクタで型システムを拡張できます。たとえば、C# のインターフェースでは、やのような新しい型を構築できます。ここで問題となるのは、これらの型コンストラクタのバリアンスをどのように設定すべきかということです。IList<T>IList<Animal>IList<Cat>
主なアプローチは2つあります。宣言箇所でのバリアンス注釈を持つ言語(C#など)では、プログラマーはジェネリック型の定義に、その型パラメータの意図するバリアンスを注釈として付加します。使用箇所でのバリアンス注釈を持つ言語(Javaなど)では、プログラマーはジェネリック型がインスタンス化される箇所に注釈を付加します。
宣言箇所での共変性アノテーションが最も一般的な言語は、C#とKotlin (キーワードoutと を使用in)、およびScalaとOCaml (キーワード+と を使用-) です。C# では共変性アノテーションはインターフェース型にのみ使用できますが、Kotlin、Scala、OCaml ではインターフェース型と具象データ型の両方に使用できます。
C# では、汎用インターフェースの各型パラメーターに、共変 ( out)、反変 ( in)、または不変 (注釈なし) のマークを付けることができます。たとえば、読み取り専用イテレーターのインターフェースを定義し、その型パラメーターに共変 (出力) を宣言することができます。IEnumerator<T>
interface IEnumerator < out T > { T Current { get ; } bool MoveNext (); }この宣言により、IEnumeratorは型パラメータに関して共変として扱われます。たとえば、はのサブタイプです。IEnumerator<Cat>IEnumerator<Animal>
型チェッカーは、インターフェース内の各メソッド宣言がin/outアノテーションと整合性のある方法で型パラメータのみを記述することを強制します。つまり、共変として宣言されたパラメータは、反変位置(反変な型コンストラクタの奇数個の下で出現する場合、その位置は反変である)に出現してはなりません。正確なルール[ 13 ] [ 14 ]は、インターフェース内のすべてのメソッドの戻り値の型が共変的に有効であり、すべてのメソッドパラメータの型が反変的に有効である必要があるということです。ここで、有効な S-lyは次のように定義されます。
T、 がマークされていない場合は共変的に有効でありin、 がマークされていない場合は反変的に有効ですout。A[]AG<A1,A2,...,An>AiG共変であると宣言されています。G反変であると宣言されているか、G不変であると宣言されます。これらのルールがどのように適用されるかの例として、インターフェースを考えてみましょう。IList<T>
interface IList < T > { void Insert ( int index , T item ); IEnumerator < T > GetEnumerator (); }Tのパラメータ型はInsert反変的に有効でなければなりません。つまり、型パラメータにTタグを付けてはなりませんout。同様に、 の結果型は共変的に有効でなければなりません。つまり、(は共変インターフェースであるため)型は共変的に有効でなければなりません。つまり、型パラメータにタグを付けてはなりません。これは、インターフェースに共変または反変のマークを付けることが許可されていないことを示しています。IEnumerator<T>GetEnumeratorIEnumeratorTTinIList
のような一般的なデータ構造の場合IList、これらの制約により、outパラメータは構造からデータを取り出すメソッドにのみ使用でき、inパラメータは構造にデータを入れるメソッドにのみ使用できるため、キーワードが選択されます。
C# では、インターフェースのパラメーターには共変注釈を付けることができますが、クラスのパラメーターには付けることができません。C# クラスのフィールドは常に可変であるため、C# で可変パラメーターを持つクラスはあまり役に立ちません。しかし、不変データを重視する言語では、共変データ型をうまく活用できます。たとえば、Scala、Kotlin、OCamlのすべてにおいて、不変リスト型は共変です。はのサブタイプです。List[Cat]List[Animal]
Scala のバリアンス注釈のチェック規則は、基本的に C# と同じです。ただし、特に不変データ構造に適用される慣用表現がいくつかあります。それらは、次のクラスの定義 (抜粋) で示されています。List[A]
sealed abstract class List [ + A ] extends AbstractSeq [ A ] { def head : A def tail : List [ A ]/** このリストの先頭に要素を追加します。 */ def :: [ B >: A ] ( x : B ): List [ B ] = new scala . collection . immutable . :: ( x , this ) /** ... */ }まず、バリアント型を持つクラスメンバーは不変でなければなりません。ここでは、head型は でありA、これは共変型 ( ) として宣言され+、実際にheadメソッド ( def) として宣言されています。これを可変フィールド ( var) として宣言しようとすると、型エラーとして拒否されます。
第二に、データ構造が不変であっても、パラメータの型が反変的に現れるメソッドが存在することがよくあります。たとえば、リストの先頭に要素を追加するメソッドを考えてみましょう。(この実装では、空でないリストのクラスである、同じ名前のクラス::の新しいオブジェクトを作成します。)このメソッドに渡す最も明白な型は、::
def :: ( x : A ):リスト[ A ]しかし、これは型エラーになります。なぜなら、共変パラメータがA反変位置(関数パラメータとして)に現れるからです。しかし、この問題を回避するトリックがあります。より一般的な型を与え、がのスーパータイプである 限り、::任意の型の要素を追加できるようにします。これはが共変であること に依存していることに注意してください。は 型を持ち、型を持つものとして扱います。一見すると、一般化された型が健全であることは明らかではないかもしれませんが、プログラマがより単純な型宣言から始めると、型エラーによってを一般化する必要がある場所が示されます。BBAListthisList[A]List[B]
コンパイラがすべてのデータ型パラメータに対して可能な限り最良のバリアンス注釈を自動的に推論するような型システムを設計することは可能です。[ 15 ]しかし、いくつかの理由から分析は複雑になる可能性があります。まず、インターフェースのバリアンスは、そのインターフェースが言及するすべてのインターフェースのバリアンスに依存するため、分析は非局所的です。次に、一意の最良のソリューションを得るためには、型システムは二変パラメータ(共変かつ反変である)を許可する必要があります。最後に、型パラメータのバリアンスは、偶然に起こるものではなく、インターフェースの設計者による意図的な選択であるべきです。II
これらの理由から[ 16 ]、ほとんどの言語はバリアンス推論をほとんど行いません。C#とScalaはバリアンス注釈を全く推論しません。OCamlはパラメータ化された具体的なデータ型のバリアンスを推論できますが、プログラマは抽象型(インターフェース)のバリアンスを明示的に指定する必要があります。
例えば、T関数をラップするOCamlデータ型を考えてみましょう。
type ( ' a , ' b ) t = T of ( ' a -> ' b )コンパイラはT、最初のパラメータに対して反変であり、2番目のパラメータに対して共変であることを自動的に推論します。プログラマは明示的な注釈を与えることもでき、コンパイラはそれが満たされているかどうかを確認します。したがって、次の宣言は前の宣言と同等です。
型(- ' a , + ' b ) t = T of ( ' a -> ' b )OCamlでは、インターフェースを指定する際に明示的な注釈が役立ちます。例えば、連想テーブルの標準ライブラリインターフェースには、マップ型コンストラクタが結果型に対して共変であることを示す注釈が含まれています。Map.S
module type S = sig type key type (+ ' a ) t val empty : ' a t val mem : key -> ' a t -> bool ... endこれにより、egが のサブタイプであることが保証されます。catIntMap.tanimalIntMap.t
宣言サイト方式の欠点の1つは、多くのインターフェース型を不変にする必要があることです。たとえば、上で見たように、はと のIList両方を含んでいるため、不変にする必要があります。より多くのバリエーションを公開するために、API設計者は、利用可能なメソッドのサブセットを提供する追加のインターフェース(たとえば、 のみを提供する「挿入専用リスト」)を提供できます。しかし、これはすぐに扱いにくくなります。InsertGetEnumeratorInsert
使用箇所におけるバリアンスとは、型が使用されるコード内の特定の箇所に注釈を付けることで、必要なバリアンスを指定することを意味します。これにより、クラスの設計者が異なるバリアンスを持つ複数のインターフェースを定義する必要なく、クラスの利用者はサブタイピングを行う機会が増えます。代わりに、ジェネリック型が実際のパラメータ化された型にインスタンス化される時点で、プログラマはメソッドのサブセットのみを使用することを指定できます。事実上、ジェネリッククラスの各定義によって、そのクラスの共変部分と反変部分のインターフェースも利用可能になります。
Java は、制限付き存在型の形式であるワイルドカードを通して、使用箇所のバリアンス注釈を提供します。パラメータ化された型は、ワイルドカードと上限または下限を組み合わせてインスタンス化できます。たとえば、またはです。のような無制限のワイルドカードは、と同等です。このような型は、境界を満たす未知の型を表します。 [ 3 ] : 139たとえば、が型である場合、型チェッカーはを受け入れます。?List<?extendsAnimal>List<?superAnimal>List<?>List<?extendsObject>List<X>XlList<?extendsAnimal>
動物a = l.get ( 3 ) ;なぜなら、その型はのサブタイプであることがわかっているが、XAnimal
l.add ( new Animal ( ) );は必ずしも ではないため、型エラーとして拒否されます。一般に、あるインターフェース が与えられた場合、への参照は、メソッドの型に が反変的に出現するインターフェースのメソッドの使用を禁止します。逆に、 の型が であれば、 を呼び出すことはできますが、 を呼び出すことはできません。AnimalXI<T>I<?extendsT>TlList<?superAnimal>l.addl.get

Java の非ワイルドカード型パラメータ化型は不変ですが (たとえば、との間にサブタイピング関係はありません)、ワイルドカード型はより厳密な境界を指定することでより具体的にすることができます。たとえば、は のサブタイプです。これは、ワイルドカード型が上限に関して共変であり(下限に関しても反変である) ことを示しています。つまり、 のようなワイルドカード型が与えられた場合、サブタイプを形成する方法は 3 つあります。クラス を特殊化する、より厳密な境界 を指定する、またはワイルドカードを特定の型に置き換える(図を参照)。 [ 3 ] : 139List<Cat>List<Animal>List<?extendsCat>List<?extendsAnimal>C<?extendsT>CT?
上記3つのサブタイピング形式のうち2つを適用することで、例えば、型を期待するメソッドに型の引数を渡すことが可能になります。これは、共変インターフェース型によってもたらされる表現力の一例です。型は、の共変メソッドのみを含むインターフェース型として機能しますが、の実装者はそれを事前に定義する必要はありませんでした。List<Cat>List<?extendsAnimal>List<?extendsAnimal>List<T>List<T>
一般的なデータ構造の場合IList、構造からデータを取り出すメソッドには共変パラメータが、構造にデータを書き込むメソッドには反変パラメータが使用されます。Joshua Bloch著『 Effective Java 』にある「Producer Extends, Consumer Super (PECS)」という覚え方は、共変性と反変性をいつ使うべきかを覚える簡単な方法です。[ 3 ] : 141
ワイルドカードは柔軟性がありますが、欠点もあります。使用部位の多様性により、API 設計者はインターフェースに対する型パラメータの多様性を考慮する必要はありませんが、代わりに複雑なメソッド シグネチャを使用する必要が生じることがよくあります。一般的な例として、インターフェースが挙げられますComparable。[ 3 ] : 66コレクション内の最大の要素を見つける関数を作成したいとします。要素はメソッドを実装する必要があります。[ 3 ] : 66したがって、最初の試みは次のようになります。compareTo
< T extends Comparable < T >> T max ( Collection < T > coll );しかし、この型は汎用性が十分ではありません。の最大値は見つけることができますが、 の最大値は見つけられません。問題は、が を実装していないことです。代わりに、(より優れた) インターフェース を実装しています。Java では、C# とは異なり、 はのサブタイプとはみなされません。代わりに、 の型を変更する必要があります。Collection<Calendar>Collection<GregorianCalendar>GregorianCalendarComparable<GregorianCalendar>Comparable<Calendar>Comparable<Calendar>Comparable<GregorianCalendar>max
< T extends Comparable <? super T >> T max ( Collection < T > coll );境界付きワイルドカードは、インターフェースから反変メソッドのみを呼び出すという情報を伝えます。この特定の例は、内のすべてのメソッドが反変であるため、その条件が自明に真となるため、煩雑です。宣言サイトシステムであれば、の定義のみに注釈を付けることで、この例をより簡潔に処理できます。?superTmaxComparableComparableComparable
メソッドパラメータに上限付きワイルドカードを使用することで、メソッドをさらに変更できます。[ 17 ]max
< T extends Comparable <? super T >> T max ( Collection <? extends T > coll );使用箇所における差異注釈は、柔軟性を高め、より多くのプログラムで型チェックを可能にする。しかしながら、言語の複雑さを増大させ、複雑な型シグネチャやエラーメッセージにつながるという批判もある。
追加の柔軟性が有用かどうかを評価する一つの方法は、既存のプログラムで使用されているかどうかを確認することです。多数のJavaライブラリ[ 15 ]の調査では、ワイルドカード注釈の39%が宣言サイト注釈で直接置き換えることができたことがわかりました。したがって、残りの61%は、Javaが使用サイトシステムを利用することで恩恵を受ける場所を示しています。
宣言サイト言語では、ライブラリは、より少ないバリアンスを公開するか、より多くのインターフェースを定義する必要があります。たとえば、Scala Collections ライブラリは、共変性を使用するクラスに対して 3 つの別々のインターフェースを定義しています。共通メソッドを含む共変ベースインターフェース、副作用のあるメソッドを追加する不変可変バージョン、および構造的共有を利用するために継承された実装を特殊化できる共変不変バージョンです。[ 18 ]この設計は宣言サイト注釈とうまく機能しますが、インターフェースの数が多いため、ライブラリのクライアントにとって複雑さのコストがかかります。また、ライブラリのインターフェースを変更することは選択肢にならない場合があります。特に、Java にジェネリクスを追加したときの目標の 1 つは、バイナリの後方互換性を維持することでした。
一方、Java のワイルドカード自体が複雑です。会議での発表[ 19 ]で、 Joshua Bloch は、理解と使用が難しすぎると批判し、クロージャのサポートを追加する際には「もうワイルドカードを追加する余裕はない」と述べています。Scala の初期バージョンでは使用箇所のバリアンス注釈が使用されていましたが、プログラマーは実際に使用するのが難しいと感じていました。一方、宣言箇所の注釈はクラスの設計時に非常に役立つことがわかりました。[ 20 ] Scala の後のバージョンでは、Java スタイルの存在型とワイルドカードが追加されましたが、Martin Oderskyによると、Java との相互運用性が必要なければ、これらは含まれなかっただろうとのことです。[ 21 ]
Ross Tate は[ 22 ]、Java のワイルドカードの複雑さの一部は、存在型の一種を使用して使用箇所の差異をエンコードするという決定によるものだと主張している。元の提案[ 23 ] [ 24 ]では、差異注釈に専用の構文を使用し、Java のより冗長な の代わりに と記述していた。List<+Animal>List<?extendsAnimal>
ワイルドカードは存在型の一種であるため、可変性以外にも様々な用途に使用できます。例えば、("不明な型のリスト" [ 25 ] ) のような型を使用すると、型パラメータを正確に指定することなく、オブジェクトをメソッドに渡したり、フィールドに格納したりできます。これは、ほとんどのメソッドで型パラメータが言及されていないクラスなどで特に役立ちます。List<?>Class
しかし、存在型の型推論は難しい問題です。コンパイラ実装者にとって、Javaのワイルドカードは型チェッカーの終了、型引数の推論、および曖昧なプログラムといった問題を引き起こします。[ 26 ]一般に、ジェネリクスを使用するJavaプログラムが型付けされているかどうかは決定不能であるため、 [ 27 ]型チェッカーは一部のプログラムで無限ループに陥るかタイムアウトする必要があります。プログラマにとっては、複雑な型エラーメッセージにつながります。Javaはワイルドカードを新しい型変数に置き換えることでワイルドカード型の型チェックを行います(いわゆるキャプチャ変換)。これにより、エラーメッセージがプログラマが直接記述していない型変数を参照するため、エラーメッセージが読みにくくなることがあります。たとえば、をに追加しようとすると、次のようなエラーが発生します。CatList<?extendsAnimal>
メソッド List.add (capture#1) は適用できません (実際の引数Catは、メソッド呼び出し変換によってcapture#1に変換できません) ここで、capture#1 は新しい型変数です。 capture#1 は、キャプチャから Animal を拡張します。? は Animal を拡張します。
宣言サイト注釈と使用サイト注釈の両方が有用であるため、一部の型システムでは両方を提供しています。[ 15 ] [ 22 ]
I<T> = int: には任意の型を入れることができますTが、結果は依然として ですint。{{cite web}}: CS1メンテナンス: 場所 (リンク)