What are the differences between Generics in C# and Java… and Templates in C++?
我主要使用Java和泛型是比较新的。我一直在读,Java做出了错误的决定,或者说.NET有更好的实现等等。
那么,泛型中C++、C、Java的主要区别是什么呢?各方面的优缺点?
我会把我的声音加入到噪音中,然后试着把事情弄清楚:好的。C泛型允许您声明类似的内容。
1 |
然后编译器将阻止您将不属于
这样做的好处是,它可以让它变得非常快。没有强制转换或任何其他内容,并且由于dll包含的信息是
缺点是,旧的C 1.0和1.1代码(在添加泛型之前)不理解这些新的
1 |
号
从表面上看,它是一样的。编译器还将阻止您将不属于
区别在于幕后发生了什么。与C语言不同的是,Java不运行并构建一个特殊的EDCOX1(2)-它只是使用了一直在Java中的普通的EDCOX1 9。当你把东西从数组中拿出来时,通常的
这种方法的好处是,不理解泛型的旧代码不必关心。它仍然像往常一样处理着同一个老的
缺点是我前面提到的速度下降,而且因为在.class文件中没有
1 |
它看起来像C和Java泛型,它会做你认为应该做的事情,但是在幕后,不同的事情正在发生。好的。
它与C泛型最为相似,因为它建立了特殊的EDCOX1×0,而不是像Java那样扔掉类型信息,但它是一个完全不同的鱼群。好的。
C和Java都是为虚拟机设计的。如果您编写的代码中有一个
C++生成原始的x86二进制代码。一切都不是一个对象,也没有底层的虚拟机需要了解一个
正因为如此,C++编译器对模板所能做的没有任何限制——基本上任何你可以手动编写的代码,你都可以得到模板来为你编写。最明显的例子是添加:好的。
在C语言和Java语言中,泛型系统需要知道什么样的方法可用于类,并且它需要传递到虚拟机。唯一能说明这一点的方法是在中硬编码实际类,或者使用接口。例如:好的。
1 | string addNames<T>( T first, T second ) { return first.Name() + second.Name(); } |
号
该代码不会在C语言或Java中编译,因为它不知道类型EDCOX1 OR 4实际上提供了一个名为NAME()的方法。你必须这样说-在C中:好的。
1 2 | interface IHasName{ string Name(); }; string addNames<T>( T first, T second ) where T : IHasName { .... } |
然后,您必须确保传递给addname的内容实现ihasname接口等。Java语法是不同的(EDOCX1,5),但是它也遇到了同样的问题。好的。
这个问题的"经典"情况是试图编写一个函数好的。
1 | string addNames<T>( T first, T second ) { return first + second; } |
。
实际上,您不能编写此代码,因为没有方法声明其中包含
C++不存在这些问题。编译器不关心将类型传递给任何VM——如果两个对象都有.name()函数,它将编译。如果他们不这样做,就不会了。很简单。好的。
所以,你有了它:-)好的。好啊。
C++很少使用"泛型"术语。相反,"模板"一词被使用,而且更准确。模板描述了一种实现通用设计的技术。
C++模板与C和Java实现的两个主要原因有很大的不同。第一个原因是C++模板不仅允许编译时类型参数,而且还允许编译时const值参数:模板可以被赋予整数或函数签名。这意味着您可以在编译时做一些非常有趣的事情,例如计算:
1 2 3 4 5 6 7 8 9 10 11 12 | 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++模板的其他显著特征,即模板特化。代码定义了一个类模板
模板专门化对于C++非常重要,因为它允许数据结构中的结构差异。模板作为一个整体是一种跨类型统一接口的方法。然而,尽管这是可取的,但在实现内部不能平等地对待所有类型。C++模板考虑了这一点。这与OOP在重写虚拟方法的情况下在接口和实现之间所产生的差异非常相似。
C++模板是其算法编程范例必不可少的。例如,几乎所有容器算法都定义为接受容器类型作为模板类型并统一处理它们的函数。实际上,这并不完全正确:C++不在容器上工作,而是在两个迭代器定义的范围内,指向容器的开始和后面。因此,整个内容由迭代器限定:begin<=elements 使用迭代器而不是容器是有用的,因为它允许对容器的部分而不是整体进行操作。 C++的另一个显著特征是类模板的部分专门化的可能性。这与haskell和其他函数语言中参数的模式匹配有些关系。例如,让我们考虑一个存储元素的类:
1 2 | template <typename T> class Store { … }; // (1) |
号
这适用于任何元素类型。但是我们可以说,通过应用一些特殊的技巧,我们可以比其他类型更有效地存储指针。我们可以通过对所有指针类型进行部分专门化来实现这一点:
1 2 | template <typename T> class Store<T*> { … }; // (2) |
现在,每当我们为一种类型实例一个容器模板时,都会使用适当的定义:
1 2 3 | Store<int> x; // Uses (1) Store<int*> y; // Uses (2) Store<string**> z; // Uses (2), with T = string*. |
。
安德斯·海尔斯伯格自己在这里描述了"C语言、Java语言和C++语言中的泛型"的区别。
对于这些差异,已经有很多很好的答案了,所以让我给出一个稍微不同的观点,并添加原因。
正如已经解释过的,主要的区别是类型擦除,即Java编译器删除泛型类型的事实,它们不会在生成的字节码中结束。然而,问题是:为什么会有人这样做?这没道理!还是这样?
那么,还有什么选择呢?如果您不在语言中实现泛型,那么在哪里实现它们呢?答案是:在虚拟机中。这破坏了向后兼容性。
另一方面,类型擦除允许您将通用客户机与非通用库混合。换句话说,在Java 5上编译的代码仍然可以部署到Java 1.4。
然而,微软决定打破对泛型的向后兼容性。这就是为什么.NET泛型比Java泛型"更好"的原因。
当然,太阳不是白痴或懦夫。他们之所以"胆怯",是因为Java在引入泛型时明显比.NET更古老和更广泛。(它们在两个世界中大致同时被引入)破坏向后兼容性将是一个巨大的痛苦。
换句话说,在Java中,泛型是语言的一部分(这意味着它们只适用于Java,而不是其他语言),在.NET中,它们是虚拟机的一部分(这意味着它们适用于所有语言,而不只是C语言和Visual Basic .NET)。
将其与.NET特性(如LINQ、lambda表达式、局部变量类型推断、匿名类型和表达式树)进行比较:这些都是语言特性。这就是为什么vb.net和c之间存在细微的差异:如果这些特性是vm的一部分,那么它们在所有语言中都是相同的。但是clr没有改变:它在.NET3.5sp1中仍然和在.NET2.0中一样。如果不使用任何.NET 3.5库,则可以编译使用LINQ和.NET 3.5编译器的C程序,并在.NET 2.0上运行它。这对于泛型和.NET 1.1来说是行不通的,但是它将与Java和Java 1.4一起使用。
跟进我以前的帖子。
无论使用IDE,模板都是C++在智能感知上失败的主要原因之一。由于模板专门化,IDE永远无法真正确定给定的成员是否存在。考虑:
1 2 3 4 5 6 7 8 9 10 11 12 | template <typename T> struct X { void foo() { } }; template <> struct X<int> { }; typedef int my_int_type; X<my_int_type> a; a.| |
现在,光标在指定的位置,在这个位置上,IDE很难说出成员
情况越来越糟。如果
1 2 3 4 5 6 | template <typename T> struct Y { typedef T my_type; }; X<Y<int>::my_type> b; |
号
经过一番思考,程序员会得出这样的结论:
错了。在编译器试图解析这个语句的时候,它实际上还不知道
1 | X<typename Y<int>::my_type> b; |
现在,代码编译。要了解这种情况下产生歧义的原因,请考虑以下代码:
1 | Y<int>::my_type(123); |
。
这个代码语句是完全有效的,并告诉C++执行函数调用EDCOX1(2)。但是,如果
Java语言和C语言在第一语言发布后都引入了泛型。然而,在引入泛型时,核心库的变化方式存在差异。C的泛型不仅仅是编译器的魔力,因此在不破坏向后兼容性的情况下,不可能对现有的库类进行泛型化。
例如,在Java中,现有的集合框架完全是泛化的。Java不具有集合类的通用和传统非泛型版本。从某种程度上说,这是非常干净的——如果您需要使用C中的集合,那么使用非通用版本的理由确实非常少,但是那些遗留类仍然存在,使环境变得混乱。
另一个显著的区别是Java和C语言中的枚举类。Java的枚举有一些看起来有些扭曲的定义:
1 2 | // java.lang.Enum Definition in Java public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { |
(见安吉丽卡·兰格对这一切的确切解释。本质上,这意味着Java可以从字符串到枚举值提供类型安全访问:
1 2 | // Parsing String to Enum in Java Colour colour = Colour.valueOf("RED"); |
。
将其与C版本进行比较:
1 2 |
。
由于在将泛型引入该语言之前,枚举已经存在于C中,因此在不破坏现有代码的情况下无法更改定义。所以,和集合一样,它仍然在这个遗留状态的核心库中。
11个月后,但我认为这个问题已经准备好了一些Java通配符。
这是Java的句法特征。假设您有一个方法:
1 | public <T> void Foo(Collection<T> thing) |
假设您不需要引用方法体中的类型T。你要声明一个名字t,然后只使用它一次,那么为什么你要为它考虑一个名字呢?相反,你可以写:
1 | public void Foo(Collection<?> thing) |
号
问号要求编译器假装您声明了一个普通的命名类型参数,该参数只需要在该点中出现一次。
你不能用通配符做任何事情,你也不能用一个命名类型的参数来做(这就是这些事情是如何在C++和C语言中完成的)。
维基百科对爪哇和C语言的泛型和Java泛型/C++模板进行了很好的对比。关于仿制药的主要文章看起来有点杂乱,但里面确实有一些很好的信息。
看起来,除了其他非常有趣的建议外,还有一个关于改进泛型和破坏向后兼容性的建议:
Currently, generics are implemented
using erasure, which means that the
generic type information is not
available at runtime, which makes some
kind of code hard to write. Generics
were implemented this way to support
backwards compatibility with older
non-generic code. Reified generics
would make the generic type
information available at runtime,
which would break legacy non-generic
code. However, Neal Gafter has
proposed making types reifiable only
if specified, so as to not break
backward compatibility.
号
在Alex Miller的文章中关于Java 7的建议
在爪哇,泛型只是编译器级,所以您得到:
1 2 |
请注意,"a"的类型是数组列表,而不是字符串列表。所以香蕉列表的类型等于猴子列表。
可以这么说。
C++模板实际上比它们的C语言和Java语言强大得多,因为它们在编译时被评估并支持专门化。这允许模板元编程,并使C++编译器等同于图灵机(即在编译过程中,可以用图灵机计算任何可计算的)。
最大的抱怨是类型删除。在这一点上,泛型不会在运行时强制执行。下面是一些关于这个主题的Sun文档的链接。
Generics are implemented by type
erasure: generic type information is
present only at compile time, after
which it is erased by the compiler.
号
注意:我没有足够的观点发表评论,所以请随意将此作为评论移动到适当的答案。
与流行的观点相反,.NET实现了真正的泛型,但没有破坏向后兼容性,为此他们付出了显式的努力。为了在.NET 2.0中使用,不必将非泛型.NET 1.0代码更改为泛型。通用列表和非通用列表在.NET Framework 2.0中仍然可用,甚至直到4.0,这完全是出于向后兼容的原因。因此,仍然使用非泛型arraylist的旧代码仍然可以工作,并且使用与以前相同的arraylist类。从1.0开始一直保持向后代码兼容性直到现在…因此,即使在.NET 4.0中,如果您选择使用1.0bcl中的任何非泛型类,也必须选择使用。
所以我不认为Java必须打破向后兼容性来支持真正的泛型。