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_type
parsing为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。