Каковы различия между Обобщением в C # и Java… и Шаблонами в C ++? [закрыто]


203

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

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

Ответы:


364

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

Обобщения 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чтобы взаимодействовать с ними. Это не такая большая проблема, потому что двоичный код 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, которую Microsoft намеренно не стала беспокоить.

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

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

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

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

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

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

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

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

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

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

Итак, вот оно :-)


8
Сгенерированный псевдоколаз для ссылочных типов в C # имеет ту же реализацию, поэтому вы не получите точно ListOfPeople. Проверьте blogs.msdn.com/ericlippert/archive/2009/07/30/…
Петр Czapla

4
Нет, вы не можете скомпилировать код Java 5 с помощью универсальных шаблонов и запустить его на старых виртуальных машинах 1.4 (по крайней мере, Sun JDK не реализует это. Некоторые сторонние инструменты делают.) Что вы можете сделать, это использовать предварительно скомпилированные файлы 1.4 JAR из 1.5 / 1.6 код.
Finnw

4
Я возражаю против утверждения, что вы не можете писать int addNames<T>( T first, T second ) { return first + second; }на C #. Универсальный тип может быть ограничен классом вместо интерфейса, и есть способ объявить класс с +оператором в нем.
Машмагар

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

3
@ phresnel Я согласен в принципе, но если бы я написал этот фрагмент на идиоматическом C ++, он был бы гораздо менее понятен для разработчиков C # / Java, и поэтому (я считаю) сделал бы хуже, объясняя разницу. Давайте согласимся не соглашаться с этим :-)
Орион Эдвардс

61

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

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

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

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

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

Еще одна отличительная черта 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*.

Иногда мне хотелось, чтобы функция дженериков в .net позволяла использовать в качестве ключей что-то кроме типов. Если бы массивы типов значений были частью Framework (я удивлен, что это не так, учитывая необходимость взаимодействия со старыми API, которые встраивают массивы фиксированного размера в структуры), было бы полезно объявить класс, который содержит несколько отдельных элементов, а затем массив типа значения, размер которого является универсальным параметром. На самом деле, самое близкое, что может прийти, это иметь объект класса, который содержит отдельные элементы, а затем также содержит ссылку на отдельный объект, содержащий массив.
суперкат

@supercat Если вы взаимодействуете с устаревшим API, идея состоит в том, чтобы использовать маршаллинг (который можно аннотировать с помощью атрибутов). В любом случае CLR не имеет массивов фиксированного размера, поэтому использование не типовых аргументов шаблона здесь не поможет.
Конрад Рудольф

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

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


18

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

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

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

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

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

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

Иными словами, в Java Generics являются частью языка (что означает, что они применяются только к 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, но будет работать с Java и Java 1.4.


3
LINQ - это, прежде всего, библиотечная функция (хотя C # и VB также добавили синтаксический сахар вместе с ней). Любой язык, нацеленный на 2.0 CLR, может в полной мере использовать LINQ, просто загрузив сборку System.Core.
Ричард Берг

Да, извини, мне следовало быть более ясным LINQ. Я имел в виду синтаксис запроса, а не монадические стандартные операторы запросов, методы расширения LINQ или интерфейс IQueryable. Очевидно, вы можете использовать их с любого языка .NET.
Йорг Миттаг

1
Я думаю, еще один вариант для Java. Даже Oracle не хочет нарушать обратную совместимость, они все же могут сделать какую-то хитрость компилятора, чтобы избежать стирания информации о типах. Например, ArrayList<T>может передаваться как новый тип с внутренним именем со (скрытым) статическим Class<T>полем. Пока новая версия универсального lib была развернута с 1,5-байтовым кодом, она сможет работать на 1,4-JVM.
Земля Двигатель

14

Продолжение моей предыдущей публикации.

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

Становится хуже. Что если бы они 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 или некоторых других IDE, основанных на GCC, может произойти что-то подобное. НАДЕЖДА! :)
Benoît

6

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

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

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

//  java.lang.Enum Definition in Java
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, 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 # до того, как дженерики были введены в язык, определение не могло быть изменено без нарушения существующего кода. Таким образом, как и коллекции, он остается в основных библиотеках в этом устаревшем состоянии.


Даже дженерики C # - это не просто магия компилятора, компилятор может сделать еще одну магию для генерирования существующей библиотеки. Там нет причин , почему они должны переименовать , ArrayListчтобы List<T>и поместить его в новое пространство имен. Дело в том, что если в исходном коде появился класс, так как ArrayList<T>он стал бы другим именем класса, сгенерированным компилятором, в коде IL, то возникновение конфликтов имен невозможно.
Земля Двигатель

4

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

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

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

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

public void Foo(Collection<?> thing)

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

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


2
Еще через 11 месяцев ... Есть вещи, которые вы можете сделать с подстановочными знаками Java, которые вы не можете сделать с параметрами именованного типа. Вы можете сделать это в Java: class Foo<T extends List<?>>и использовать, Foo<StringList>но в C # вы должны добавить этот дополнительный параметр типа: class Foo<T, T2> where T : IList<T2>и использовать неуклюжий Foo<StringList, String>.
Р. Мартиньо Фернандес


1

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

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


1

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


1

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

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

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

Так сказать.


1

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

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

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


0

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

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

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


Это не тот тип обратной совместимости, о котором говорят люди. Идея заключается в обратной совместимости для среды выполнения : код, написанный с использованием обобщений в .NET 2.0, не может быть запущен в более старых версиях .NET Framework / CLR. Точно так же, если бы в Java были введены «истинные» обобщения, новый код Java не мог бы работать на старых JVM (потому что это требует критических изменений в байт-коде).
Цаман

Это .net, а не дженерики. Всегда требует перекомпиляции для определения конкретной версии CLR. Есть совместимость байт-кода, есть совместимость кода. Кроме того, я отвечал конкретно о необходимости преобразования старого кода, в котором использовался старый List, для использования нового универсального List, что вовсе не соответствует действительности.
Sheepy

1
Я думаю, что люди говорят о прямой совместимости . Т.е. код .net 2.0 для запуска на .net 1.1, который сломается, потому что среда выполнения 1.1 ничего не знает о 2.0 «псевдоклассе». Не должно ли тогда быть, что «Java не реализует истинный универсальный, потому что они хотят поддерживать прямую совместимость»? (а не назад)
Sheepy

Проблемы совместимости тонкие. Я не думаю, что проблема заключалась в том, что добавление «настоящих» обобщений в Java повлияло бы на любые программы, использующие более старые версии Java, а скорее на код, который использовал «новые улучшенные» обобщения, было бы трудно обмениваться такими объектами со старым кодом, который ничего не знал о новых типах. Предположим, например, что у программы есть то, ArrayList<Foo>что она хочет передать старому методу, который должен заполнить ArrayListэкземплярами Foo. Если ArrayList<foo>не является ArrayList, как можно заставить это работать?
суперкат
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.