我主要使用Java和泛型相对较新.我一直在阅读Java做出错误决定或者.NET有更好的实现等.
那么,泛型中C++,C#,Java之间的主要区别是什么?每个人的利弊?
我会把声音添加到噪音中,然后把它弄清楚:
Listfoo = new List ();
然后编译器将阻止您放入不在Person
列表中的内容.
在幕后,C#编译器只是放入List
.NET dll文件,但在运行时,JIT编译器会编写一组新的代码,就好像你已经编写了一个特殊的列表类,只是为了包含人员 - 就像ListOfPerson
.
这样做的好处是它非常快.没有任何转换或任何其他东西,并且因为dll包含这是List的信息,Person
稍后使用反射查看它的其他代码可以告诉它包含Person
对象(因此你得到intellisense等等).
这样做的缺点是旧的C#1.0和1.1代码(在他们添加泛型之前)并不了解这些新内容List
,因此您必须手动将事物转换回原来List
与之互操作.这不是一个大问题,因为C#2.0二进制代码不向后兼容.唯一一次,如果你将一些旧的C#1.0/1.1代码升级到C#2.0
ArrayListfoo = new ArrayList ();
从表面上看,它看起来是一样的,它的类型是.编译器还会阻止您放入不在Person
列表中的内容.
不同之处在于幕后发生的事情.与C#不同,Java不会构建一个特殊的东西ListOfPerson
- 它只使用ArrayList
一直使用Java 的普通旧版本.当你从阵列中取出东西时,通常Person p = (Person)foo.get(1);
必须完成通常的铸造舞蹈.编译器正在为你节省按键,但速度命中/转换仍然会像往常一样发生.
当人们提到"类型擦除"时,这就是他们所说的.编译器会为您插入强制转换,然后"擦除"它Person
不仅仅是一个列表的事实Object
这种方法的好处是,不了解泛型的旧代码不必关心.它仍然处理与ArrayList
以往相同的旧版本.这在java世界中更为重要,因为他们希望支持使用带有泛型的Java 5编译代码,并使其在旧的1.4或以前的JVM上运行,微软故意决定不打扰.
缺点是我之前提到的速度命中,也因为没有ListOfPerson
伪类或类似的东西进入.class文件,稍后会查看它的代码(使用反射,或者如果你将它从另一个集合中拉出来)它被转换成Object
等等的地方)不能以任何方式告诉它只是一个包含Person
而不仅仅是任何其他数组列表的列表.
std::list* foo = new std::list ();
它看起来像C#和Java泛型,它会做你认为它应该做的事情,但在幕后不同的事情正在发生.
它与C#泛型最常见的是它构建特殊pseudo-classes
而不仅仅像java一样抛出类型信息,但它是一个完全不同的鱼群.
C#和Java都生成专为虚拟机设计的输出.如果你编写一些包含Person
类的代码,在这两种情况下,有关Person
类的一些信息将进入.dll或.class文件,而JVM/CLR将对此进行处理.
C++生成原始x86二进制代码.一切都不是一个对象,并且没有底层虚拟机需要了解一个Person
类.没有装箱或拆箱,功能不必属于类,或任何东西.
因此,C++编译器对模板的功能没有任何限制 - 基本上你可以手动编写任何代码,你可以为你编写模板.
最明显的例子是添加东西:
在C#和Java中,泛型系统需要知道哪些方法可用于类,并且需要将其传递给虚拟机.告诉它的唯一方法是通过硬编码实际类或使用接口.例如:
string addNames( T first, T second ) { return first.Name() + second.Name(); }
该代码不会在C#或Java中编译,因为它不知道该类型T
实际上提供了一个名为Name()的方法.你必须告诉它 - 在C#中这样:
interface IHasName{ string Name(); }; string addNames( T first, T second ) where T : IHasName { .... }
然后你必须确保传递给addNames的东西实现了IHasName接口等等.java语法是不同的(
),但它遇到了同样的问题.
这个问题的"经典"案例是尝试编写一个执行此操作的函数
string addNames( T first, T second ) { return first + second; }
您实际上无法编写此代码,因为无法使用其中的+
方法声明接口.你失败了.
C++没有遇到这些问题.编译器不关心将类型传递给任何VM - 如果你的对象都有.Name()函数,它将编译.如果他们不这样做,那就不会.简单.
所以你有它 :-)
C++很少使用"泛型"术语.相反,使用"模板"一词并且更准确.模板描述了一个技术,实现了通用的设计.
C++模板与C#和Java实现的模板有很大不同,主要有两个原因.第一个原因是C++模板不仅允许编译时类型参数,还允许编译时const-value参数:模板可以作为整数或甚至函数签名给出.这意味着你可以在编译时做一些非常时髦的东西,比如计算:
templatestruct product { static unsigned int const VALUE = N * product ::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 使用迭代器而不是容器是有用的,因为它允许对容器的某些部分进行操作而不是整体操作. C++的另一个显着特征是可以对类模板进行部分特化.这有点与Haskell和其他函数语言中的参数模式匹配有关.例如,让我们考虑一个存储元素的类: 这适用于任何元素类型.但是,让我们说通过应用一些特殊技巧,我们可以比其他类型更有效地存储指针.我们可以通过部分专门针对所有指针类型来实现: 现在,每当我们为一种类型实例化容器模板时,都会使用适当的定义:template
template
Store
Anders Hejlsberg本人描述了这里的差异" C#,Java和C++中的泛型 ".
目前已经有很多上好的答案什么的差异,所以让我给一个稍微不同的角度,并添加原因.
正如已经解释过的,主要区别在于类型擦除,即Java编译器擦除泛型类型并且它们不会在生成的字节码中结束.然而,问题是:为什么有人这样做?这没有意义!或者是吗?
那么,有什么选择呢?如果您没有使用该语言实现泛型,那么您在哪里实现它们?答案是:在虚拟机中.这打破了向后兼容性.
另一方面,类型擦除允许您将通用客户端与非通用库混合使用.换句话说:在Java 5上编译的代码仍然可以部署到Java 1.4.
然而,微软决定打破泛型的后向兼容性.这就是.NET Generics比Java Generics"更好" 的原因.
当然,太阳不是白痴或懦夫.他们"加油"的原因是,当他们引入泛型时,Java比.NET更老,更广泛.(它们在两个世界中大致同时被引入.)向后兼容性将是一个巨大的痛苦.
换句话说:在Java中,泛型是语言的一部分(这意味着它们只适用于Java,而不适用于其他语言),在.NET中它们是虚拟机的一部分(这意味着它们适用于所有语言,而不是只是C#和Visual Basic.NET).
将此与.NET功能(如LINQ,lambda表达式,局部变量类型推断,匿名类型和表达式树)进行比较:这些都是语言功能.这就是为什么VB.NET和C#之间存在细微差别的原因:如果这些功能是VM的一部分,那么它们在所有语言中都是相同的.但是CLR没有改变:它在.NET 3.5 SP1中仍然与在.NET 2.0中一样.您可以编译一个使用LINQ和.NET 3.5编译器的C#程序,并且仍然可以在.NET 2.0上运行它,前提是您不使用任何.NET 3.5库.这将不使用泛型和.NET 1.1的工作,但它会与Java和Java 1.4的工作.
我之前发帖的后续行动.
无论使用何种IDE,模板都是C++在intellisense中如此糟糕失败的主要原因之一.由于模板专业化,IDE无法确定给定成员是否存在.考虑:
templatestruct X { void foo() { } }; template <> struct X { }; typedef int my_int_type; X a; a.|
现在,光标位于指示的位置,IDE很难在那个时候说成员a
是否以及什么.对于其他语言,解析很简单,但对于C++,需要事先进行相当多的评估.
它变得更糟.如果my_int_type
在类模板中定义了怎么办?现在它的类型将取决于另一个类型参数.在这里,甚至编译器都失败了.
templatestruct Y { typedef T my_type; }; X ::my_type> b;
经过一番思考后,程序员会得出结论,这段代码与上面的代码相同:Y
解析为int
,因此b
应该是相同的类型a
,对吧?
错误.在编译器尝试解析此语句时,它实际上Y
还不知道!因此,它不知道这是一种类型.它可能是其他东西,例如成员函数或字段.这可能会引起歧义(虽然不是在目前的情况下),因此编译器失败.我们必须明确告诉它我们引用一个类型名称:
X::my_type> b;
现在,代码编译.要了解这种情况是如何产生歧义,请考虑以下代码:
Y::my_type(123);
这段代码语句完全有效,并告诉C++执行函数调用Y
.但是,如果my_type
不是函数而是类型,则此语句仍然有效并执行特殊的转换(函数样式转换),这通常是构造函数调用.编译器无法分辨我们的意思,所以我们必须在这里消除歧义.
Java和C#在第一语言发布后引入了泛型.但是,在引入泛型时,核心库的更改方式存在差异. C#的泛型不仅仅是编译器魔术,因此在不破坏向后兼容性的情况下无法生成现有的库类.
例如,在Java中,现有的集合框架是完全通用的. Java没有集合类的泛型和遗留非泛型版本. 在某些方面,这更加清晰 - 如果您需要在C#中使用集合,那么使用非泛型版本的理由非常少,但这些遗留类仍然存在,使整个环境变得混乱.
另一个值得注意的区别是Java和C#中的Enum类. Java的Enum有一些曲折的定义:
// java.lang.Enum Definition in Java public abstract class Enum> implements Comparable , 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");
由于在将泛型引入语言之前,Enum已经存在于C#中,因此在不破坏现有代码的情况下,定义无法改变.因此,与集合一样,它仍处于这种遗留状态的核心库中.