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

时间:2008-08-28 05:08:07

标签: c# java c++ generics templates

我主要使用Java和泛型相对较新。我一直在读Java认为做错了决定,或者.NET有更好的实现等等。

那么,仿制药中C ++,C#,Java之间的主要区别是什么?每个人的利弊?

13 个答案:

答案 0 :(得分:364)

答案 1 :(得分:61)

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

C ++模板与C#和Java实现的模板有很大不同,主要有两个原因。第一个原因是C ++模板不仅允许编译时类型参数,还允许编译时const-value参数:模板可以作为整数甚至函数签名给出。这意味着你可以在编译时做一些非常时髦的东西,例如计算:

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时使用该特殊化。这允许我定义模板定义的递归。我相信这是Andrei Alexandrescu首先发现的。

模板专业化对于C ++很重要,因为它允许数据结构中的结构差异。模板作为一个整体是跨类型统一接口的一种方法。但是,虽然这是可取的,但在实现中不能平等对待所有类型。 C ++模板将此考虑在内。这与OOP在接口和实现之间在重写虚拟方法方面的差异非常大。

C ++模板对于其算法编程范例至关重要。例如,几乎所有容器算法都被定义为接受容器类型作为模板类型并统一处理它们的函数。实际上,这不太正确:C ++不适用于容器,而是适用于由两个迭代器定义的范围,指向容器的开头和结尾。因此,整个内容由迭代器限定:begin&lt; = elements&lt;端。

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

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

答案 2 :(得分:35)

Anders Hejlsberg本人在此处描述了这些差异“Generics in C#, Java, and C++”。

答案 3 :(得分:18)

的差异已经有很多好的答案,所以让我给出一个略有不同的观点并添加为什么

如前所述,主要区别在于类型擦除,即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一起使用。

答案 4 :(得分:14)

我之前发帖的后续行动。

模板是C ++在intellisense中如此糟糕地失败的主要原因之一,无论使用何种IDE。由于模板专业化,IDE无法确定给定成员是否存在。考虑:

template <typename T>
struct X {
    void foo() { }
};

template <>
struct X<int> { };

typedef int my_int_type;

X<my_int_type> a;
a.|

现在,光标位于指示的位置,如果成员a拥有,那么IDE很难说。对于其他语言,解析很简单,但对于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的类型相同1}},对吧?

错误。在编译器尝试解析此语句时,它实际上并不知道Y<int>::my_type!因此,它不知道这是一种类型。它可能是其他东西,例如成员函数或字段。这可能会引起歧义(虽然不是在目前的情况下),因此编译器失败。我们必须明确告诉它我们引用了一个类型名称:

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

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

Y<int>::my_type(123);

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

答案 5 :(得分:6)

Java和C#在首次发布语言后引入了泛型。但是,在引入泛型时,核心库的更改方式存在差异。 C#的泛型不仅仅是编译器魔法,因此无法在不破坏向后兼容性的情况下生成现有的库类。

例如,在Java中,现有的Collections Framework 完全是通用的 Java没有集合类的通用和遗留非泛型版本。在某些方面,这更清晰 - 如果你需要在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非常清楚explanation of exactly why这是如此。基本上,这意味着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#中,因此在不破坏现有代码的情况下,定义无法改变。因此,与集合一样,它仍处于这种遗留状态的核心库中。

答案 6 :(得分:4)

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

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

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

并且假设您不需要在方法体中引用类型T.你宣布一个名字T然后只使用它一次,所以你为什么要考虑它的名字?相反,你可以写:

public void Foo(Collection<?> thing)

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

使用通配符无法对命名类型参数执行任何操作(这些操作总是在C ++和C#中完成)。

答案 7 :(得分:2)

维基百科有很好的文章比较Java/C# genericsJava generics/C++模板。 main article on Generics似乎有点混乱,但确实有一些好的信息。

答案 8 :(得分:1)

最大的抱怨是类型擦除。其中,泛型不会在运行时强制执行。 Here's a link to some Sun docs on the subject

  

泛型按类型实现   擦除:泛型类型信息是   仅在编译时出现   它被编译器擦除。

答案 9 :(得分:1)

C ++模板实际上比它们的C#和Java对应物更强大,因为它们在编译时进行评估并支持专业化。这允许模板元编程并使C ++编译器等同于图灵机(即在编译过程中,您可以计算任何可用图灵机计算的东西)。

答案 10 :(得分:1)

在Java中,泛型只是编译器级别,因此您得到:

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

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

可以这么说。

答案 11 :(得分:1)

在其他非常有趣的提案中,有一个关于改进泛型和破坏向后兼容性的建议:

  

目前,仿制药已经实施   使用擦除,这意味着   通用类型信息不是   在运行时可用,这使得一些   一种难以编写的代码。泛型   以这种方式实施支持   向后兼容旧版本   非通用代码。具体化的泛型   会制作通用类型   运行时可用的信息,   这将破坏传统的非泛型   码。但是,Neal Gafter有   建议只制作类型   如果指定,以免破裂   向后兼容。

Alex Miller's article about Java 7 Proposals

答案 12 :(得分:0)

注意:我没有足够的评论意见,所以请随意将其作为对适当答案的评论。

与流行的相信相反,我从来不知道它来自何处,.net在不破坏向后兼容性的情况下实现了真正的泛型,并且他们花费了明确的努力。 您不必将非泛型.net 1.0代码更改为泛型,只是在.net 2.0中使用。通用列表和非泛型列表在.Net framework 2.0中仍然可用,甚至直到4.0,除了向后兼容性原因之外别无其他。因此,仍使用非泛型ArrayList的旧代码仍然可以工作,并使用与以前相同的ArrayList类。 从1.0到现在,始终保持向后代码兼容性......所以即使在.net 4.0中,如果您选择这样做,仍然需要使用1.0 BCL中的任何非泛型类。

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