[java] C # 및 Java의 제네릭과 C ++의 템플릿의 차이점은 무엇입니까? [닫은]

나는 주로 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-classesJava와 같이 유형 정보를 버리는 대신 특수하게 빌드한다는 점 에서 완전히 다른 주전자입니다.

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 #에서 항상 수행되는 방식입니다).