C#和Java中的泛型与C ++中的模板有什么区别?

C#和Java中的泛型与C ++中的模板有什么区别?

What are the differences between Generics in C# and Java… and Templates in C++?

我主要使用Java和泛型是比较新的。我一直在读,Java做出了错误的决定,或者说.NET有更好的实现等等。

那么,泛型中C++、C、Java的主要区别是什么呢?各方面的优缺点?


我会把我的声音加入到噪音中,然后试着把事情弄清楚:好的。C泛型允许您声明类似的内容。

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

然后编译器将阻止您将不属于Person的内容放入列表中。在幕后,C编译器只是将List放入.NET dll文件中,但在运行时,JIT编译器会去构建一组新的代码,就好像您编写了一个专门用于包含人的列表类——类似于ListOfPerson)。好的。

这样做的好处是,它可以让它变得非常快。没有强制转换或任何其他内容,并且由于dll包含的信息是Person的列表,因此稍后使用反射查看它的其他代码可以告诉它包含Person对象(因此您可以获得intellisense等)。好的。

缺点是,旧的C 1.0和1.1代码(在添加泛型之前)不理解这些新的List,因此您必须手动将内容转换回普通的旧List,以便与它们进行互操作。这不是一个大问题,因为C 2.0二进制代码不向后兼容。唯一会发生这种情况的是,如果您将一些旧的C 1.0/1.1代码升级到C 2.0好的。Java泛型允许你声明这样的东西。

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

从表面上看,它是一样的。编译器还将阻止您将不属于Person的内容放入列表中。好的。

区别在于幕后发生了什么。与C语言不同的是,Java不运行并构建一个特殊的EDCOX1(2)-它只是使用了一直在Java中的普通的EDCOX1 9。当你把东西从数组中拿出来时,通常的Person p = (Person)foo.get(1);的演员舞仍然需要完成。编译器正在为您保存按键,但速度命中/强制转换仍然像以前一样发生。当人们提到"类型擦除"时,这就是他们所说的。编译器为您插入强制转换,然后"删除"它是一个Person的列表,而不仅仅是Object的列表。好的。

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

缺点是我前面提到的速度下降,而且因为在.class文件中没有ListOfPerson伪类或类似的东西,以后查看它的代码(有反射,或者如果从另一个集合中提取它,在这个集合中它被转换成Object等等)不能以任何方式说明它的含义。o是一个只包含Person的列表,而不只是任何其他数组列表。好的。C++模板允许你声明这样的东西

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

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

它与C泛型最为相似,因为它建立了特殊的EDCOX1×0,而不是像Java那样扔掉类型信息,但它是一个完全不同的鱼群。好的。

C和Java都是为虚拟机设计的。如果您编写的代码中有一个Person类,在这两种情况下,关于Person类的一些信息将进入.dll或.class文件,而jvm/clr将对此进行处理。好的。

C++生成原始的x86二进制代码。一切都不是一个对象,也没有底层的虚拟机需要了解一个Person类。不需要装箱或取消装箱,函数不必属于类,甚至任何东西。好的。

正因为如此,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++模板的其他显著特征,即模板特化。代码定义了一个类模板product,它有一个值参数。它还为该模板定义了一个专门化,当参数的计算结果为1时使用该模板。这允许我在模板定义上定义递归。我相信这是安德烈·亚历山大·埃斯库首次发现的。

模板专门化对于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很难说出成员a有没有成员。对于其他语言,解析将是简单的,但是对于C++,需要事先进行相当程度的评估。

情况越来越糟。如果my_int_type也在类模板中定义了呢?现在它的类型将依赖于另一个类型参数。在这里,即使编译器也会失败。

1
2
3
4
5
6
template <typename T>
struct Y {
    typedef T my_type;
};

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

经过一番思考,程序员会得出这样的结论:Y::my_type解析为int,因此b应该与a是同一类型,对吗?

错了。在编译器试图解析这个语句的时候,它实际上还不知道Y::my_type!因此,它不知道这是一种类型。它可以是其他东西,例如成员函数或字段。这可能会导致歧义(尽管在目前的情况下不是这样),因此编译器会失败。我们必须明确地告诉它我们引用了一个类型名:

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

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

1
Y<int>::my_type(123);

这个代码语句是完全有效的,并告诉C++执行函数调用EDCOX1(2)。但是,如果my_type不是函数而是类型,则此语句仍然有效,并执行一个特殊的强制转换(函数样式强制转换),这通常是一个构造函数调用。编译器无法分辨我们的意思,所以我们必须在这里消除歧义。


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
//  Parsing String to Enum in C#
Colour colour = (Colour)Enum.Parse(typeof(Colour),"RED");

由于在将泛型引入该语言之前,枚举已经存在于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 = new ArrayList<String>()
a.getClass() => ArrayList

请注意,"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必须打破向后兼容性来支持真正的泛型。


推荐阅读