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

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

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

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

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

List foo = new List(); 

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

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

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

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

 ArrayList foo = new ArrayList(); 

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

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

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

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

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

 std::list* foo = new std::list(); 

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

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

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

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

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

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

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

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

 interface IHasName{ string Name(); }; string addNames( T first, T second ) where T : IHasName { .... } 

И тогда вы должны убедиться, что вещи, которые вы передаете addNames, реализуют интерфейс IHasName и т. Д. Синтаксис java отличается ( ), но он испытывает те же проблемы.

«Классическим» случаем для этой проблемы является попытка написать функцию, которая делает это

 string addNames( T first, T second ) { return first + second; } 

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

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

Итак, у вас есть это 🙂

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

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

 template  struct product { static unsigned int const VALUE = N * product::VALUE; }; template <> struct product<1> { static unsigned int const VALUE = 1; }; // Usage: unsigned int const p5 = product<5>::VALUE; 

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

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

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

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

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

 template  class Store { … }; // (1) 

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

 template  class Store { … }; // (2) 

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

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

Сам Андерс Хейлсберг рассказал о различиях здесь « Дженерики в C #, Java и C ++ ».

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

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

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

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

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

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

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

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

Последующие действия по сравнению с предыдущей публикацией.

Шаблоны – одна из основных причин, по которым C ++ терпит неудачу так сильно в intellisense, независимо от используемой IDE. Из-за специализации шаблона среда IDE никогда не может быть действительно уверена, существует ли данный член или нет. Рассматривать:

 template  struct X { void foo() { } }; template <> struct X { }; typedef int my_int_type; X a; a.| 

Теперь, курсор находится в указанной позиции, и для этой IDE сложно сказать, если и что у членов a . Для других языков синтаксический анализ был бы прост, но для C ++ требуется довольно немного оценки.

Становится хуже. Что, если my_int_type были определены внутри шаблона classа? Теперь его тип будет зависеть от аргумента другого типа. И здесь даже компиляторы терпят неудачу.

 template  struct Y { typedef T my_type; }; X::my_type> b; 

После некоторого раздумья программист сделал вывод, что этот код такой же, как и выше: Y::my_type разрешает int , поэтому b должен быть того же типа, что и a , правильно?

Неправильно. В момент, когда компилятор пытается разрешить этот оператор, он еще не знает Y::my_type ! Поэтому он не знает, что это тип. Это может быть что-то другое, например, функция-член или поле. Это может привести к двусмысленности (хотя и не в данном случае), поэтому компилятор терпит неудачу. Мы должны прямо сказать, что мы ссылаемся на имя типа:

 X::my_type> b; 

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

 Y::my_type(123); 

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

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

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

Другим заметным отличием являются classы Enum в Java и C #. Java Enum имеет это несколько извилистое определение:

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

(см. очень ясное объяснение Анжелики Лангер, почему именно так. По сути, это означает, что 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 Wildcard.

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

 public  void Foo(Collection thing) 

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

 public void Foo(Collection thing) 

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

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

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

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

Генерики реализуются с помощью стирания типа: информация об общем типе присутствует только во время компиляции, после чего она удаляется компилятором.

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

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

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

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

Так сказать.

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

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

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

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

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

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

  • Функция шаблона C ++ компилируется в заголовке, но не выполняется
  • объявлять функцию шаблона шаблона classа шаблона
  • Статическая переменная шаблона
  • C ++ шаблоны Turing-complete?
  • НЕ используя шаблон репозитория, используйте ORM as is (EF)
  • gcc может скомпилировать вариационный шаблон, в то время как clang не может
  • функция члена шаблона classа шаблона, вызванная из функции шаблона
  • шаблоны: переменные-члены родительского classа не видны в унаследованном classе
  • Вычисление шаблона для функции на основе возвращаемого типа?
  • Почему необходим allocator :: rebind, когда у нас есть параметры шаблона шаблона?
  • Как использовать class в Java?
  • Давайте будем гением компьютера.