c#和Java中的泛型有什么不同?和模板在c++ ?

我主要使用Java,泛型相对较新。我一直读到Java做出了错误的决定,或者。net有更好的实现等等。

那么,c++, c#, Java在泛型上的主要区别是什么?利弊各有哪些?

58402 次浏览

Wikipedia上有很多比较Java / c#泛型Java泛型/ c++模板的文章。泛型的主要文章看起来有点混乱,但它确实有一些好的信息。

Anders Hejlsberg自己描述了这里的差异“c#、Java和c++中的泛型”。

最大的抱怨是字体擦除。在这种情况下,不在运行时强制执行泛型。这里有一些关于这个主题的Sun文档的链接

泛型是由类型实现的 Erasure:泛型类型信息为 之后,仅在编译时出现

c++模板实际上比c#和Java模板强大得多,因为它们在编译时进行计算,并支持专门化。这允许模板元编程,并使c++编译器等价于图灵机(即在编译过程中,你可以计算任何可以用图灵机计算的东西)。

c++很少使用“泛型”术语。相反,使用“模板”这个词更准确。Templates描述了一个技术来实现通用设计。

c++模板与c#和Java实现的模板非常不同,主要有两个原因。第一个原因是c++模板不仅允许编译时类型参数,还允许编译时常量值参数:模板可以作为整数甚至函数签名。这意味着你可以在编译时做一些非常奇怪的事情,例如计算:

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时就使用该特化。这允许我在模板定义上定义递归。我相信这是由安德烈Alexandrescu首先发现的。

模板专门化对于c++很重要,因为它允许数据结构的结构差异。模板作为一个整体是一种跨类型统一接口的方法。然而,尽管这是可取的,但在实现中不能平等对待所有类型。c++模板考虑到了这一点。这与OOP在覆盖虚方法的接口和实现之间的区别非常相似。

c++模板对于它的算法编程范型是必不可少的。例如,几乎所有容器的算法都定义为接受容器类型为模板类型并统一对待它们的函数。实际上,这并不完全正确:c++不能在容器上工作,而是在范围上工作,范围由两个迭代器定义,分别指向容器的开始部分和结束部分。因此,整个内容都由迭代器限定:begin <= elements <结束。

使用迭代器而不是容器是有用的,因为它允许对容器的部分而不是整个进行操作。

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*.

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

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

注意,'a'的类型是数组列表,而不是字符串列表。所以香蕉列表的类型等于()猴子列表。

可以这么说。

跟进我之前的帖子。

模板是c++在智能感知上如此糟糕的主要原因之一,不管使用的是哪种IDE。由于模板专门化,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中,现有的集合框架完全genericisedJava没有集合类的泛型版本和遗留的非泛型版本。在某些方面,这是更干净的-如果你需要在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");

因为Enum在引入泛型之前就已经存在于c#中,所以如果不改变定义就会破坏现有的代码。因此,像集合一样,它以这种遗留状态保留在核心库中。

我将加入我的声音,试着把事情弄清楚:

c#泛型允许你声明这样的东西。

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

,然后编译器将阻止你将不是Person的东西放入列表中。
在幕后,c#编译器只是把List<Person>放到。net dll文件中,但是在运行时,JIT编译器会构建一组新的代码,就好像你写了一个专门用来包含人的列表类——类似ListOfPerson

这样做的好处是速度很快。没有强制转换或任何其他东西,并且由于dll包含这是Person列表的信息,其他稍后使用反射查看它的代码可以告诉它包含Person对象(所以你会得到智能感知等等)。

这样做的缺点是旧的c# 1.0和1.1代码(在添加泛型之前)不理解这些新的List<something>,所以你必须手动将它们转换回普通的旧List来与它们互操作。这不是什么大问题,因为c# 2.0二进制代码不向后兼容。这种情况只会发生在你将一些旧的c# 1.0/1.1代码升级到c# 2.0的时候

Java泛型允许您声明类似的内容。

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

表面上看是一样的,也差不多是一样的。编译器也会阻止你把不是Person的东西放入列表中。

区别在于幕后发生了什么。与c#不同,Java不会构建一个特殊的ListOfPerson——它只是使用Java中一直存在的普通的旧ArrayList。当你从数组中取出东西时,通常的Person p = (Person)foo.get(1);强制转换仍然必须完成。编译器为您节省了按键,但仍然会像往常一样引起快速命中/强制转换。
当人们提到“输入擦除”;这就是他们在谈论的。编译器为你插入类型转换,然后“擦除”它应该是Person的列表而不仅仅是Object

的事实

这种方法的好处是,不理解泛型的旧代码不必关心。它仍然像往常一样处理相同的旧ArrayList。这在java世界中更为重要,因为他们希望支持使用带泛型的java 5编译代码,并让它运行在旧的1.4或以前的JVM上,而微软故意决定不去麻烦这些。

缺点是我之前提到的速度问题,也因为.class文件中没有ListOfPerson伪类或类似的东西,所以稍后查看它的代码(通过反射,或者如果你从另一个集合中将它转换为Object等)无法以任何方式判断它是一个只包含Person而不是任何其他数组列表的列表。

c++模板允许你这样声明

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

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

它与c#泛型最相似的地方在于,它构建了特殊的pseudo-classes,而不是像java那样仅仅扔掉类型信息,但这是完全不同的事情。

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接口等等。java的语法是不同的(<T extends IHasName>),但它遭受同样的问题。

这个问题的“经典”案例是尝试编写一个这样做的函数

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

你不能实际编写这段代码,因为没有办法在其中声明带有+方法的接口。你失败了。

c++没有这些问题。编译器并不关心将类型传递给任何VM -如果你的两个对象都有. name()函数,它会编译。如果他们不这样做,它就不会。简单。

所以,你有了它:-)

已经有很多关于什么的很好的答案,所以让我给出一个稍微不同的角度,并添加为什么

正如已经解释过的,主要的区别是类型擦除,即Java编译器擦除泛型类型的事实,它们不会在生成的字节码中结束。然而,问题是:为什么会有人这么做呢?这说不通啊!真的是这样吗?

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

另一方面,类型擦除允许混合泛型客户端和非泛型库。换句话说:在Java 5上编译的代码仍然可以部署到Java 1.4。

然而,微软决定打破泛型的向后兼容性。这是为什么。net泛型比Java泛型“更好”。

当然,孙不是白痴也不是懦夫。他们“退缩”的原因是,当他们引入泛型时,Java明显比。net更古老,更广泛。(它们在两个世界几乎同时被引入。)打破向后兼容性将是一个巨大的痛苦。

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

将此与。net特性如LINQ、lambda表达式、局部变量类型推断、匿名类型和表达式树进行比较:这些都是语言特性。这就是VB和VB之间有细微差别的原因。NET和c#:如果这些特性是VM的一部分,那么它们在所有语言中是相同的。但是CLR并没有改变:它在。net 3.5 SP1和。net 2.0中是一样的。你可以用。net 3.5编译器编译一个使用LINQ的c#程序,并且仍然可以在。net 2.0上运行它,前提是你不使用任何。net 3.5库。这将适用于泛型和。net 1.1,但它适用于Java和Java 1.4。

看起来,在其他非常有趣的建议中,有一个是关于改进泛型和打破向后兼容性的:

目前实现了泛型 使用擦除,这意味着 泛型类型信息则不是 在运行时可用,这使得一些 这种代码很难写。泛型 采用这种方式实现支持吗 与旧版本的向后兼容性 非泛型代码。具体化的泛型 是泛型类型吗 运行时可用的信息, 这将打破传统的非泛型 代码。然而,尼尔·加福特做到了 建议只使类型具体化 如有指定,以免折断 向后兼容性。< / p >

Alex Miller关于Java 7提案的文章

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

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

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

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

public void Foo(Collection<?> thing)

问号要求编译器假装您声明了一个普通的命名类型参数,该参数只需要在该位置出现一次。

用通配符可以做的事情,用命名类型参数也可以做(在c++和c#中,这些事情总是这样做的)。

注:我没有足够的观点来评论,所以请随意将此作为评论移动到适当的答案。

与流行的观点相反,我从来不明白它从何而来,.net实现了真正的泛型而不破坏向后兼容性,他们为此付出了明确的努力。 你不必为了在。net 2.0中使用而将非泛型的。net 1.0代码更改为泛型。泛型列表和非泛型列表在. net framework 2.0中仍然可用,甚至直到4.0,完全是出于向后兼容的原因。因此,仍然使用非泛型ArrayList的旧代码仍然可以工作,并使用与以前相同的ArrayList类。 向后代码兼容性始终保持从1.0到现在…因此,即使在。net 4.0中,如果你选择使用1.0 BCL中的任何非泛型类,你仍然可以选择这样做

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