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

Он имеет больше всего общего с C# дженериками в том, что он создает специальные pseudo-classes вместо того, чтобы просто выбросить информацию о типе, как это делает 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(), он скомпилируется. Если они этого не сделают, это не так. Просто.

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

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++ работает не с контейнерами, а с диапазонами, которые определяются двумя итераторами, указывающими на начало и конец контейнера. Таким образом, весь контент ограничен итераторами: начало <= элементы <конец.

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

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

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

Как уже было объяснено, основным отличием является стирание типов, то есть тот факт, что компилятор 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.

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

Шаблоны являются одной из основных причин, почему 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 это не функция, а скорее тип, этот оператор по-прежнему будет действительным и будет выполнять специальное приведение (приведение к типу функции), которое часто является вызовом конструктора. Компилятор не может сказать, что мы имеем в виду, поэтому мы должны устранить неоднозначность здесь.

И 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# до того, как в язык были введены обобщения, определение не могло быть изменено без нарушения существующего кода. Таким образом, как и коллекции, он остается в основных библиотеках в этом устаревшем состоянии.

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

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

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

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

public void Foo(Collection<?> thing)

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

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

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

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

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

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

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

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

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

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

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

Так сказать.

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

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

Другие вопросы по тегам