나는 주로 Java를 사용하고 제네릭은 비교적 새롭습니다. Java가 잘못된 결정을 내렸거나 .NET이 더 나은 구현 등을 가지고 있음을 계속 읽습니다.
그렇다면 제네릭에서 C ++, C #, Java의 주요 차이점은 무엇입니까? 각각의 장단점?
답변
나는 소리에 목소리를 더하고 일을 명확하게하는 데 찌를 것이다 :
C # Generics를 사용하면 이와 같은 것을 선언 할 수 있습니다.
List<Person> foo = new List<Person>();
그런 다음 컴파일러는 Person
목록에 없는 것을 넣지 못하게 합니다.
뒤에서 C # 컴파일러는 List<Person>
.NET dll 파일에 넣지 만 런타임에 JIT 컴파일러는 마치 사람들을 포함하기 위해 특별한 목록 클래스를 작성한 것처럼 새로운 코드 세트를 작성하고 빌드합니다 ListOfPerson
.
이것의 장점은 정말 빠르다는 것입니다. 캐스팅이나 다른 것들이 없으며 dll에 이것이 List of라는 정보가 포함되어 있기 때문에 Person
나중에 리플렉션을 사용하여 그것을 보는 다른 코드는 Person
객체 가 포함되어 있음을 알 수 있습니다 (따라서 인텔리전스 등을 얻습니다).
이것의 단점은 오래된 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에서 제네릭 시스템은 클래스에 사용할 수있는 메소드를 알아야하며이를 가상 머신으로 전달해야합니다. 이를 알 수있는 유일한 방법은 실제 클래스를 하드 코딩하거나 인터페이스를 사용하는 것입니다. 예를 들면 다음과 같습니다.
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 인터페이스 등을 구현하는지 확인해야합니다. 자바 문법이 다릅니다 (<T extends IHasName>
) 동일한 문제가 발생합니다.
이 문제에 대한 ‘고전적인’사례는 이것을하는 함수를 작성하려고합니다.
string addNames<T>( T first, T second ) { return first + second; }
인터페이스를 선언 할 수있는 방법이 없기 때문에 실제로이 코드를 작성할 수 없습니다 +
메소드를 . 당신은 실패합니다.
C ++에는 이러한 문제가 없습니다. 컴파일러는 유형을 VM으로 전달하는 것을 신경 쓰지 않습니다. 두 객체 모두에 .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로 평가 될 때마다 사용되는 해당 템플리트의 전문화도 정의합니다.이를 통해 템플리트 정의에 대한 재귀를 정의 할 수 있습니다. 나는 이것이 Andrei Alexandrescu에 의해 처음 발견되었다고 믿는다. .
템플릿 구조화는 데이터 구조에서 구조적 차이를 허용하므로 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*.
답변
Anders Hejlsberg 자신도 여기서 ” C #, Java 및 C ++의 제네릭 “의 차이점을 설명했습니다 .
답변
차이점이 무엇인지에 대한 좋은 대답이 이미 많으 므로 약간 다른 관점을 제시하고 이유를 추가하겠습니다 .
이미 설명했듯이, 주요 차이점은 유형 삭제입니다 . 즉, Java 컴파일러가 일반 유형을 지우고 생성 된 바이트 코드로 끝나지 않는다는 사실입니다. 그러나 문제는 왜 누군가 그렇게 할 것입니까? 말이되지 않습니다! 아니면?
대체 대안은 무엇입니까? 당신이 언어의 제네릭을 구현하지 않는 경우, 어디 않습니다 당신이 그들을 구현? 그리고 그 대답은 가상 머신입니다. 이전 버전과의 호환성을 손상시킵니다.
반면에 유형 삭제를 사용하면 일반 클라이언트를 비 일반 라이브러리와 혼합 할 수 있습니다. 즉, Java 5에서 컴파일 된 코드는 여전히 Java 1.4에 배포 할 수 있습니다.
그러나 Microsoft는 제네릭의 이전 버전과의 호환성을 중단하기로 결정했습니다. 사용자들은 .NET 제네릭 자바 제네릭보다 “더 나은”왜.
물론 썬은 바보 나 겁쟁이가 아닙니다. 그들이 “축소”한 이유는 Java가 제네릭을 도입했을 때 .NET보다 훨씬 오래되고 더 널리 퍼져 있기 때문입니다. (두 세계에서 거의 동시에 도입되었습니다.) 이전 버전과의 호환성을 깨는 것은 큰 고통이었습니다.
또 다른 방법으로 말하면 Java에서 Generics는 언어 의 일부이며 ( 다른 언어가 아닌 Java 에만 적용됨 ) .NET에서 가상 컴퓨터의 일부입니다 (즉 , 모든 언어에 적용되며 C # 및 Visual Basic.NET).
이것을 LINQ, 람다 식, 로컬 변수 형식 유추, 익명 형식 및 식 트리와 같은 .NET 기능과 비교하십시오. 이들은 모두 언어 기능입니다. 그렇기 때문에 VB.NET과 C #간에 미묘한 차이점이 있습니다. 이러한 기능이 VM의 일부인 경우 모든 언어 에서 동일 합니다. 그러나 CLR은 변경되지 않았습니다. .NET 3.5 SP1의 .NET 2.0과 동일합니다. .NET 3.5 라이브러리와 함께 LINQ를 사용하는 C # 프로그램을 컴파일하고 .NET 3.5 라이브러리를 사용하지 않는 경우 .NET 2.0에서 계속 실행할 수 있습니다. 즉 것 없는 제네릭 및 .NET 1.1과 작동하지만 그것은 것입니다 자바와 자바 1.4와 함께 작동합니다.
답변
이전 게시물에 대한 후속 조치.
템플릿은 사용 된 IDE에 관계없이 인텔리전스에서 C ++가 심하게 실패하는 주된 이유 중 하나입니다. 템플릿 전문화로 인해 특정 멤버가 존재하는지 여부를 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 #의 제네릭은 컴파일러 마법되지 않습니다 할 수 없었습니다 그래서 generify 이전 버전과의 호환성을 깨지 않고 기존 라이브러리 클래스를.
예를 들어 Java에서 기존 Collections Framework 는 완전히 일반화되었습니다 . Java에는 일반 및 레거시 비 제네릭 버전의 컬렉션 클래스가 없습니다. 어떤면에서 이것은 훨씬 깨끗합니다. C #에서 컬렉션을 사용해야하는 경우 비 일반 버전을 사용해야 할 이유는 거의 없지만 레거시 클래스는 그대로 남아있어 풍경을 어지럽 힙니다.
또 다른 눈에 띄는 차이점은 Java 및 C #의 Enum 클래스입니다. Java의 Enum은 다음과 같이 다소 구불 구불 한 정의를 가지고 있습니다.
// java.lang.Enum Definition in Java
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
( 이것이 왜 그런지에 대한 Angelika Langer의 매우 명확한 설명을 참조하십시오 . 본질적으로 이것은 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");
generics가 언어에 도입되기 전에 Enum이 C #에 이미 존재했기 때문에 기존 코드를 손상시키지 않고 정의를 변경할 수 없었습니다. 따라서 컬렉션과 마찬가지로이 레거시 상태의 핵심 라이브러리에 남아 있습니다.
답변
11 개월 늦었지만이 질문은 일부 Java Wildcard에 대한 준비가 된 것 같습니다.
이것은 Java의 구문 기능입니다. 방법이 있다고 가정하십시오.
public <T> void Foo(Collection<T> thing)
그리고 메소드 본문에서 유형 T를 참조 할 필요가 없다고 가정하십시오. 이름 T를 선언 한 다음 한 번만 사용하므로 이름을 생각해야하는 이유는 무엇입니까? 대신 다음과 같이 쓸 수 있습니다.
public void Foo(Collection<?> thing)
물음표는 컴파일러가 해당 지점에서 한 번만 나타나야하는 일반적인 명명 된 형식 매개 변수를 선언 한 것처럼 가장하도록 요청합니다.
와일드 카드로 수행 할 수있는 작업은 없습니다. 명명 된 형식 매개 변수로는 수행 할 수 없습니다 (이는 C ++ 및 C #에서 항상 수행되는 방식입니다).