Odpowiedzi:
Dodam mój głos do hałasu i postaram się wyjaśnić:
List<Person> foo = new List<Person>();
a wtedy kompilator zapobiegnie umieszczaniu rzeczy, których nie ma Person
na liście.
Za kulisami kompilator C # po prostu umieszcza List<Person>
w pliku dll .NET, ale w czasie wykonywania kompilator JIT idzie i buduje nowy zestaw kodu, tak jakbyś napisał specjalną klasę list tylko do przechowywania ludzi - coś w rodzaju ListOfPerson
.
Zaletą tego jest to, że sprawia, że jest naprawdę szybki. Nie ma rzutowania ani żadnych innych rzeczy, a ponieważ dll zawiera informacje, że jest to lista Person
, inny kod, który patrzy na nią później przy użyciu refleksji, może stwierdzić, że zawiera Person
obiekty (więc otrzymujesz inteligencję i tak dalej).
Minusem tego jest to, że stary kod C # 1.0 i 1.1 (przed dodaniem generycznych) nie rozumie tych nowych List<something>
, więc musisz ręcznie przekonwertować rzeczy z powrotem na zwykły stary, List
aby z nimi współpracować. Nie stanowi to większego problemu, ponieważ kod binarny C # 2.0 nie jest kompatybilny wstecz. Jedyny raz to się stanie, jeśli zaktualizujesz stary kod C # 1.0 / 1.1 do C # 2.0
ArrayList<Person> foo = new ArrayList<Person>();
Na powierzchni wygląda tak samo i jest w pewnym sensie. Kompilator zapobiegnie również umieszczaniu rzeczy, których nie ma Person
na liście.
Różnica polega na tym, co dzieje się za kulisami. W przeciwieństwie do C #, Java nie buduje specjalnej wersji ListOfPerson
- po prostu używa zwykłego starego, ArrayList
który zawsze był w Javie. Kiedy wyciągniesz rzeczy z szeregu, Person p = (Person)foo.get(1);
nadal musisz wykonać zwykły taniec castingowy. Kompilator oszczędza ci naciskania klawiszy, ale szybkość trafienia / rzucania jest wciąż zwiększana, jak zawsze.
Kiedy ludzie wspominają o „skasowaniu typu”, właśnie o tym mówią. Kompilator wstawia dla Ciebie rzutowania, a następnie „kasuje” fakt, że ma to być lista Person
nie tylkoObject
Zaletą tego podejścia jest to, że stary kod, który nie rozumie generycznych, nie musi się tym przejmować. Nadal ma do czynienia z tym samym starym, ArrayList
co zawsze. Jest to ważniejsze w świecie Java, ponieważ chcieli obsługiwać kompilowanie kodu przy użyciu języka Java 5 z ogólnymi wersjami i uruchamianie go na starej wersji JVM 1.4 lub wcześniejszej, z którą firma Microsoft celowo postanowiła nie zawracać sobie głowy.
Minusem jest szybkość, o której wspominałem wcześniej, a także dlatego, że nie ma ListOfPerson
pseudoklasy lub czegoś podobnego wchodzącego do plików .class, kodu, który patrzy na to później (z refleksją lub jeśli wyciągniesz go z innej kolekcji gdzie został przekształcony Object
itp.) nie może w żaden sposób powiedzieć, że ma to być lista zawierająca tylko, Person
a nie tylko dowolną inną listę tablic.
std::list<Person>* foo = new std::list<Person>();
Wygląda jak C # i Java ogólne i zrobi to, co według ciebie powinno być, ale za kulisami dzieją się różne rzeczy.
Ma to najwięcej wspólnego z generycznymi C #, ponieważ tworzy specjalne, pseudo-classes
a nie tylko wyrzuca informacje o typie, jak java, ale jest to zupełnie inny czajnik ryb.
Zarówno C #, jak i Java produkują dane wyjściowe przeznaczone dla maszyn wirtualnych. Jeśli napiszesz kod zawierający Person
klasę, w obu przypadkach pewne informacje o Person
klasie przejdą do pliku .dll lub .class, a JVM / CLR zrobi z tym różne rzeczy.
C ++ produkuje surowy kod binarny x86. Wszystko nie jest przedmiotem i nie ma żadnej maszyny wirtualnej, która musiałaby wiedzieć o Person
klasie. Nie ma żadnego boksowania ani rozpakowywania, a funkcje nie muszą należeć do klas ani niczego.
Z tego powodu kompilator C ++ nie nakłada żadnych ograniczeń na to, co możesz zrobić z szablonami - w zasadzie każdy kod, który możesz napisać ręcznie, możesz uzyskać szablony do napisania dla ciebie.
Najbardziej oczywistym przykładem jest dodawanie rzeczy:
W języku C # i Javie system ogólny musi wiedzieć, jakie metody są dostępne dla klasy, i musi przekazać to do maszyny wirtualnej. Jedynym sposobem, aby to powiedzieć, jest albo na stałe zakodować rzeczywistą klasę, albo przy użyciu interfejsów. Na przykład:
string addNames<T>( T first, T second ) { return first.Name() + second.Name(); }
Ten kod nie będzie się kompilował w języku C # ani Javie, ponieważ nie wie, że ten typ T
faktycznie udostępnia metodę o nazwie Name (). Musisz to powiedzieć - w C # jak to:
interface IHasName{ string Name(); };
string addNames<T>( T first, T second ) where T : IHasName { .... }
A potem musisz się upewnić, że to, co przekazujesz do addNames, implementuje interfejs IHasName i tak dalej. Składnia Java jest inna ( <T extends IHasName>
), ale ma te same problemy.
„Klasycznym” przypadkiem tego problemu jest próba napisania funkcji, która to robi
string addNames<T>( T first, T second ) { return first + second; }
Nie można tak naprawdę napisać tego kodu, ponieważ nie ma możliwości zadeklarowania interfejsu za pomocą +
zawartej w nim metody. Przegrywasz
C ++ nie cierpi na żaden z tych problemów. Kompilator nie dba o przekazywanie typów do żadnych maszyn wirtualnych - jeśli oba obiekty mają funkcję .Name (), skompiluje się. Jeśli nie, to nie będzie. Prosty.
Więc masz to :-)
int addNames<T>( T first, T second ) { return first + second; }
w języku C #. Typ ogólny może być ograniczony do klasy zamiast interfejsu, a istnieje sposób zadeklarowania klasy za pomocą +
operatora.
C ++ rzadko używa terminologii „ogólna”. Zamiast tego używa się słowa „szablony” i jest ono bardziej dokładne. Szablony opisują jedną technikę pozwalającą uzyskać ogólny projekt.
Szablony C ++ bardzo różnią się od tego, co implementują zarówno C #, jak i Java z dwóch głównych powodów. Pierwszym powodem jest to, że szablony C ++ nie tylko dopuszczają argumenty typu czas kompilacji, ale także argumenty stałej wartości kompilacji: szablony mogą być podawane jako liczby całkowite, a nawet podpisy funkcji. Oznacza to, że w czasie kompilacji możesz wykonywać dość funky, np. Obliczenia:
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;
Ten kod wykorzystuje również inną wyróżniającą się cechę szablonów C ++, a mianowicie specjalizację szablonów. Kod definiuje jeden szablon klasy, product
który ma jeden argument wartości. Definiuje także specjalizację dla tego szablonu, która jest używana za każdym razem, gdy argument ma wartość 1. Pozwala mi to zdefiniować rekurencję w stosunku do definicji szablonu. Wierzę, że po raz pierwszy odkrył to Andrei Alexandrescu .
Specjalizacja szablonów jest ważna dla C ++, ponieważ pozwala na różnice strukturalne w strukturach danych. Szablony jako całość to sposób na ujednolicenie interfejsu między typami. Jednak, chociaż jest to pożądane, wszystkie typy nie mogą być traktowane jednakowo w ramach implementacji. Szablony C ++ uwzględniają to. Jest to ta sama różnica, jaką OOP robi między interfejsem a implementacją z przesłonięciem metod wirtualnych.
Szablony C ++ są niezbędne dla paradygmatu programowania algorytmicznego. Na przykład prawie wszystkie algorytmy dla kontenerów są zdefiniowane jako funkcje, które akceptują typ kontenera jako typ szablonu i traktują je jednolicie. W rzeczywistości nie jest to w porządku: C ++ nie działa na kontenerach, ale raczej na zakresach zdefiniowanych przez dwa iteratory, wskazujących początek i koniec kontenera. Zatem cała treść jest ograniczona przez iteratory: początek <= elementy <koniec.
Korzystanie z iteratorów zamiast kontenerów jest przydatne, ponieważ pozwala operować na częściach kontenera zamiast na całości.
Kolejną cechą wyróżniającą C ++ jest możliwość częściowej specjalizacji szablonów klas. Jest to nieco związane z dopasowaniem wzorca argumentów w języku Haskell i innych językach funkcjonalnych. Rozważmy na przykład klasę, która przechowuje elementy:
template <typename T>
class Store { … }; // (1)
Działa to dla każdego typu elementu. Powiedzmy jednak, że możemy przechowywać wskaźniki bardziej efektywnie niż inne typy, stosując specjalną sztuczkę. Możemy to zrobić częściowo specjalizując się we wszystkich typach wskaźników:
template <typename T>
class Store<T*> { … }; // (2)
Teraz, ilekroć wystąpimy szablon kontenera dla jednego typu, używana jest odpowiednia definicja:
Store<int> x; // Uses (1)
Store<int*> y; // Uses (2)
Store<string**> z; // Uses (2), with T = string*.
Anders Hejlsberg sam opisał tutaj różnice „ Generics w C #, Java i C ++ ”.
Istnieje już wiele pozytywnych odpowiedzi na jakie różnice są, więc podam nieco innej perspektywy i dodać dlaczego .
Jak już wyjaśniono, główną różnicą jest usuwanie typu , tj. Fakt, że kompilator Java usuwa typy ogólne i nie kończą się one w wygenerowanym kodzie bajtowym. Pytanie jednak brzmi: dlaczego ktokolwiek miałby to zrobić? To nie ma sensu! A może to?
Jaka jest alternatywa? Jeśli nie zaimplementować rodzajowych w języku, w którym należy je wdrożyć? Odpowiedź brzmi: w maszynie wirtualnej. Co psuje kompatybilność wsteczną.
Z drugiej strony, usuwanie typu pozwala mieszać ogólnych klientów z bibliotekami ogólnymi. Innymi słowy: kod skompilowany na Javie 5 nadal można wdrożyć w Javie 1.4.
Microsoft postanowił jednak zerwać wsteczną kompatybilność dla leków generycznych. To dlaczego .NET Generics są „lepsze” niż Java rodzajowych.
Oczywiście, że Słońce nie jest idiotą ani tchórzem. Powodem, dla którego „stchórzyli”, było to, że Java była znacznie starsza i bardziej rozpowszechniona niż .NET, kiedy wprowadzono generyczne. (Zostały wprowadzone mniej więcej w tym samym czasie w obu światach.) Zerwanie wstecznej kompatybilności byłoby ogromnym bólem.
Mówiąc jeszcze inaczej: w Javie, Generics są częścią Języka (co oznacza, że dotyczą tylko Javy, a nie innych języków), w .NET są częścią Maszyny Wirtualnej (co oznacza, że dotyczą wszystkich języków, a nie tylko C # i Visual Basic.NET).
Porównaj to z funkcjami .NET, takimi jak LINQ, wyrażenia lambda, lokalne wnioskowanie o typach zmiennych, typy anonimowe i drzewa wyrażeń: wszystkie są funkcjami językowymi . Właśnie dlatego istnieją subtelne różnice między VB.NET i C #: gdyby te funkcje były częścią maszyny wirtualnej, byłyby takie same we wszystkich językach. Ale CLR się nie zmienił: nadal jest taki sam w .NET 3.5 SP1 jak w .NET 2.0. Możesz skompilować program w języku C #, który używa LINQ z kompilatorem .NET 3.5 i nadal uruchamiać go w .NET 2.0, pod warunkiem, że nie używasz żadnych bibliotek .NET 3.5. Że nie praca z rodzajowych i .NET 1.1, ale to będzie działać z Java i Java 1.4.
ArrayList<T>
może być emitowany jako nowy wewnętrznie nazwany typ z (ukrytym) Class<T>
polem statycznym . Tak długo, jak nowa wersja ogólnej biblioteki lib zostanie wdrożona z kodem bajtu 1.5+, będzie mogła działać na JVM 1.4-JVM.
Kontynuacja mojego poprzedniego postu.
Szablony są jednym z głównych powodów, dla których C ++ zawodzi tak beznadziejnie w intellisense, niezależnie od używanego IDE. Ze względu na specjalizację szablonów IDE nigdy nie może być naprawdę pewien, czy dany element istnieje, czy nie. Rozważać:
template <typename T>
struct X {
void foo() { }
};
template <>
struct X<int> { };
typedef int my_int_type;
X<my_int_type> a;
a.|
Teraz kursor znajduje się we wskazanej pozycji i IDE w tym momencie trudno powiedzieć, czy i co a
mają członkowie . W przypadku innych języków parsowanie byłoby proste, ale w przypadku C ++ potrzebna jest wcześniej sporo oceny.
Pogarsza się. Co jeśli my_int_type
zostałyby zdefiniowane również w szablonie klasy? Teraz jego typ będzie zależał od innego argumentu typu. I tutaj zawodzą nawet kompilatory.
template <typename T>
struct Y {
typedef T my_type;
};
X<Y<int>::my_type> b;
Po krótkiej refleksji programista doszedł do wniosku, że ten kod jest taki sam jak powyżej: Y<int>::my_type
postanawia int
, dlatego b
powinien być tego samego typu co a
, prawda?
Źle. W momencie, gdy kompilator próbuje rozwiązać tę instrukcję, tak naprawdę Y<int>::my_type
jeszcze nie wie ! Dlatego nie wie, że jest to typ. Może to być coś innego, np. Funkcja członka lub pole. Może to powodować niejasności (choć nie w tym przypadku), dlatego kompilator zawiedzie. Musimy wyraźnie powiedzieć, że odwołujemy się do nazwy typu:
X<typename Y<int>::my_type> b;
Teraz kod się kompiluje. Aby zobaczyć, jak powstają niejasności w tej sytuacji, rozważ następujący kod:
Y<int>::my_type(123);
Ta instrukcja kodu jest całkowicie poprawna i mówi C ++, aby wykonała wywołanie funkcji Y<int>::my_type
. Jeśli jednak my_type
nie jest funkcją, ale typem, to instrukcja nadal byłaby ważna i wykonałaby rzut specjalny (rzutowanie w stylu funkcji), który często jest wywołaniem konstruktora. Kompilator nie może powiedzieć, co mamy na myśli, więc musimy tutaj ujednoznacznić.
Zarówno Java, jak i C # wprowadziły generyczne po ich pierwszej wersji językowej. Istnieją jednak różnice w tym, jak zmieniły się biblioteki podstawowe, gdy wprowadzono generyczne. Generyczne C # to nie tylko magia kompilatora, więc nie było możliwe wygenerowanie istniejących klas bibliotek bez naruszenia kompatybilności wstecznej.
Na przykład w Javie istniejący Framework Kolekcje został całkowicie uogólniony . Java nie ma zarówno ogólnej, jak i starszej nie-ogólnej wersji klas kolekcji. Pod pewnymi względami jest to o wiele czystsze - jeśli chcesz użyć kolekcji w języku C #, naprawdę nie ma powodu, aby iść z wersją inną niż ogólna, ale te starsze klasy pozostają na miejscu, zagracając krajobraz.
Kolejną zauważalną różnicą są klasy Enum w Javie i C #. Enum Javy ma tę nieco zawiłą definicję:
// java.lang.Enum Definition in Java
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
(zobacz bardzo jasne wyjaśnienie Angeliki Langer , dlaczego tak jest. Zasadniczo oznacza to, że Java może zapewnić typowi bezpieczny dostęp od łańcucha do wartości Enum:
// Parsing String to Enum in Java
Colour colour = Colour.valueOf("RED");
Porównaj to z wersją C #:
// Parsing String to Enum in C#
Colour colour = (Colour)Enum.Parse(typeof(Colour), "RED");
Ponieważ Enum istniało już w języku C # przed wprowadzeniem do języka generics, definicja nie mogła się zmienić bez zerwania istniejącego kodu. Podobnie jak kolekcje, pozostaje on w podstawowych bibliotekach w tym starszym stanie.
ArrayList
na List<T>
i umieścić go w nowej przestrzeni nazw. Faktem jest, że jeśli w kodzie źródłowym pojawi się klasa, ponieważ ArrayList<T>
będzie ona inną nazwą klasy generowanej przez kompilator w kodzie IL, więc nie może wystąpić konflikt nazw.
11 miesięcy spóźnienia, ale myślę, że to pytanie jest gotowe na niektóre elementy Java Wildcard.
Jest to składniowa funkcja Java. Załóżmy, że masz metodę:
public <T> void Foo(Collection<T> thing)
Załóżmy, że nie musisz odwoływać się do typu T w treści metody. Podajesz nazwę T, a następnie używasz jej tylko raz, więc dlaczego miałbyś wymyślić jej nazwę? Zamiast tego możesz napisać:
public void Foo(Collection<?> thing)
Znak zapytania prosi kompilator o udawanie, że zadeklarowałeś normalny parametr nazwanego typu, który musi pojawić się tylko raz w tym miejscu.
Nic nie można zrobić z symbolami wieloznacznymi, czego nie można zrobić również z parametrem nazwanego typu (tak właśnie dzieje się w C ++ i C #).
class Foo<T extends List<?>>
i użyć, Foo<StringList>
ale w C # musisz dodać ten dodatkowy parametr typu: class Foo<T, T2> where T : IList<T2>
i użyć niezgrabnego Foo<StringList, String>
.
Wikipedia ma świetne opisy porównujące zarówno szablony Java / C # generics, jak i Java generics / C ++ . Główny artykuł na Generics wydaje się nieco zagracone ale ma jakieś dobre informacje w nim.
Największą skargą jest usunięcie typu. W związku z tym generyczne nie są wymuszane w czasie wykonywania. Oto link do niektórych dokumentów firmy Sun na ten temat .
Generyczne są implementowane przez kasowanie typu: ogólne informacje o typie są obecne tylko w czasie kompilacji, po czym są usuwane przez kompilator.
Szablony C ++ są w rzeczywistości znacznie potężniejsze niż ich odpowiedniki w języku C # i Java, ponieważ są oceniane podczas kompilacji i obsługują specjalizację. Pozwala to na Meta-Programowanie Szablonów i sprawia, że kompilator C ++ jest równoważny maszynie Turinga (tzn. Podczas procesu kompilacji można obliczyć wszystko, co można obliczyć za pomocą maszyny Turinga).
Wygląda na to, że pośród innych bardzo interesujących propozycji jest jedna dotycząca udoskonalenia generycznych i zerwania z kompatybilnością wsteczną:
Obecnie generyczne są implementowane przy użyciu skasowania, co oznacza, że ogólne informacje o typie nie są dostępne w czasie wykonywania, co utrudnia napisanie pewnego rodzaju kodu. Generyczne zostały zaimplementowane w ten sposób, aby wspierać wsteczną kompatybilność ze starszym nie-ogólnym kodem. Reified generics udostępniłby informacje o typie ogólnym w czasie wykonywania, co złamałoby starszy nie-ogólny kod. Jednak Neal Gafter zaproponował, aby typy były możliwe do ponownego uruchomienia tylko wtedy, gdy jest to określone, aby nie naruszyć kompatybilności wstecznej.
Uwaga: Nie mam wystarczającego sensu, aby komentować, więc możesz przesłać to jako komentarz do odpowiedniej odpowiedzi.
Wbrew powszechnemu przekonaniu, którego nigdy nie rozumiem, skąd się wziął. Net zaimplementował prawdziwe generyczne produkty bez naruszania wstecznej kompatybilności i dołożył do tego wyraźnego wysiłku. Nie musisz zamieniać nieogólnego kodu .net 1.0 na ogólne, aby używać go w .net 2.0. Zarówno lista ogólna, jak i ogólna są nadal dostępne w .Net Framework 2.0, nawet do wersji 4.0, właśnie z tego powodu, tylko ze względu na kompatybilność wsteczną. Dlatego stare kody, które nadal używały nietypowej ArrayList, będą nadal działać i będą używać tej samej klasy ArrayList, jak poprzednio. Kompatybilność z kodem wstecznym jest zawsze utrzymywana od wersji 1.0 do teraz ... Więc nawet w .net 4.0 nadal musisz wybrać dowolną klasę inną niż ogólna od 1.0 BCL, jeśli zdecydujesz się to zrobić.
Więc nie sądzę, że java musi zerwać wsteczną kompatybilność, aby obsługiwać prawdziwe generyczne.
ArrayList<Foo>
który chce przekazać starszej metodzie, która ma wypełnić ArrayList
instancje Foo
. Jeśli ArrayList<foo>
nie jest ArrayList
, jak to działa?