1. ホーム
  2. c#

[解決済み】C#やJavaのGenericsと...C++のTemplatesの違いは?[終了しました]

2022-04-12 19:51:55

質問

私は主にJavaを使用しており、ジェネリックは比較的新しいものです。Javaは間違った決定をしたとか、.NETの方が良い実装をしているとか、そういう記事を何度も読みました。

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

解決方法は?

雑音に声を加えて、一刀両断にする。

C#のジェネリックでは、このような宣言が可能です。

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

でないものを置くと、コンパイラが防いでくれます。 Person をリストに追加します。

裏側では、C#コンパイラはただ単に List<Person> を .NET dll ファイルに追加しますが、実行時には、JIT コンパイラが新しいコードセットを構築します。 ListOfPerson .

この利点は、本当に速くなることです。キャストも何もありませんし、DLLにはこれがListであるという情報が含まれているので 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の世界ではより重要なことで、Java 5を使ったコードをジェネリックスでコンパイルし、古い1.4以前のJVMで動作させることをサポートしたかったのですが、マイクロソフトは意図的にそれをしないことにしたのです。

デメリットは、前述したスピードの低下と、(1)(2)(3)がないことです。 ListOfPerson 擬似クラスやそのようなものが.classファイルに入っていると、後でそれを見るコード(リフレクションを使ったり、他のコレクションからそれを取り出して、それが Object だけを含むリストであることを意味するものであることは、何ら見分けがつきません。 Person であり、他の配列リストとは異なる。

C++のテンプレートでは、以下のような宣言が可能です。

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

見た目はC#とJavaのジェネリックのようで、思ったとおりに動いてくれますが、裏では違うことが起きているのです。

C#のジェネリックと最も共通するのは、特殊な pseudo-classes javaのように型情報を捨ててしまうのではなく、全く別の魚の缶詰のようなものです。

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(); }

このコードは、C# や Java ではコンパイルできません。 T というメソッドを提供しています。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()関数を持っていれば、コンパイルされます。もし、両方のオブジェクトに.Name()関数があれば、コンパイルできます。単純なことです。

というわけで、これで完成です :-)