Відповіді:
Я додаю свій голос до шуму і задумую, щоб зрозуміти все:
List<Person> foo = new List<Person>();
і тоді компілятор не дозволить вам внести речі, які не входять Person
до списку.
За лаштунками компілятор C # просто вкладає List<Person>
у файл .NET dll, але під час виконання компілятор JIT переходить і створює новий набір коду, як ніби ви написали спеціальний клас списку просто для вміщення людей - щось на зразокListOfPerson
.
Користь від цього полягає в тому, що це робить це дійсно швидко. Немає кастингу чи будь-яких інших речей, і оскільки dll містить інформацію про те, що це Перелік Person
, інший код, який переглядає його пізніше, використовуючи відображення, може сказати, що він міститьPerson
об'єкти (тому ви отримуєте інтелігенцію тощо).
Недоліком цього є те, що старий код C # 1.0 та 1.1 (до того, як вони додали дженерики) не розуміє цих нових List<something>
, тому вам доведеться вручну перетворювати речі назад у звичайний старийList
щоб взаємодіяти з ними. Це не така велика проблема, оскільки двійковий код C # 2.0 не є сумісним назад. Єдиний раз, коли це станеться, якщо ви модернізуєте старий код C # 1.0 / 1.1 до C # 2.0
ArrayList<Person> foo = new ArrayList<Person>();
На поверхні це виглядає так само, і це свого роду. Компілятор також не дозволить вам внести речі, які не входять Person
до списку.
Різниця в тому, що відбувається за лаштунками. На відміну від C #, Java не працює і не створює спеціального ListOfPerson
- вона просто використовує звичайний старий, ArrayList
який завжди був у Java. Коли ви виймаєте речі з масиву, звичайний Person p = (Person)foo.get(1);
кастинг-танець все-таки потрібно зробити. Компілятор економить ваші натискання клавіш, але швидкість удару / лиття все ще відбувається так, як це було завжди.
Коли люди згадують "Тип стирання", це те, про що вони говорять. Компілятор вставляє касти для вас, а потім 'стирає' той факт, що він мав бути списком Person
не простоObject
Перевага такого підходу полягає в тому, що старий код, який не розуміє дженерики, не повинен дбати. Це все ще стосується того самого старого, ArrayList
як завжди. Це важливіше у світі java, оскільки вони хотіли підтримати компіляцію коду за допомогою Java 5 з генерикою, а також запустити його на старих 1.4 або попередніх версіях JVM, з якими мікрософт навмисно вирішив не заважати.
Мінус - це швидкість, про яку я згадував раніше, а також тому, що її немає ListOfPerson
що в файлах .class псевдокласу чи чогось подібного, код, який переглядає його згодом (з відображенням, або якщо ви витягнете його з іншої колекції де це було перетворено на Object
тощо), жодним чином не може сказати, що це список, що містить тільки, Person
а не будь-який інший список масиву.
std::list<Person>* foo = new std::list<Person>();
Це схоже на дженерики C # та Java, і воно буде робити те, що, на вашу думку, повинно робити, але за лаштунками відбуваються різні речі.
Він має найбільш спільне з C # generics у тому, що він будує спеціальний pseudo-classes
а не просто викидає інформацію про тип, як це робить java, але це зовсім інший чайник з рибою.
І C #, і Java дають вихід, призначений для віртуальних машин. Якщо ви пишете якийсь код, у якому є Person
клас, в обох випадках деяку інформацію про aPerson
клас буде потрапляти у файл .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 ++ не страждає від жодної з цих проблем. Компілятор не піклується про передачу типів до будь-яких віртуальних машин - якщо обидва об'єкти мають функцію. Якщо вони цього не роблять, це не буде. Простий.
Отже, ось у вас це є :-)
int addNames<T>( T first, T second ) { return first + second; }
на C #. Загальний тип може бути обмежений класом замість інтерфейсу, і є спосіб оголосити клас з +
оператором у ньому.
C ++ рідко використовує термінологію "generics". Натомість слово "шаблони" використовується і є більш точним. Шаблони описують одну техніку досягнення загального дизайну.
Шаблони 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 ++ враховують це. Це дуже однакова відмінність, яку OOP робить між інтерфейсом та реалізацією з перевагою віртуальних методів.
Шаблони 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*.
Сам Андер Хейльсберг описав відмінності тут " Генерики в 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 # є тонкі відмінності: якби ці функції були частиною 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.
ArrayList<T>
може випромінюватися як новий внутрішньо названий тип із (прихованим) статичним Class<T>
полем. Поки нова версія generic lib буде розгорнута з кодом 1,5+ байтів, вона зможе працювати на 1,4-JVM.
Продовження моєї попередньої публікації.
Шаблони - одна з головних причин, чому C ++ не спрацьовує так безглуздо при інтеліссенсі, незалежно від використовуваної 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 існуючий Framework Collections був повністю узагальнений . У Java немає як загальної, так і застарілої негенеричної версії класів колекцій. У чомусь це набагато зрозуміліше - якщо вам потрібно використовувати колекцію в C #, насправді дуже мало підстав для того, щоб перейти з не загальною версією, але ці застарілі класи залишаються на місці, захаращуючи пейзаж.
Ще одна помітна відмінність - класи Enum на Java та C #. Java Enum має таке дещо покрутливе визначення:
// 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 # до введення генерики до мови, визначення не могло змінитися без порушення існуючого коду. Отже, як і колекції, він залишається в основних бібліотеках у цьому спадковому стані.
ArrayList
щоб List<T>
і помістити його в новий простір імен. Справа в тому, що якщо у вихідному коді з'явився клас, ArrayList<T>
він стане іменем класу, створеним компілятором, в коді IL, тому конфлікту імен не може статися.
Пізніше на 11 місяців, але я думаю, що це питання готове до деяких матеріалів Java Wildcard.
Це синтаксична особливість Java. Припустимо, у вас є метод:
public <T> void Foo(Collection<T> thing)
І припустимо, вам не потрібно посилатися на тип T в тілі методу. Ви оголошуєте ім’я T, а потім використовуєте його лише один раз, то чому б вам довелося думати ім’я для нього? Натомість ви можете написати:
public void Foo(Collection<?> thing)
Знак питання просить компілятор зробити вигляд, що ви оголосили нормальний параметр типу з назвою, який потрібно лише один раз відобразити в цьому місці.
З подвійними кодами ви нічого не можете зробити, що також не можна зробити з іменованим параметром типу (саме так це завжди робиться в C ++ і C #).
class Foo<T extends List<?>>
і використовувати, Foo<StringList>
але в C # ви повинні додати цей параметр додаткового типу: class Foo<T, T2> where T : IList<T2>
і використовувати незграбність Foo<StringList, String>
.
У Вікіпедії є великі записи, в яких порівнюються як Java / C # generics, так і Java generics / C ++ шаблони. Основна стаття на Дженерики здається трохи метушня , але у нього є деякі хороші дані в ньому.
Найбільша скарга - стирання типу. При цьому генеричні дані не застосовуються під час виконання. Ось посилання на деякі документи Sun на цю тему .
Загальна інформація реалізується за допомогою стирання типу: інформація про загальний тип присутня лише під час компіляції, після чого стирається компілятором.
Шаблони C ++ насправді набагато потужніші, ніж їх аналоги C # та Java, оскільки вони оцінюються під час компіляції та спеціалізації підтримки. Це дозволяє здійснювати метапрограмування шаблонів і робить компілятор C ++ еквівалентним машині Тьюрінга (тобто під час процесу компіляції ви можете обчислити все, що можна обчислити за допомогою машини Тьюрінга).
Схоже, серед інших дуже цікавих пропозицій є ще одна інформація про вдосконалення генерики та порушення зворотної сумісності:
В даний час дженерики реалізовані за допомогою стирання, що означає, що інформація про загальний тип недоступна під час виконання, що робить якийсь код важко записати. Generics були реалізовані таким чином, щоб підтримувати зворотну сумісність зі старими негенеричними кодами. Змінена дженерика зробить інформацію про загальний тип доступною під час виконання, що порушить застарілий не загальний код. Однак, Neal Gafter запропонував зробити типи, які можна повторно застосовувати, лише якщо зазначено, щоб не порушити зворотну сумісність.
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 повинна порушувати відсталу сумісність, щоб підтримувати справжні дженерики.
ArrayList<Foo>
те, що вона хоче перейти до більш старого методу, який повинен бути заповнений ArrayList
з екземплярами Foo
. Якщо ArrayList<foo>
це не так ArrayList
, як це зробити?