C#とJavaのジェネリックスとC ++のテンプレートの違いは何ですか?[閉まっている]


203

私は主にJavaを使用しており、ジェネリックは比較的新しいものです。Javaが間違った決定をしたか、.NETの方が実装が優れているなど、私は読み続けています。

では、ジェネリックにおけるC ++、C#、Javaの主な違いは何ですか?それぞれの長所/短所?

回答:


364

私はノイズに声を加えて、物事を明確にすることを試みます:

C#ジェネリックスを使用すると、このようなものを宣言できます。

List<Person> foo = new List<Person>();

そして、コンパイラーはPersonリストにないものを入れないようにします。
背後ではC#コンパイラがList<Person>.NET dllファイルに入れているだけですが、実行時にJITコンパイラが新しいコードセットを作成してビルドします。ListOfPerson。です。

これの利点は、それが本当に速くなることです。キャストやその他の要素はありません。dllには、これがのリストであるという情報が含まれているため、Person後でリフレクションを使用して調べた他のコードは、Personオブジェクトが含まれていることを通知できます(インテリセンスなどを取得します)。

これの欠点は、古いC#1.0および1.1コード(ジェネリックを追加する前)がこれらの新しいを理解しないためList<something>、手動で変換してプレーンな古いコードに戻す必要があることです。Listして相互運用する必要があることです。C#2.0バイナリコードには下位互換性がないため、これはそれほど大きな問題ではありません。これが発生するのは、古いC#1.0 / 1.1コードをC#2.0にアップグレードする場合のみです。

Java Genericsでは、このようなものを宣言できます。

ArrayList<Person> foo = new ArrayList<Person>();

表面的には同じように見えますが、まあまあです。コンパイラーはPerson、リストにないものを入れないようにします。

違いは、舞台裏で何が起こるかです。C#とは異なり、Javaは特別なビルドを行いませんListOfPerson- ArrayList常にJavaで使用されているプレーンな古いものを使用するだけです。配列から何かを取り出しても、通常のPerson p = (Person)foo.get(1);キャストダンスを実行する必要があります。コンパイラーはキー入力を節約しますが、いつものように速度のヒット/キャスティングはまだ発生します。
人々が「Type Erasure」に言及するとき、これは彼らが話していることです。コンパイラーがキャストを挿入してから、それがPerson単なるリストではないことを意味するという事実を「消去」しますObject

このアプローチの利点は、ジェネリックスを理解しない古いコードが気にする必要がないことです。それArrayListはいつもと同じ古いものをまだ扱っています。ジェネリックでJava 5を使用してコードをコンパイルし、それを古い1.4または以前のJVMで実行することをサポートしたかったため、これはJavaの世界でより重要です。

欠点は、前述のスピードヒットです。またListOfPerson、.classファイルに入る疑似クラスなどがないため、後でそれを調べるコード(リフレクションを使用するか、別のコレクションからプルした場合)変換された場所Objectなど)はPerson、他の配列リストだけではなく、それだけを含むリストであることを意味しているとはまったく言えません。

C ++テンプレートを使用すると、次のように宣言できます

std::list<Person>* foo = new std::list<Person>();

これはC#とJavaのジェネリックのように見え、期待どおりに動作しますが、裏ではさまざまなことが起こっています。

pseudo-classesJavaのように型情報を捨てるのではなく、特別に構築するという点で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()関数がある場合、コンパイルされます。そうでない場合は、そうなりません。シンプル。

だから、それがあります:-)


8
C#で参照型用に生成された疑似クラスは同じ実装を共有するため、ListOfPeopleを正確に取得できません。blogs.msdn.com/ericlippert/archive/2009/07/30/…を
Piotr Czapla 2009

4
いいえ、ジェネリックを使用してJava 5コードをコンパイルすることはでき、古い1.4 VMで実行することできません(少なくともSun JDKはこれを実装していません。サードパーティの一部のツールはこれを実装しています)。以前にコンパイルした1.4 JARを使用して、 1.5 / 1.6コード。
finnw

4
int addNames<T>( T first, T second ) { return first + second; }C#では記述できないという文には反対です。ジェネリック型は、インターフェイスではなくクラスに制限することができ、+演算子を含むクラスを宣言する方法があります。
Mashmagar、2011

4
@AlexanderMalakhovそれはわざと慣用的ではありません。ポイントは、C ++のイディオムについて教育することではなく、同じように見えるコードの一部が各言語でどのように異なる方法で処理されるかを示すことでした。この目標は、コードの見た目を変えるほど難しくなるでしょう
Orion Edwards

3
@phresnel私は原則的に同意しますが、そのスニペットを慣用的なC ++で記述した場合、C#/ Java開発者には理解しづらくなるため、(私は)違いを説明するのにより悪い仕事をしたでしょう。これについては同意しないことに同意しましょう:-)
Orion Edwards

61

C ++が「ジェネリック」の用語を使用することはほとんどありません。代わりに、「テンプレート」という単語が使用され、より正確になります。テンプレートは、一般的な設計を実現するための1つの手法を説明します。

C ++テンプレートは、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と評価されるたびに使用される、そのテンプレートの特殊化も定義します。これにより、テンプレート定義の再帰を定義できます。これはAndrei Alexandrescuによって最初に発見されたと思います。

テンプレートの特殊化は、データ構造の構造的な違いを可能にするため、C ++にとって重要です。テンプレートは全体として、タイプ間でインターフェースを統合する手段です。ただし、これは望ましいことですが、すべての型を実装内で等しく扱うことはできません。C ++テンプレートはこれを考慮に入れます。これは、仮想メソッドのオーバーライドにより、インターフェイスと実装の間でOOPが行う違いとほとんど同じです。

C ++テンプレートは、そのアルゴリズムプログラミングパラダイムに不可欠です。たとえば、コンテナのほとんどすべてのアルゴリズムは、コンテナタイプをテンプレートタイプとして受け入れ、それらを均一に扱う関数として定義されています。実際、それは正しくありません。C++はコンテナーでは機能せず、コンテナーの最初と最後を指す2つのイテレーターで定義された範囲で機能します。したがって、コンテンツ全体は反復子によって制限されます。開始<=要素<終了。

コンテナの代わりにイテレータを使用すると、コンテナ全体ではなくコンテナの一部を操作できるため便利です。

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*.

.netのジェネリックス機能を使用して、型以外のものがキーとして使用されるようにしたい場合があります。値タイプの配列がフレームワークの一部である場合(構造内に固定サイズの配列を埋め込む古いAPIとやり取りする必要があることを考えると、ある意味ではそうではないことに驚いています)、いくつかの個別のアイテムと、サイズがジェネリックパラメーターである値型配列を含むクラス。それがそうであるように、最も近いものは、個々の項目を保持するクラスオブジェクトを持ち、配列を保持する別のオブジェクトへの参照も保持することです。
スーパーキャット2013年

@supercatレガシーAPIとやり取りする場合、マーシャリング(属性を介して注釈を付けることができます)を使用することが考えられます。とにかく、CLRには固定サイズの配列がないので、型のないテンプレート引数があると、ここでは役に立ちません。
Konrad Rudolph

私が困惑しているのは、固定サイズの値型配列を使用するのは難しいことではなく、多くのデータ型を値ではなく参照でマーシャリングできるようになったことだと思います。値によるマーシャルは他の方法ではまったく処理できない場合に役立ちますが、参照によるマーシャルは、それが使用可能なほとんどすべてのケースで優れていると考え、そのようなケースに固定された構造体を含めることができるようにしますサイズの配列は便利な機能のようです。
スーパーキャット2013年

ところで、タイプではないジェネリックパラメーターが役立つ別の状況として、ディメンション化された数量を表すデータタイプがあります。量を表すインスタンス内に次元情報を含めることができますが、タイプ内にそのような情報があると、コレクションが特定の次元単位を表すオブジェクトを保持することになっていることを指定できます。
スーパーキャット2013年


18

良い答えの多くは、上ですでに存在するどのようなので、私は少し異なる視点を与え、追加してみましょう、違いがある理由を

すでに説明したように、主な違いは型の消去です。つまり、Javaコンパイラーがジェネリック型を消去し、それらが生成されたバイトコードに含まれないという事実です。しかし、問題は、なぜ誰もがそうするのでしょうか?意味がありません!それとも?

さて、代替手段は何ですか?あなたが言語でジェネリックを実装していない場合は、どこあなたはそれらを実装しますか?そして答えは、仮想マシン内です。これは後方互換性を壊します。

一方、型消去では、汎用クライアントと非汎用ライブラリを混在させることができます。つまり、Java 5でコンパイルされたコードは、Java 1.4にもデプロイできます。

ただし、マイクロソフトはジェネリックの下位互換性を解除することを決定しました。これが、.NET GenericsがJava Genericsより「優れている」理由です。

もちろん、Sunはばかや臆病者ではありません。彼らが「うまくいかなくなった」理由は、Javaがジェネリックを導入したとき、.NETよりもかなり古く、広く普及していたためです。(これらは両方の世界でほぼ同時に導入されました。)下位互換性を壊すことは非常に困難でした。

さらに言い換えると、Javaではジェネリックは言語の一部であり(つまり、Javaにのみ適用れ、他の言語には適用れません)、. NETでは、仮想マシンの一部です(つまり、すべての言語に適用され、 C#とVisual Basic.NETのみ)。

これを、LINQ、ラムダ式、ローカル変数型推論、匿名型、式ツリーなどの.NET機能と比較してください。これらはすべて言語機能です。そのため、VB.NETとC#の間には微妙な違いがあります。それらの機能がVMの一部である場合、すべての言語で同じになります。しかし、CLRは変更されていません。.NET3.5 SP1でも、.NET 2.0と同じです。.NET 3.5ライブラリを使用しない場合は、LINQを使用するC#プログラムを.NET 3.5コンパイラでコンパイルし、.NET 2.0で実行できます。それは考えていないジェネリック医薬品と.NET 1.1で動作しますが、それは考えたJavaおよびJava 1.4で動作します。


3
LINQは、主にライブラリ機能です(ただし、C#とVBには、それに加えて構文シュガーも追加されています)。2.0 CLRをターゲットとするすべての言語は、System.Coreアセンブリをロードするだけで、LINQを最大限に活用できます。
Richard Berg

ええ、申し訳ありませんが、私はもっと明確にすべきでした。LINQ。私はクエリ構文を参照していましたが、モナド標準クエリ演算子、LINQ拡張メソッド、IQueryableインターフェイスではありませんでした。もちろん、どの.NET言語からでも使用できます。
イェルクWミッターク

1
Javaの別のオプションを考えています。Oracleでさえ下位互換性を損なうことを望まない場合でも、型情報が消去されないようにするためにコンパイラーをトリックにすることができます。たとえばArrayList<T>、(非表示の)静的Class<T>フィールドを持つ、内部的に名前が付けられた新しいタイプとして出力できます。ジェネリックlibの新しいバージョンが1.5バイト以上のコードでデプロイされている限り、1.4のJVMで実行できます。
Earth Engine

14

以前の投稿のフォローアップ。

テンプレートは、使用されているIDEに関係なく、C ++がIntelliSenseで非常に失敗する主な理由の1つです。テンプレートは特殊化されているため、特定のメンバーが存在するかどうかをIDEが確認することはできません。考慮してください:

template <typename T>
struct X {
    void foo() { }
};

template <>
struct X<int> { };

typedef int my_int_type;

X<my_int_type> a;
a.|

現在、カーソルは指定された位置にあり、メンバーaが持っているかどうか、何を持っているかをIDEがその時点で言うのは難しいです。他の言語の場合、解析は簡単ですが、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が関数ではなくタイプの場合でも、このステートメントは有効であり、しばしばコンストラクター呼び出しである特別なキャスト(関数スタイルのキャスト)を実行します。コンパイラーはどちらを意味するのか判断できないため、ここで曖昧さをなくす必要があります。


2
私は全く同意します。しかし、いくつかの希望があります。オートコンプリートシステムとC ++コンパイラは非常に密接に相互作用する必要があります。Visual Studioにはそのような機能は決してないだろうと私は確信していますが、Eclipse / CDTまたはGCCに基づく他のIDEで問題が発生する可能性があります。望む !:)
ブノワ・

6

JavaとC#はどちらも、最初の言語リリース後にジェネリックを導入しました。ただし、ジェネリックが導入されたときにコアライブラリがどのように変更されたかには違いがあります。 C#のジェネリックは単なるコンパイラーマジックではないため、下位互換性を損なうことなく既存のライブラリクラスを生成することはできませんでした。

たとえば、Javaでは、既存のコレクションフレームワーク完全に汎用化されました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#に既に存在していたため、既存のコードを壊さずに定義を変更することはできませんでした。したがって、コレクションと同様に、このレガシー状態でもコアライブラリに残ります。


C#のジェネリックでさえ、単なるコンパイラーの魔法ではなく、コンパイラーは既存のライブラリーを生成するためにさらに魔法をかけることができます。名前ArrayListを変更List<T>して新しい名前空間に配置する必要がある理由はありません。実際には、クラスがソースコードに表示さArrayList<T>れた場合、それがILコードで別のコンパイラによって生成されたクラス名になるため、名前の競合は発生しません。
Earth Engine

4

11か月遅れますが、この質問はいくつかのJavaワイルドカード関連のものに対応できると思います。

これはJavaの構文機能です。次のメソッドがあるとします。

public <T> void Foo(Collection<T> thing)

また、メソッド本体で型Tを参照する必要がないとします。名前Tを宣言し、それを1回だけ使用しているのに、なぜその名前を考える必要があるのでしょうか。代わりに、次のように書くことができます:

public void Foo(Collection<?> thing)

疑問符は、コンパイラーに、その場所に1回だけ出現する必要がある通常の名前付き型パラメーターを宣言したふりをするように要求します。

ワイルドカードを使用してできることは、名前付きの型パラメーターを使用して実行することはできません(これは、C ++およびC#でこれらが常に行われる方法です)。


2
さらに11か月遅れます。名前付きの型パラメーターでは実行できないJavaワイルドカードを使用して実行できる処理があります。あなたはこれをJavaで行うことができます:class Foo<T extends List<?>>そして使用しますFoo<StringList>が、C#ではあなたはその余分な型パラメーターを追加する必要があります:class Foo<T, T2> where T : IList<T2>とclunkyを使用しFoo<StringList, String>ます。
R.マルティーニョフェルナンデス2010



1

C ++テンプレートは、コンパイル時に評価され、特殊化をサポートするため、実際にはC#およびJavaのテンプレートよりもはるかに強力です。これにより、テンプレートメタプログラミングが可能になり、C ++コンパイラがチューリングマシンと同等になります(つまり、コンパイルプロセス中に、チューリングマシンで計算できるものなら何でも計算できます)。


1

Javaでは、ジェネリックはコンパイラレベルのみであるため、次のようになります。

a = new ArrayList<String>()
a.getClass() => ArrayList

「a」のタイプは配列リストであり、文字列のリストではないことに注意してください。したがって、バナナのリストのタイプは、サルのリストとequal()になります。

いわば。


1

他の非常に興味深い提案の中に、ジェネリックの洗練と下位互換性の破壊に関するものがあるように見えます:

現在、ジェネリックは消去を使用して実装されています。つまり、ジェネリック型情報は実行時に利用できないため、ある種のコードを書くのが難しくなっています。ジェネリックはこの方法で実装され、古い非ジェネリックコードとの下位互換性をサポートします。ジェネリファイを具体化すると、実行時にジェネリック型情報が利用可能になり、レガシー非ジェネリックコードが破損します。ただし、Neal Gafterは、下位互換性を壊さないように、指定された場合にのみ型をreifiableにすることを提案しています。

Java 7の提案についてのアレックス・ミラーの記事


0

注意:コメントするのに十分なポイントがないので、コメントとして適切な回答に移動してください。

ポピュラーな信念とは対照的に、それがどこから来たのか私には理解できませんでしたが、.netは後方互換性を損なうことなく真のジェネリックを実装しました。.net 2.0で使用するためだけに、ジェネリックでない.net 1.0コードをジェネリックに変更する必要はありません。ジェネリックリストと非ジェネリックリストの両方は、4.0まででも.Net Framework 2.0で引き続き使用できます。これは、下位互換性の理由だけです。したがって、ジェネリック以外のArrayListを使用していた古いコードは引き続き機能し、以前と同じArrayListクラスを使用します。下位コードの互換性は、1.0から現在まで常に維持されています...したがって、.net 4.0でも、そうする場合は、1.0 BCLの非ジェネリッククラスを使用するオプションを選択する必要があります。

そのため、真のジェネリックをサポートするためにjavaが下位互換性を解除する必要はないと思います。


それは、人々が話しているような下位互換性ではありません。アイデアは、ランタイムの下位互換性です。.NET2.0でジェネリックを使用して記述されたコードは、.NETフレームワーク/ CLRの古いバージョンでは実行できません。同様に、Javaが「真の」ジェネリックを導入する場合、新しいJavaコードは古いJVMで実行できません(バイトコードに重大な変更を加える必要があるため)。
tzaman 2010

これはジェネリックではなく.netです。特定のCLRバージョンをターゲットにするには、常に再コンパイルが必要です。バイトコードの互換性、コードの互換性があります。また、古いジェネリックリストを使用していた古いコードを新しいジェネリックリストを使用するように変換する必要があることについて、具体的には返信していましたが、これはまったく当てはまりません。
Sheepy

1
人々は前方互換性について話していると思います。つまり、.net 1.1で実行する.net 2.0コードは、1.1ランタイムが2.0の「疑似クラス」について何も知らないため、機能しなくなります。では、「Javaは前方互換性を維持したいため、真の総称を実装しない」ということではないでしょうか。(逆方向ではなく)
10

互換性の問題は微妙です。「本物の」ジェネリックをJavaに追加すると、古いバージョンのJavaを使用するすべてのプログラムに影響するという問題ではなかったと思いますが、「新しく改良された」ジェネリックを使用したコードは、そのようなオブジェクトを古いコードと交換するのに苦労します。新しいタイプについては何も知りませんでした。たとえば、プログラムにのインスタンスを設定ArrayList<Foo>することになっている古いメソッドに渡したいがあると仮定ArrayListしますFooArrayList<foo>がでない場合、それArrayListをどのように機能させますか?
スーパーキャット2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.