私は主に Java を使用しており、ジェネリックは比較的新しいものです。Javaが間違った決定をした、または.NETの方が実装が優れているなどを読み続けています。
ジェネリックにおける C++、C#、Java の主な違いは何ですか? それぞれの長所/短所は?
ノイズに私の声を加えて、物事を明確にすることに挑戦します。
List<Person> foo = new List<Person>();
コンパイラは、リストにないものを入れないようPerson
にします。
舞台裏では、C# コンパイラはList<Person>
.NET dll ファイルに入れているだけですが、実行時に JIT コンパイラが移動して新しいコード セットを構築します。これは、人を含めるためだけに特別なリスト クラスを記述したかのように、ListOfPerson
.
これの利点は、非常に高速になることです。キャストやその他のものはありません。dll にはこれが List であるという情報が含まれているため、Person
後でリフレクションを使用してそれを参照する他のコードは、Person
オブジェクトが含まれていることを知ることができます (したがって、インテリセンスなどを取得できます)。
これの欠点は、古い C# 1.0 および 1.1 コード (ジェネリックが追加される前) がこれらの newList<something>
を理解しないことです。そのため、それらと相互運用するには手動で単純な古いものに戻す必要がありList
ます。C# 2.0 のバイナリ コードには後方互換性がないため、これはそれほど大きな問題ではありません。これが発生するのは、古い C# 1.0/1.1 コードを C# 2.0 にアップグレードする場合のみです。
ArrayList<Person> foo = new ArrayList<Person>();
表面上は同じように見えます。コンパイラは、リストにないものを入れることも防ぎますPerson
。
違いは、舞台裏で何が起こるかです。C# とは異なり、Java は特別なものを構築するのではなく、常に Java にあっListOfPerson
た単純な古いものを使用するだけです。ArrayList
配列から物を取り出したら、通常のPerson p = (Person)foo.get(1);
キャスティング ダンスを行う必要があります。コンパイラはキーを押す手間を省きますが、スピード ヒット/キャストは以前と同じように発生します。
人々が「Type Erasure」に言及するとき、これは彼らが話していることです。Person
コンパイラはキャストを挿入し、それが単なるリストではないという事実を「消去」します。Object
このアプローチの利点は、ジェネリックを理解していない古いコードを気にする必要がないことです。これまでと同じ古いものを扱ってArrayList
います。これは Java の世界ではより重要です。なぜなら、Java 5 をジェネリックで使用してコードをコンパイルし、古い 1.4 または以前の JVM で実行することをサポートしたかったからです。Microsoft は意図的に気にしないことにしました。
欠点は、前に述べた速度の低下です。またListOfPerson
、.class ファイルに入る疑似クラスなどがないため、後でそれを参照するコード (リフレクションを使用するか、別のコレクションからプルした場合)変換された場所など) は、他の配列リストだけではなく、それだけObject
を含むリストであることを意図していることを決して伝えることはできません。Person
std::list<Person>* foo = new std::list<Person>();
これは C# や Java のジェネリックに似ており、実行すべきだと思われることを実行しますが、舞台裏ではさまざまなことが起こっています。
pseudo-classes
Java のように型情報を捨てるのではなく、特別にビルドするという点で C# ジェネリックと最も共通点がありますが、まったく別の魚のやかんです。
C# と Java の両方で、仮想マシン用に設計された出力が生成されます。クラスを含むコードを記述Person
した場合、どちらの場合も、クラスに関する一部の情報Person
が .dll または .class ファイルに入り、JVM/CLR がこれを処理します。
C++ は生の x86 バイナリ コードを生成します。すべてがオブジェクトではなくPerson
、クラスについて知る必要がある基礎となる仮想マシンはありません。ボックス化やボックス化解除はなく、関数はクラスなどに属している必要はありません。
このため、C++ コンパイラは、テンプレートを使用してできることを制限しません。基本的に、手動で記述できるコードはすべて、テンプレートを取得して記述できます。
最も明白な例は、次のようなものを追加することです:
C# と Java では、ジェネリック システムはクラスで使用できるメソッドを認識し、これを仮想マシンに渡す必要があります。これを伝える唯一の方法は、実際のクラスをハードコーディングするか、インターフェイスを使用することです。例えば:
string addNames<T>( T first, T second ) { return first.Name() + second.Name(); }
T
このコードは、型が実際に Name() というメソッドを提供していることを認識していないため、C# または Java ではコンパイルされません。あなたはそれを言わなければなりません-C#では次のようになります:
interface IHasName{ string Name(); };
string addNames<T>( T first, T second ) where T : IHasName { .... }
次に、addNames に渡すものが IHasName インターフェイスなどを実装していることを確認する必要があります。Java の構文は異なりますが ( <T extends IHasName>
)、同じ問題があります。
この問題の「古典的な」ケースは、これを行う関数を作成しようとすることです
string addNames<T>( T first, T second ) { return first + second; }
メソッドを含むインターフェイスを宣言する方法がないため、実際にこのコードを記述することはできません+
。あなたは失敗します。
C++ には、これらの問題はありません。コンパイラは、型を VM に渡すことを気にしません。両方のオブジェクトに .Name() 関数がある場合、コンパイルされます。そうでなければ、そうはなりません。単純。
それで、あなたはそれを持っています:-)
C++ では、「ジェネリック」という用語はほとんど使用されません。代わりに、「テンプレート」という言葉が使用されており、より正確です。テンプレートでは、一般的な設計を実現するための1 つの手法について説明します。
C++ テンプレートは、主に 2 つの理由から、C# と Java の両方が実装するものとは大きく異なります。最初の理由は、C++ テンプレートではコンパイル時の型引数だけでなく、コンパイル時の const-value 引数も許可されているためです。テンプレートは、整数または関数シグネチャとしても指定できます。これは、計算などの非常にファンキーな処理をコンパイル時に実行できることを意味します。
template <unsigned int N>
struct product {
static unsigned int const VALUE = N * product<N - 1>::VALUE;
};
template <>
struct product<1> {
static unsigned int const VALUE = 1;
};
// Usage:
unsigned int const p5 = product<5>::VALUE;
このコードは、C++ テンプレートのもう 1 つの際立った機能、つまりテンプレートの特殊化も使用します。product
コードは、1つの値引数を持つ 1つのクラス テンプレートを定義します。また、引数が 1 に評価されるたびに使用されるそのテンプレートの特殊化も定義します。これにより、テンプレート定義に対する再帰を定義できます。これはAndrei Alexandrescuによって最初に発見されたと思います。
テンプレートの特殊化は、データ構造の構造上の違いを許容するため、C++ にとって重要です。全体としてのテンプレートは、タイプ間でインターフェースを統一する手段です。ただし、これは望ましいことですが、実装内ですべての型を同等に扱うことはできません。C++ テンプレートはこれを考慮に入れています。これは、OOP が仮想メソッドのオーバーライドを使用してインターフェイスと実装の間で行う違いとほとんど同じです。
C++ テンプレートは、そのアルゴリズム プログラミング パラダイムに不可欠です。たとえば、コンテナーのほとんどすべてのアルゴリズムは、コンテナー型をテンプレート型として受け入れ、それらを一様に扱う関数として定義されています。実際、これは正しくありません。C++ はコンテナーでは機能せず、コンテナーの先頭と末尾の後ろを指す 2 つの反復子によって定義される範囲で機能します。したがって、コンテンツ全体が反復子によって囲まれます: begin <= elements < end.
コンテナの代わりにイテレータを使用すると、コンテナ全体ではなく一部を操作できるため便利です。
C++ のもう 1 つの際立った特徴は、クラス テンプレートの部分的な特殊化の可能性です。これは、Haskell やその他の関数型言語における引数のパターン マッチングに多少関連しています。たとえば、要素を格納するクラスを考えてみましょう。
template <typename T>
class Store { … }; // (1)
これは、どの要素タイプでも機能します。しかし、特別なトリックを適用することで、ポインターを他の型よりも効率的に格納できるとしましょう。これを行うには、すべてのポインター型を部分的に特殊化します。
template <typename T>
class Store<T*> { … }; // (2)
これで、1 つの型のコンテナー テンプレートをインスタンス化するたびに、適切な定義が使用されます。
Store<int> x; // Uses (1)
Store<int*> y; // Uses (2)
Store<string**> z; // Uses (2), with T = string*.
Anders Hejlsberg 自身が「C#、Java、および C++ のジェネリック」で違いを説明しています。
違いについてはすでに多くの良い答えがありますので、少し異なる視点を与えて、その理由を追加しましょう。
すでに説明したように、主な違いは型消去です。つまり、Javaコンパイラがジェネリック型を消去し、生成されたバイトコードに含まれないという事実です。しかし、問題は、なぜ誰かがそれをするのかということです。意味がありません!それともそうですか?
さて、代替手段は何ですか?言語でジェネリックを実装しない場合、どこに実装しますか?そしてその答えは、仮想マシンでです。これは下位互換性を壊します。
一方、型消去を使用すると、ジェネリッククライアントと非ジェネリックライブラリを混在させることができます。言い換えると、Java 5でコンパイルされたコードは、引き続きJava1.4にデプロイできます。
ただし、Microsoftは、ジェネリック医薬品の下位互換性を破ることを決定しました。そのため、.NETGenericsはJavaGenericsよりも「優れています」。
もちろん、Sunは馬鹿でも臆病者でもありません。彼らが「ひよこ」になった理由は、ジェネリックスを導入したとき、Javaは.NETよりもかなり古く、普及していたためです。(これらは両方の世界でほぼ同時に導入されました。)下位互換性を壊すことは大きな苦痛でした。
さらに別の言い方をすれば、Javaではジェネリックは言語の一部であり(つまり、 Javaにのみ適用され、他の言語には適用されません)、. NETでは仮想マシンの一部です(つまり、ジェネリックはすべての言語に適用されます。 C#とVisual Basic.NETのみ)。
これを、LINQ、ラムダ式、ローカル変数型推論、無名型、式ツリーなどの.NET機能と比較してください。これらはすべて言語機能です。そのため、VB.NETとC#の間には微妙な違いがあります。これらの機能がVMの一部である場合、それらはすべての言語で同じになります。ただし、CLRは変更されていません。.NET3.5SP1でも.NET2.0と同じです。LINQを使用するC#プログラムを.NET 3.5コンパイラでコンパイルし、.NET 3.5ライブラリを使用しない場合は、.NET2.0で実行できます。これはジェネリックスと.NET1.1では機能しませんが、JavaとJava1.4では機能します。
前回の投稿の続きです。
テンプレートは、使用する IDE に関係なく、C++ がインテリセンスでひどく失敗する主な理由の 1 つです。テンプレートの特殊化により、IDE は、特定のメンバーが存在するかどうかを実際に確認することはできません。検討:
template <typename T>
struct X {
void foo() { }
};
template <>
struct X<int> { };
typedef int my_int_type;
X<my_int_type> a;
a.|
現在、カーソルは指定された位置にあり、IDE がその時点でメンバーが何を持っているかを判断するのは非常に困難ですa
。他の言語の場合、解析は簡単ですが、C++ の場合は事前にかなりの評価が必要です。
ひどくなる。my_int_type
クラス テンプレート内でも定義されている場合はどうなるでしょうか。現在、その型は別の型引数に依存します。そしてここでは、コンパイラでさえ失敗します。
template <typename T>
struct Y {
typedef T my_type;
};
X<Y<int>::my_type> b;
少し考えた後、プログラマーはこのコードが上記と同じであると結論Y<int>::my_type
付けるでしょint
う。b
a
違う。コンパイラがこのステートメントを解決しようとする時点では、実際にはまだわかりY<int>::my_type
ません! したがって、これが型であることはわかりません。メンバー関数やフィールドなど、何か他のものである可能性があります。これによりあいまいさが生じる可能性があり (この場合ではありませんが)、コンパイラは失敗します。型名を参照していることを明示的に伝える必要があります。
X<typename Y<int>::my_type> b;
これで、コードがコンパイルされます。この状況からあいまいさがどのように発生するかを確認するには、次のコードを検討してください。
Y<int>::my_type(123);
このコード ステートメントは完全に有効で、C++ に への関数呼び出しを実行するように指示しY<int>::my_type
ます。ただし、my_type
が関数ではなく型の場合、このステートメントは引き続き有効であり、多くの場合コンストラクター呼び出しである特別なキャスト (関数スタイルのキャスト) を実行します。コンパイラは私たちが何を意味するのかを判断できないため、ここで明確にする必要があります。
Java と C# はどちらも、最初の言語リリース後にジェネリックを導入しました。ただし、ジェネリックが導入されたときにコア ライブラリがどのように変更されたかには違いがあります。 C# のジェネリックは単なるコンパイラ マジックではないため、下位互換性を損なうことなく既存のライブラリ クラスを生成することはできませんでした。
たとえば、Java では、既存のCollections Frameworkが完全に汎用化されました。 Java には、コレクション クラスの汎用バージョンと従来の非汎用バージョンの両方がありません。 いくつかの点で、これははるかにクリーンです。C# でコレクションを使用する必要がある場合、非ジェネリック バージョンを使用する理由はほとんどありませんが、これらのレガシー クラスはそのまま残り、ランドスケープが乱雑になります。
もう 1 つの顕著な違いは、Java と C# の Enum クラスです。 Java の Enum には、このやや曲がりくねった定義があります。
// java.lang.Enum Definition in Java
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
(これがなぜそうであるかについてのAngelika Langerの非常に明確な説明を参照してください。本質的に、これはJavaが文字列からそのEnum値への型安全なアクセスを与えることができることを意味します:
// Parsing String to Enum in Java
Colour colour = Colour.valueOf("RED");
これを C# のバージョンと比較します。
// Parsing String to Enum in C#
Colour colour = (Colour)Enum.Parse(typeof(Colour), "RED");
Enum はジェネリックが言語に導入される前に C# に既に存在していたため、既存のコードを壊さずに定義を変更することはできませんでした。したがって、コレクションと同様に、この古い状態でコア ライブラリに残ります。
11 か月遅れましたが、この質問は Java ワイルドカードの準備ができていると思います。
これは Java の構文上の機能です。メソッドがあるとします:
public <T> void Foo(Collection<T> thing)
また、メソッド本体で型 T を参照する必要がないとします。T という名前を宣言し、それを 1 回しか使用していないのに、なぜその名前を考えなければならないのでしょうか? 代わりに、次のように記述できます。
public void Foo(Collection<?> thing)
疑問符は、その場所に 1 回だけ表示する必要がある通常の名前付き型パラメーターを宣言したかのようにコンパイラーに要求します。
ワイルドカードを使用してできることで、名前付き型パラメーターを使用してもできないことはありません (これは、C++ および C# で常に行われている方法です)。
ウィキペディアには、 Java/C# ジェネリックとJava ジェネリック/C++テンプレートの両方を比較した優れた記事があります。ジェネリックに関するメインの記事は少し雑然としているように見えますが、有益な情報が含まれています。
他の非常に興味深い提案の中で、ジェネリックスの改良と下位互換性の破壊に関する提案があるようです。
現在、ジェネリックは消去を使用して実装されています。つまり、ジェネリック型の情報は実行時に利用できないため、ある種のコードを記述しにくくなっています。ジェネリックは、古い非ジェネリックコードとの下位互換性をサポートするためにこの方法で実装されました。ジェネリックを改良すると、実行時にジェネリック型の情報が利用できるようになり、従来の非ジェネリックコードが壊れてしまいます。ただし、Neal Gafterは、下位互換性を損なわないように、指定された場合にのみ型を再有効化することを提案しています。
最大の不満は型消去です。その中で、ジェネリックは実行時に強制されません。 これは、件名に関するいくつかの Sun ドキュメントへのリンクです。
ジェネリックは型消去によって実装されます。ジェネリック型情報はコンパイル時にのみ存在し、その後コンパイラによって消去されます。
C++ テンプレートは、コンパイル時に評価され、特殊化をサポートするため、実際には C# や Java のテンプレートよりもはるかに強力です。これにより、テンプレートのメタプログラミングが可能になり、C++ コンパイラがチューリング マシンと同等になります (つまり、コンパイル プロセス中に、チューリング マシンで計算可能なものは何でも計算できます)。
Java では、ジェネリックはコンパイラ レベルのみであるため、次のようになります。
a = new ArrayList<String>()
a.getClass() => ArrayList
'a' の型は、文字列のリストではなく、配列リストであることに注意してください。したがって、バナナのリストの型は equal() サルのリストになります。
いわば。
注意:コメントするのに十分なポイントがないので、これをコメントとして適切な回答に移動してください。
どこから来たのかわからないという一般的な考えに反して、.net は下位互換性を損なうことなく真のジェネリックを実装し、そのために明示的な努力を費やしました。.net 2.0 で使用するためだけに、非ジェネリックの .net 1.0 コードをジェネリックに変更する必要はありません。ジェネリック リストと非ジェネリック リストの両方が .Net Framework 2.0 でも 4.0 まで引き続き使用できます。これは、下位互換性のためだけです。したがって、非ジェネリック ArrayList をまだ使用していた古いコードは引き続き機能し、以前と同じ ArrayList クラスを使用します。下位コード互換性は、1.0 から現在まで常に維持されています。したがって、.net 4.0 でも、1.0 BCL の非ジェネリック クラスを使用することを選択した場合は、オプションを使用する必要があります。
したがって、Java が真のジェネリックをサポートするために下位互換性を破る必要はないと思います。