Каковы различия между дженериками в C# и Java... и шаблонами в C++? [закрытый]

Я в основном использую Java, а дженерики относительно новые. Я продолжаю читать, что Java приняла неправильное решение или что .NET имеет лучшие реализации и т. д. так далее.

Итак, каковы основные различия между C++, C#, Java в дженериках? За / против каждого?

13 ответов


я добавлю свой голос к шуму и попытаюсь прояснить ситуацию:

C# дженерики позволяют объявить что-то вроде этого.

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

и тогда компилятор не позволит вам помещать вещи, которые не являются Person в список.
За кулисами компилятор C# просто ставит List<Person> в dll-файл .NET, но во время выполнения компилятор JIT идет и строит новый набор кода, как если бы вы написали специальный класс списка только для содержащие людей-что-то вроде ListOfPerson.

преимущество этого заключается в том, что он делает это очень быстро. Там нет кастинга или каких-либо других вещей, и потому что dll содержит информацию, что это список Person, другой код, который смотрит на него позже, используя отражение, может сказать, что он содержит Person объекты (таким образом, вы получаете intellisense и так далее).

недостатком этого является то, что старый код C# 1.0 и 1.1 (до того, как они добавили дженерики) не понимает эти новые List<something>, поэтому вам нужно вручную преобразовать вещи обратно в обычный старый List взаимодействовать с ними. Это не такая большая проблема, потому что двоичный код# 2.0 не совместимы. Это произойдет только в том случае, если вы обновляете старый код C# 1.0/1.1 до C# 2.0

Java Generics позволяют объявить что-то вроде этого.

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

на поверхности это выглядит так же, и это своего рода. Компилятор также не позволит вам поместить то, чего нет!--6--> в список.

разница в том, что происходит за кулисами. В отличие от C#, Java не идет и не строит специальный ListOfPerson - он просто использует старые ArrayList который всегда был на Java. Когда вы получаете вещи из массива, обычно Person p = (Person)foo.get(1); кастинг-танец еще предстоит сделать. Компилятор сохраняет вам нажатия клавиш, но скорость попадания/литья по-прежнему возникает так же, как и всегда.
Когда люди упоминают "стирание типа", это о чем они говорят. Компилятор вставляет приведения для вас, а затем "стирает" тот факт, что это должен быть список Person не только Object

преимущество этого подхода заключается в том, что старый код, который не понимает дженерики, не должен заботиться. Он все еще имеет дело с тем же старым ArrayList как всегда. Это более важно в мире java, потому что они хотели поддержать компиляцию кода с использованием Java 5 с дженериками и его запуск на старом 1.4 или предыдущем JVM, с которыми microsoft намеренно решила не заморачиваться.

недостатком является скорость удара, о которой я упоминал ранее, а также потому, что нет ListOfPerson псевдо-класс или что-то в этом роде .файлы классов, код, который смотрит на него позже (с отражением, или если вы вытащите его из другой коллекции, где он был преобразован в Object или так далее) никак не могу сказать, что это должен быть список, содержащий только Person и не только любой другой массив список.

шаблоны C++ позволяют объявить что-то вроде этого

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

это похоже на C# и Java-дженерики, и он будет делать то, что вы думаете, что он должен делать, но за кулисами происходят разные вещи.

он имеет больше всего общего с дженериками C# в том, что он строит специальные pseudo-classes вместо того, чтобы просто выбрасывать информацию о типе, как это делает java, но это совершенно другой чайник рыбы.

оба C# и Java производят вывод, предназначенный для виртуальных машин. Если вы пишете код, который имеет Person класс в нем, в обоих случаях информацию о Person класс войдет .dll или .файл класса, и JVM / CLR будет делать вещи с этим.

C++ создает необработанный двоичный код x86. Все не объект, и нет базовой виртуальной машины, которая должна знать о Person класса. Там нет бокса или распаковки, и функции не должны принадлежать занятия или что-нибудь в этом роде.

из - за этого компилятор C++ не накладывает никаких ограничений на то, что вы можете делать с шаблонами-в основном любой код, который вы можете написать вручную, вы можете получить шаблоны для записи для вас.
Самый очевидный пример-добавление вещей:

в C# и Java система generics должна знать, какие методы доступны для класса, и она должна передать это виртуальной машине. Единственный способ сказать это - либо жесткое кодирование фактический класс in или использование интерфейсов. Например:

string addNames<T>( T first, T second ) { return first.Name() + second.Name(); }

этот код не будет компилироваться на C# или Java, потому что он не знает, что тип T фактически предоставляет метод с именем Name (). Вы должны сказать это-на 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++ не страдает ни от одной из этих проблем. Компилятор не заботится о передаче типов вниз к любой виртуальной машине - если оба ваших объекта имеют a .Функция Name (), она будет компилироваться. Если нет, то не будет. Просто.

Итак, у вас есть это :-)


C++ редко использует терминологию "дженерики". Вместо этого используется слово "шаблоны", которое является более точным. Шаблоны описывает один техника для достижения общего дизайна.

шаблоны C++ сильно отличаются от того, что реализуют C# и Java по двум основным причинам. Первая причина заключается в том, что шаблоны C++ не только допускают аргументы типа времени компиляции, но и аргументы const-значения времени компиляции: шаблоны могут быть заданы как целые числа или даже сигнатуры функций. Этот означает, что вы можете делать некоторые довольно фанковые вещи во время компиляции, например вычисления:

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++, а именно специализацию шаблонов. Код определяет один шаблон класса,product, которая имеет один аргумент. Он также определяет специализацию для шаблона, который используется, когда аргумент равен 1. Это позволяет мне определить рекурсию над определениями шаблонов. Я считаю, что это было впервые обнаружено Андрей Александреску.

специализация шаблонов важна для C++, поскольку она позволяет учитывать структурные различия в структурах данных. Шаблоны в целом-это средство унификации интерфейса между типами. Однако, хотя это и желательно, все типы не могут рассматриваться одинаково внутри реализации. Шаблоны C++ учитывают это. Это очень похоже на то, что ООП делает между интерфейсом и реализацией с переопределением виртуального методы.

шаблоны C++ необходимы для его парадигмы алгоритмического программирования. Например, почти все алгоритмы для контейнеров определяются как функции, которые принимают тип контейнера как тип шаблона и обрабатывают их равномерно. На самом деле, это не совсем так: C++ работает не на контейнерах, а на диапазоны которые определяются двумя итераторами, указывающими на начало и конец контейнера. Таким образом, все содержание ограничено итераторы: begin

использование итераторов вместо контейнеров полезно, поскольку позволяет работать с частями контейнера, а не с целым.

еще одной отличительной особенностью C++ является возможность частичная специализация для шаблонов классов. Это несколько связано с сопоставлением шаблонов для аргументов в Haskell и других функциональных языках. Например, рассмотрим класс, который хранит элементы:

template <typename T>
class Store { … }; // (1)

это работает для любого типа элемента. Но предположим, что мы можем хранить указатели более эффективно, чем другие типы, применяя какой-то специальный трюк. Мы можем сделать это частично специализировать для всех типов указателя:

template <typename T>
class Store<T*> { … }; // (2)

теперь, когда мы создаем шаблон контейнера для одного типа, используется соответствующее определение:

Store<int> x; // Uses (1)
Store<int*> y; // Uses (2)
Store<string**> z; // Uses (2), with T = string*.

Андерс Хейльсберг сам описал различия здесь "дженерики в C#, Java и c++".


тут уже много хороших ответов на что различия есть, поэтому позвольте мне дать немного другую перспективу и добавить почему.

Как уже объяснялось, основное отличие - введите erasure, т. е. тот факт, что компилятор Java стирает общие типы, и они не заканчиваются в сгенерированном байт-коде. Однако возникает вопрос: зачем кому-то это делать? Это не имеет смысла! Или нет?

хорошо, какая альтернатива? Если вы не реализуете дженерики на языке, где do вы их реализовать? И ответ: в виртуальной машине. Что нарушает обратную совместимость.

стирание типов, с другой стороны, позволяет смешивать универсальные клиенты с не-универсальными библиотеками. Другими словами, код, который был скомпилирован на Java 5 все еще можно развернуть в Java 1.4.

Microsoft, однако, решила нарушить обратную совместимость для дженериков. Это почему .NET Generics "лучше", чем Java Generics.

конечно, Сун не идиоты и не трусы. Причина, почему они "струсили", что Java был значительно старше, и шире, чем .Сети, когда они ввели дженериков. (Они были введены примерно в то же время в обоих мирах.) Нарушение обратной совместимости было бы огромной болью.

еще один способ: в Java дженерики являются частью язык (что означает, что они применяются только на Java, а не на другие языки), в .NET они являются частью Виртуальная Машина (что означает, что они относятся к все языки, а не только C# и Visual Basic.NET).

сравните это с функциями .NET, такими как LINQ, лямбда-выражения, вывод типа локальной переменной, анонимные типы и деревья выражений: это все язык функции. Вот почему существуют тонкие различия между VB.NET и C#: если эти функции были частью виртуальной машины, они будут одинаковыми в все языки. Но среда CLR не изменилась: в .NET 3.5 SP1 она такая же, как и в .NET 2.0. Вы можете скомпилировать программу C#, использующую LINQ с компилятором .NET 3.5, и запустить ее на .NET 2.0, если не используете библиотеки .NET 3.5. Вот бы не работа с дженериками и .NET 1.1, но это would работа с Java и Java 1.4.


продолжение моего предыдущего постинга.

шаблоны являются одной из основных причин, почему C++ терпит неудачу в intellisense, независимо от используемой среды IDE. Из-за специализации шаблона среда 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++, довольно много оценки необходимо заранее.

это еще хуже. Что, если ... --5--> были также определены внутри шаблона класса? Теперь его тип будет зависеть от другого аргумента типа. И здесь даже компиляторы терпят неудачу.

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# - это не просто магия компилятора и поэтому не было возможности generify существующие классы библиотеки без нарушения обратной совместимости.

например, в Java существующий Основа Коллекции был полностью genericised. Java не имеет как универсальной, так и устаревшей непатентованной версии классов коллекций. в некотором смысле это намного чище - если вам нужно использовать коллекцию в C#, на самом деле очень мало причин идти с неродовой версией, но эти устаревшие классы остаются на месте, загромождая ландшафт.

еще одно заметное различие - классы перечислений в Java и C#. перечисление Java имеет этот несколько извилистый вид определение:

//  java.lang.Enum Definition in Java
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {

(см. Angelika Langer очень ясно объяснение, почему именно это так. По сути, это означает, что Java может дать безопасный доступ типа из строки к его значению перечисления:

//  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");

поскольку перечисление уже существовало в C# до того, как дженерики были введены в язык, определение не могло измениться без нарушения существующего кода. Так что, как и коллекции, он остается в ядре библиотеки в этом устаревшем состоянии.


11 месяцев спустя, но я думаю, что этот вопрос готов для некоторых подстановочных знаков Java.

это синтаксическая особенность Java. Предположим, у вас есть метод:

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

и предположим, что вам не нужно ссылаться на тип T в теле метода. Вы объявляете имя T, а затем используете его только один раз, так почему вы должны думать об имени для него? Вместо этого, вы можете написать:

public void Foo(Collection<?> thing)

знак вопроса просит компилятор притвориться, что вы объявили обычный параметр именованного типа, который должен отображаться только один раз в этом месте.

нет ничего, что вы можете сделать с подстановочными знаками, которые вы также не можете сделать с параметром именованного типа (как это всегда делается в C++ и c#).


Википедия имеет отличные записи, сравнивающие оба Java / C# generics и Java generics / C++ шаблоны. The основная статья о дженериках кажется немного загроможденным, но в нем есть хорошая информация.


самая большая жалоба-стирание типа. В этом случае дженерики не применяются во время выполнения. вот ссылка на некоторые документы Sun по этому вопросу.

дженерики реализуются по типу стирание: общие сведения о типе присутствует только во время компиляции, после который стирается компилятором.


шаблоны C++ на самом деле намного мощнее, чем их аналоги C# и Java, поскольку они оцениваются во время компиляции и поддерживают специализацию. Это позволяет Метапрограммировать шаблоны и делает компилятор C++ эквивалентным машине Тьюринга (т. е. во время процесса компиляции вы можете вычислить все, что можно вычислить с помощью машины Тьюринга).


в Java дженерики являются только уровнем компилятора, поэтому вы получаете:

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

обратите внимание, что тип " a " - это список массивов, а не список строк. Таким образом, тип списка бананов будет равен() списку обезьян.

так сказать.


похоже, среди других очень интересных предложений есть одно об уточнении дженериков и нарушении обратной совместимости:

В настоящее время реализованы дженерики использование стирания, что означает, что общие сведения о типе не доступно во время выполнения, что делает такой код трудно написать. Дженерик были реализованы таким образом, чтобы поддерживать обратная совместимость со старыми неродовой код. Овеществленные дженерики сделал бы общий тип информация, доступная во время выполнения, что сломает наследие non-generic код. Тем не менее, Нил Gafter имеет предложенный видах делая reifiable только если определено, то для того чтобы не сломать обратная совместимость.

at статья Алекса Миллера о предложениях Java 7


NB: у меня недостаточно комментариев, поэтому не стесняйтесь перемещать это как комментарий к соответствующему ответу.

Вопреки распространенному мнению, которое я никогда не понимал, откуда оно взялось, .net реализовал истинные дженерики без нарушения обратной совместимости, и они потратили на это явные усилия. Вам не нужно менять свой неродовой код .net 1.0 на дженерики только для использования в .net 2.0. Как общие, так и не общие списки по-прежнему доступны в .Net framework 2.0 даже до 4.0, именно по причине обратной совместимости. Поэтому старые коды, которые все еще использовали не универсальный ArrayList, будут работать и использовать тот же класс ArrayList, что и раньше. Обратная совместимость кода всегда поддерживается с 1.0 до сих пор... Поэтому даже в .net 4.0 вам все равно придется использовать любой класс non-generics из 1.0 BCL, если вы решите это сделать.

поэтому я не думаю, что java должна нарушать обратную совместимость для поддержки истинных дженериков.