C#和Java中的generics和C ++中的模板之间有什么区别?

我主要使用Java和generics相对较新。 我一直在阅读Java做出错误的决定,或.NET有更好的实现等。

那么,C ++,C#和Java在generics中的主要区别是什么呢? 每个人的优点/缺点?

我会把我的声音join噪音中,并采取刺探的方式来明确:

C#generics允许你声明这样的东西。

List<Person> foo = new List<Person>(); 

然后编译器会阻止你把不是Person东西放到列表中。
在幕后,C#编译器只是将List<Person>放入.NET dll文件中,但是在运行时,JIT编译器会创build一个新的代码集,就好像您已经编写了一个专门的包含人员的列表类一样 -像ListOfPerson

这样做的好处是它使它非常快速。 没有任何投射或任何其他的东西,因为该DLL包含的信息,这是一个人名单,其他代码,稍后使用reflection看它可以告诉它包含Person物件(所以你得到智能感知等)。

不足之处在于旧的C#1.0和1.1代码(在添加generics之前)不能理解这些新的List<something> ,因此您必须手动将事物转换回普通的旧List以与它们进行互操作。 这不是一个大问题,因为C#2.0二进制代码不是向后兼容的。 唯一会发生的事情是,如果你将一些旧的C#1.0 / 1.1代码升级到C#2.0

Javagenerics允许你声明这样的东西。

 ArrayList<Person> foo = new ArrayList<Person>(); 

在表面看起来是一样的,它是一样的。 编译器也会阻止你把不是Person东西放到列表中。

不同之处在于幕后发生的事情。 与C#不同的是,Java不会去构build一个特殊的ListOfPerson它只是使用一直在Java中的普通的旧ArrayList 。 当你从数组中取出东西时,通常Person p = (Person)foo.get(1); 还要继续铸造舞蹈。 编译器正在保存你的按键,但速度命中/铸造仍然像以前一样。
当人们提到“types擦除”时,这就是他们正在谈论的内容。 编译器会为你插入强制转换,然后“擦除”这个事实,即它是一个Person而不仅仅是Object

这种方法的好处是,不懂generics的旧代码不必关心。 它仍然处理和以前一样的旧ArrayList 。 这在Java世界中更为重要,因为他们希望支持使用带有generics的Java 5编译代码,并使其运行在旧的1.4或以前的JVM上,微软故意决定不去打扰。

缺点是我之前提到的速度,也因为没有ListOfPerson伪类或类似的东西进入.class文件,后来看它的代码(reflection,或者如果你把它从另一个集合已经被转换成Object等等)不能以任何方式告诉它它是一个只包含Person的列表,而不是任何其他的数组列表。

C ++模板允许你声明这样的东西

 std::list<Person>* foo = new std::list<Person>(); 

它看起来像C#和Java的generics,它会做你认为应该做的,但在幕后不同的事情正在发生。

它与C#generics最为相似,因为它构build了特殊的pseudo-classes而不是像Java一样抛出types信息,但它是一个完全不同的水壶。

C#和Java都产生了为虚拟机devise的输出。 如果你写了一些其中包含一个Person类的代码,在这两种情况下,有关Person类的一些信息将进入.dll或.class文件,JVM / CLR将完成这个任务。

C ++生成原始的x86二进制代码。 一切都不是一个对象,没有底层的虚拟机需要知道一个Person 。 没有拳击或拆箱,function不必属于类,甚至任何东西。

正因为如此,C ++编译器没有限制你可以对模板做什么 – 基本上你可以手动编写任何代码,你可以得到模板来为你写。
最明显的例子是添加东西:

在C#和Java中,generics系统需要知道哪些方法可用于某个类,并且需要将其传递给虚拟机。 要告诉它的唯一方法是对实际的类进行硬编码,或者使用接口。 例如:

 string addNames<T>( T first, T second ) { return first.Name() + second.Name(); } 

该代码不会在C#或Java中编译,因为它不知道typesT实际上提供了一个名为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 ++没有这些问题。 编译器不关心将types传递给任何虚拟机 – 如果两个对象都有一个.Name()函数,它将编译。 如果他们不这样做,它不会。 简单。

所以你有它 :-)

C ++很less使用“generics”术语。 相反,使用“模板”这个词并且更准确。 模板描述了一种实现通用devise的技术。

C ++模板与C#和Java实现的两个主要原因非常不同。 第一个原因是C ++模板不仅允许编译时types参数,而且还允许编译时常量值参数:模板可以作为整数或函数签名给出。 这意味着你可以在编译时做一些非常时髦的事情,例如计算:

 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 ++模板的其他特征,即模板特化。 代码定义了一个类模板,即具有一个值参数的产品。 它还定义了一个专用于该模板的参数,计算结果为1时使用。这使我可以定义一个recursion模板定义。 我相信这是Andrei Alexandrescu最先发现的。

模板专门化对于C ++来说很重要,因为它允许在数据结构上存在结构上的差异。 作为一个整体的模板是跨types统一接口的一种手段。 但是,尽pipe这是可取的,但在实施过程中不能一视同仁。 C ++模板考虑到了这一点。 这与OOP在接口和实现之间所做的与虚拟方法的重写非常相似。

C ++模板是其algorithm编程范例的关键。 例如,容器的几乎所有algorithm都被定义为接受容器types作为模板types的函数,并将其统一处理。 实际上,这并不正确:C ++不能在容器上工作,而是在由两个迭代器定义的范围上,指向容器的开始和结尾。 因此,整个内容由迭代器限定:begin <= elements <end。

使用迭代器代替容器是有用的,因为它允许在容器的部分而不是整体上运行。

C ++的另一个显着特点是类模板部分专业化的可能性。 这与Haskell和其他函数式语言中参数的模式匹配有些相关。 例如,让我们考虑一个存储元素的类:

 template <typename T> class Store { … }; // (1) 

这适用于任何元素types。 但是让我们说,通过应用一些特殊的技巧,我们可以比其他types更有效地存储指针。 我们可以通过对所有指针types进行部分专门化来实现:

 template <typename T> class Store<T*> { … }; // (2) 

现在,每当我们为一种types实例化一个容器模板时,就会使用适当的定义:

 Store<int> x; // Uses (1) Store<int*> y; // Uses (2) Store<string**> z; // Uses (2), with T = string*. 

Anders Hejlsberg本人在这里描述了“ C#,Java和C ++中的generics ”的区别。

关于这些差异,已经有很多很好的答案,所以让我给出一个稍微不同的观点,并补充原因

正如已经解释的那样,主要区别在于types擦除 ,即Java编译器擦除genericstypes,并且不会以生成的字节码结束。 但问题是:为什么有人会那样做? 这没有道理! 还是呢?

那么,有什么select? 如果你不用语言实现generics,你在哪里实现它们? 答案是:在虚拟机中。 这打破了向后兼容性。

另一方面,types擦除允许您将通用客户端与非通用库混合在一起。 换句话说:在Java 5上编译的代码仍然可以部署到Java 1.4。

不过,微软决定打破仿制药的向后兼容性。 这就是为什么.NETgenerics比Javagenerics“更好” 原因。

当然,太阳不是白痴或懦夫。 他们之所以“加紧”,是因为Java在引入generics时比.NET长得多,也比.NET更广泛。 (它们在两个世界中大致同时被引入)。打破向后兼容性将是一个巨大的痛苦。

换句话说,在Java中,generics是语言的一部分(这意味着它们适用于Java,而不适用于其他语言),在.NET中它们是虚拟机的一部分(这意味着它们适用于所有语言,只是C#和Visual Basic.NET)。

将其与.NETfunction(如LINQ,lambdaexpression式,局部variablestypes推断,匿名types和expression式树)进行比较:这些都是语言function。 这就是为什么VB.NET和C#之间存在细微差别的原因:如果这些function是VM的一部分,它们在所有语言中都是一样的。 但CLR并没有改变:.NET 3.5 SP1和.NET 2.0一样。 如果您不使用任何.NET 3.5库,您可以编译一个使用.NET 3.5编译器的LINQ的C#程序,并仍然在.NET 2.0上运行它。 这不适用于generics和.NET 1.1,但它可以与Java和Java 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来说,在这个时候,如果和成员之间有什么联系,那该死的很难。 对于其他语言,parsing将是直接的,但对于C ++来说,需要相当多的评估。

它变得更糟。 如果my_int_type是在类模板中定义的呢? 现在它的types将取决于另一个types的参数。 而在这里,甚至编译器都会失败。

 template <typename T> struct Y { typedef T my_type; }; X<Y<int>::my_type> b; 

经过一番思考,程序员会得出结论:这个代码和上面一样: Y<int>::my_typeparsing为int ,所以b应该和a相同,对不对?

错误。 在编译器试图parsing这个语句的地方,它实际上并不知道Y<int>::my_type ! 因此,它不知道这是一种types。 它可能是别的东西,例如成员函数或字段。 这可能会引起歧义(尽pipe不是在本例中),因此编译器失败。 我们必须明确地告诉它,我们引用一个types名称:

 X<typename Y<int>::my_type> b; 

现在,代码编译。 要了解这种情况如何产生歧义,请考虑以下代码:

 Y<int>::my_type(123); 

这段代码语句非常有效,它告诉C ++执行对Y<int>::my_type的函数调用。 但是,如果my_type不是一个函数,而是一个types,那么这个语句仍然是有效的,并执行一个特殊的转换(函数式转换),这通常是一个构造函数调用。 编译器不能说出我们的意思,所以我们必须在这里消除歧义。

Java和C#在第一次发布语言之后引入了generics。 然而,在引入generics时,核心库的变化是有区别的。 C#的generics不仅仅是编译器的魔力 ,所以在不破坏向后兼容性的情况下,不可能生成现有的库类。

例如,在Java中现有的集合框架是完全通用的Java不具有generics和传统非generics版本的集合类。 从某种意义上讲,这样做更简洁 – 如果您需要在C#中使用集合,那么使用非通用版本的原因实在是太less了,但是这些遗留类仍然存在,使得整个环境变得混乱。

另一个显着的区别是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可以从stringtypes安全地访问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#中,所以在不破坏现有代码的情况下定义不会改变。 所以,就像collections一样,它仍然处于这个遗留状态的核心图书馆。

11个月后,但我认为这个问题已经准备好了一些Java通配符的东西。

这是Java的一个语法特征。 假设你有一个方法:

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

假设你不需要在方法体中引用typesT. 你声明了一个名字T,然后只使用它一次,那为什么还要为它想一个名字呢? 相反,你可以写:

 public void Foo(Collection<?> thing) 

问号要求编译器假装声明一个正常的命名types参数,只需要在该位置出现一次即可。

没有办法用通配符来做,而且你不能用命名的types参数来做这些事情(这些事情总是用C ++和C#来完成的)。

维基百科对Java / C#generics和Javagenerics/ C ++模板进行了比较。 关于generics的主要文章看起来有点混乱,但它确实有一些很好的信息。

最大的抱怨是types擦除。 在那里,generics不是在运行时强制的。 这里有一些关于这个主题的Sun文档的链接 。

generics是通过types擦除来实现的:genericstypes信息仅在编译时出现,之后被编译器擦除。

C ++模板实际上比C#和Java的模板更强大,因为它们在编译时被评估并支持专业化。 这允许模板元编程,并使C ++编译器等同于图灵机(即在编译过程中,您可以计算任何可以用图灵机计算的东西)。

在Java中,generics只是编译器级别,所以你得到:

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

请注意,“a”的types是数组列表,而不是string列表。 所以,香蕉列表的types将等于()一个猴子列表。

可以这么说。

看起来,除了其他非常有趣的build议之外,还有一个关于提炼generics并打破向后兼容性的build议:

目前,generics是使用擦除来实现的,这意味着genericstypes信息在运行时不可用,这使得某种代码难以写入。 generics以这种方式实现,以支持向后兼容旧的非generics代码。 泛化generics将在运行时使genericstypes信息可用,这将打破传统的非generics代码。 但是,Neal Gafter提出只有在指定的情况下才可以确定types,从而不会破坏后向兼容性。

在亚历克斯米勒关于Java 7提案的文章中

注意:我没有足够的评论意见,所以请随意将其作为评论予以适当的回答。

与stream行的相信,我从来不知道它来自哪里,.net实现了真正的generics,而不会破坏后向兼容性,并且为此付出了明确的努力。 您不必将非generics.net 1.0代码更改为仅用于.net 2.0中的generics。 generics列表和非generics列表在.NET Framework 2.0中仍然可用,直到4.0,除了向后兼容的原因外,其它都不是。 因此,仍然使用非genericsArrayList的旧代码仍然可以工作,并使用与之前相同的ArrayList类。 后向代码的兼容性始终保持从1.0到现在…因此,即使在.net 4.0中,如果您select这样做,您仍然必须select使用1.0 BCL中的任何非generics类。

所以我不认为java必须打破向后兼容性来支持真正的generics。