私の理解では、C# でのジェネリックの差異の指定は、型宣言レベルで行われます。つまり、ジェネリック型を作成するときに、型引数の差異を指定します。一方、Java では、ジェネリックが使用される場所で分散が指定されます。ジェネリック型の変数を作成するときは、その型引数がどのように変化するかを指定します。
各オプションの長所と短所は何ですか?
C# と Java のジェネリックは他の多くの点で異なりますが、これらの違いは主に分散と直交するため、宣言サイトと使用サイトの差異の違いについてだけお答えします。
最初に、私の記憶が正しければ、ユース サイトの分散は宣言サイトの分散よりも厳密に強力です (簡潔さは犠牲になりますが)、または少なくとも Java のワイルドカードは (実際にはユース サイトの分散よりも強力です)。この強化された機能は、C# や Java など、ステートフルな構成要素が頻繁に使用される言語で特に役立ちます (ただし、Scala では、特にその標準リストが不変であるため、それほど有用ではありません)。List<E>
(または)を検討してくださいIList<E>
。E を追加する方法と E を取得する方法の両方を持っているため、E に関して不変であり、宣言サイトの差異は使用できません。List<+Number>
ただし、使用部位分散を使用すると、 の共変部分集合を取得し、 の反変部分集合を取得するList
ように言うことができます。List<-Number>
List
. 宣言サイト言語では、ライブラリの設計者は、サブセットごとに個別のインターフェイス (クラスの複数の継承を許可する場合はクラス) を作成し、List
それらのインターフェイスを拡張する必要があります。ライブラリ設計者がこれを行わない場合 (C#IEnumerable
は の共変部分の小さなサブセットのみを行うことに注意してくださいIList
)、運が悪く、言語で行う必要があるのと同じ面倒なことに頼らなければなりません。分散の。
これが、宣言サイト継承に対する使用サイト継承の利点です。使用サイト継承に対する宣言サイト継承の利点は、基本的にユーザーにとって簡潔であることです (設計者がすべてのクラス/インターフェイスを共変部分と反変部分に分離する努力をした場合)。IEnumerable
またはのようなものについてIterator
は、インターフェイスを使用するたびに共分散を指定する必要がないのは良いことです。Java は、長い構文を使用することでこれを特に厄介なものにしました (Java のソリューションが基本的に理想的な双分散を除く)。
もちろん、これら 2 つの言語機能は共存できます。自然に共変または反変である型パラメーター ( IEnumerable
/などIterator
) については、宣言でそのように宣言します。自然に不変である型パラメーター ( in など(I)List
) については、使用するたびに必要な分散の種類を宣言します。混乱を招くだけなので、宣言サイトの差異を使用して引数の使用サイトの差異を指定しないでください。
私が踏み込んでいない他のより詳細な問題があります (ワイルドカードが使用場所の差異よりも実際にどのように強力であるかなど) が、これがあなたのコンテンツに対するあなたの質問に答えることを願っています. 私は使用場所の差異に偏っていることを認めますが、プログラマーや言語研究者との議論で出てきた両方の主な利点を描写しようとしました.
ほとんどの人は、宣言サイトの分散を好むようです。なぜなら、ライブラリのユーザーにとっては簡単になるからです(ライブラリ開発者にとっては少し難しくなりますが、ライブラリ開発者は、分散がどこにあるかに関係なく、分散について考えなければならないと私は主張します)実際に書かれています。)
ただし、Java も C# も優れた言語設計の例ではないことに注意してください。
Java 5 の互換性のある VM の改善と型消去により、Java は適切な分散を取得し、JVM とは独立して動作しますが、使用場所の分散により使用が少し面倒になり、型消去の特定の実装は当然の批判を集めました。
C#の宣言サイト分散モデルは、ライブラリのユーザーから負担を取り除きますが、具体化されたジェネリックの導入中に、基本的に分散ルールを VM に組み込みました。今日でも、この間違いのために、共変/反変を完全にサポートすることはできません (そして、具体化されたコレクション クラスの後方互換性のない導入により、プログラマーは 2 つの陣営に分かれています)。
これは、CLR を対象とするすべての言語に困難な制限を課し、CLR には「はるかに優れた機能」があるように見えますが、代替プログラミング言語が JVM でより活発である理由の 1 つです。
Scalaを見てみましょう: Scala は、JVM 上で実行される完全なオブジェクト指向の機能ハイブリッドです。それらは Java のように型消去を使用しますが、ジェネリックの実装と (宣言サイト) バリアンスの両方が理解しやすく、Java (または C#) よりも簡単で強力です。VM はバリアンスがどのようにしなければならないかについて規則を課さないからです。仕事。Scala コンパイラはバリアンス表記をチェックし、実行時に例外をスローするのではなく、コンパイル時に不健全なソース コードを拒否できます。また、生成された .class ファイルは Java からシームレスに使用できます。
宣言サイトの差異の欠点の 1 つは、場合によっては型の推論が難しくなるように見えることです。
同時に、Scala は@specialized
、要求されたプリミティブ型に特化したクラスまたはメソッドの 1 つまたは複数の追加の実装を生成するように Scala コンパイラに指示するアノテーションを使用することにより、C# のようにコレクションにボックス化せずにプリミティブ型を使用できます。
Scala は、マニフェストを使用してジェネリックを「ほぼ」具体化することもできます。これにより、C# のように実行時にジェネリック型を取得できます。
Java スタイルのジェネリックの欠点
結果の 1 つは、Java バージョンが参照型 (またはボックス化された値型) でのみ機能し、値型では機能しないことです。多くのシナリオで高パフォーマンスのジェネリックを妨げ、特殊な型を手動で作成する必要があるため、これが最大の欠点である IMO です。
「このリストには x 型のオブジェクトのみが含まれています」のような不変条件は保証されず、getter ごとにランタイム チェックが必要です。ジェネリック型は実際に存在します。
リフレクションを使用する場合、ジェネリック オブジェクトのインスタンスにどのジェネリック パラメータがあるかを問い合わせることはできません。
Java スタイルのジェネリックの利点
分散を取得し、異なるジェネリック パラメーター間でキャストできます。
Java: Java 5 以降のユース サイト分散ジェネリック。1.0 以降の別の構文を持つ壊れた共変配列。ジェネリックのランタイム型情報はありません。
C#: C# 2.0 以降のユース サイト分散ジェネリック。C# 4.0 で宣言サイトの差異を追加しました。1.0 以降、異なる構文で壊れた共変配列 (Java と同じ問題)。「具体化された」ジェネリックは、コンパイル時に型情報が失われないことを意味します。
Scala: 言語の初期バージョン (少なくとも 2008 年以降) からの使用サイト/宣言サイトの両方の差異。配列は個別の言語機能ではないため、同じジェネリック構文と型差異規則を使用します。特定のコレクションは、JVM 配列を使用して VM レベルで実装されるため、Java コードと比較して同等またはそれ以上の実行時パフォーマンスが得られます。
C#/Java 配列型の安全性の問題を詳しく説明するには、Dog[] を Pet[] にキャストし、Cat を追加して、コンパイル時にキャッチされないランタイム エラーをトリガーすることができます。Scala はこれを正しく実装しました。