Каковы различия между Generics в С# и Java... и шаблонами в С++?

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

Итак, каковы основные отличия между С++, С#, Java в дженериках? Плюсы/минусы каждого?

Ответы

Ответ 1

Я добавлю свой голос к шуму и сделаю удар, чтобы все было ясно:

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

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

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

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

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

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

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

Поверхность выглядит одинаково, и это своего рода. Компилятор также помешает вам помещать в список вещи, которые не являются Person.

Разница в том, что происходит за кулисами. В отличие от С#, Java не идет и не создает специальный ListOfPerson - он просто использует простой старый ArrayList, который всегда был на Java. Когда вы получаете вещи из массива, обычный кастинг-танец 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>();

Похоже на С# и Java generics, и он будет делать то, что, по вашему мнению, должен делать, но за кулисами происходят разные вещи.

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

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

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

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

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

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

Этот код не будет компилироваться на С# или Java, потому что он не знает, что тип T фактически предоставляет метод под названием Name(). Вы должны сказать это - в С#, как это:

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

Вы действительно не можете написать этот код, потому что нет способов объявить интерфейс с помощью метода +. Вы терпите неудачу.

С++ не страдает ни одной из этих проблем. Компилятор не заботится о передаче типов к любой виртуальной машине - если оба объекта имеют функцию .Name(), она будет компилироваться. Если они этого не сделают, это не произойдет. Простой.

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

Ответ 2

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

Шаблоны С++ сильно отличаются от того, что оба С# и Java реализуют по двум основным причинам. Первая причина заключается в том, что шаблоны С++ не только допускают аргументы типа компиляции, но и аргументы 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;

В этом коде также используется другая отличительная особенность шаблонов С++, а именно специализированная специализация. Код определяет один шаблон шаблона, product, который имеет один аргумент значения. Он также определяет специализацию для этого шаблона, которая используется всякий раз, когда аргумент оценивается в 1. Это позволяет мне определить рекурсию над определениями шаблонов. Я считаю, что это было впервые обнаружено Andrei Alexandrescu.

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

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

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

Другой отличительной особенностью С++ является возможность частичной специализации для шаблонов классов. Это несколько связано с сопоставлением шаблонов в аргументах в 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*.

Ответ 4

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

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

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

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

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

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

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

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

Ответ 5

Следуйте за моей предыдущей публикацией.

Шаблоны - одна из основных причин, по которым С++ терпит неудачу так же в 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. Для других языков синтаксический анализ был бы простым, но для С++ требуется довольно немного оценки.

Ухудшается. Что, если 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);

Этот оператор кода отлично действует и сообщает С++ выполнить вызов функции Y<int>::my_type. Однако, если my_type не является функцией, а скорее типом, этот оператор все равно будет действительным и выполняет специальный листинг (приведение в стиле функции), который часто является вызовом конструктора. Компилятор не может сказать, что мы имеем в виду, поэтому мы должны устранить эту проблему.

Ответ 6

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

Например, в Java существующая Структура коллекций была полностью обобщена. Java не имеет как универсальной, так и старой версии классов коллекций. В некотором смысле это намного чище - если вам нужно использовать коллекцию на С#, на самом деле очень мало причин идти с не-универсальной версией, но эти унаследованные классы остаются на месте, загромождая пейзаж.

Еще одно заметное отличие - это классы Enum в Java и С#. 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");

Сравните это с версией С#:

//  Parsing String to Enum in C#
Colour colour = (Colour)Enum.Parse(typeof(Colour), "RED");

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

Ответ 7

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

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

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

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

public void Foo(Collection<?> thing)

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

Там вы ничего не можете сделать с помощью подстановочных знаков, которые вы также не можете использовать с параметром named type (как это всегда делается в С++ и С#).

Ответ 9

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

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

Ответ 10

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

Ответ 11

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

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

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

Так сказать.

Ответ 12

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

В настоящее время реализованы дженерики используя стирание, что означает, что информация общего типа не доступный во время выполнения, что делает некоторые тип кода трудно писать. Дженерики были реализованы таким образом, чтобы обратная совместимость со старыми не общий код. Обоснованные дженерики сделает общий тип информация доступна во время выполнения, которые нарушили бы устаревшие не общие код. Однако Нил Гафтер предлагаемые типы, подлежащие только повторному использованию если указано, чтобы не нарушать обратная совместимость.

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

Ответ 13

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

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

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