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

那么,C++、C#、Java 在泛型方面的主要区别是什么?各自的优点/缺点?

有帮助吗?

解决方案

我将把我的声音加入到噪音中,并尝试把事情说清楚:

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 - 它只是使用普通的旧的 ArrayList 这一直是Java中的。当你从数组中取出东西时,通常 Person p = (Person)foo.get(1); 选角舞蹈还得做。编译器正在为您节省按键操作,但速度影响/转换仍然像以前一样发生。
当人们提到“类型擦除”时,他们所说的就是这个。编译器会为您插入强制转换,然后“擦除”它应该是一个列表的事实 Person 不只是 Object

这种方法的好处是不理解泛型的旧代码不必关心。它仍然在处理同样的旧问题 ArrayList 一如既往。这在 Java 世界中更为重要,因为他们希望支持使用带有泛型的 Java 5 编译代码,并使其在旧的 1.4 或以前的 JVM 上运行,而微软故意决定不去打扰。

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

C++ 模板允许你声明这样的东西

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

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

它与 C# 泛型最共同之处在于它构建了特殊的 pseudo-classes 而不是像 java 那样仅仅丢弃类型信息,但这是完全不同的事情。

C# 和 Java 都会产生专为虚拟机设计的输出。如果您编写一些具有 Person 其中的类,在这两种情况下都有一些关于 Person class 将进入 .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++ 不存在这些问题。编译器不关心将类型传递给任何虚拟机 - 如果您的两个对象都有 .Name() 函数,它将编译。如果他们不这样做,那就不会。简单的。

所以你有它 :-)

其他提示

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

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,就会使用该模板。这允许我定义模板定义的递归。我相信这是由 安德烈·亚历山德雷斯库.

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

C++ 模板对于其算法编程范例至关重要。例如,几乎所有容器算法都被定义为接受容器类型作为模板类型并统一对待它们的函数。事实上,这并不完全正确:C++ 不适用于容器,而是适用于 范围 由两个迭代器定义,分别指向容器的开头和结尾。因此,整个内容被迭代器限制:开始 <= 元素 < 结束。

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

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

安德斯·海尔斯伯格 (Anders Hejlsberg) 本人描述了此处的差异“C#、Java 和 C++ 中的泛型".

网上已经有很多很好的答案了 什么 差异是,所以让我给出一个稍微不同的观点并添加 为什么.

正如已经解释过的,主要区别是 类型擦除, , IE。事实上,Java 编译器会删除泛型类型,并且它们最终不会出现在生成的字节码中。然而,问题是:为什么有人会这样做?这没有道理!或者确实如此?

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

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

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

当然,Sun并不是白痴或胆小鬼。他们“退缩”的原因是,当他们引入泛型时,Java 比 .NET 更古老、更广泛。(它们在两个世界中几乎同时引入。)破坏向后兼容性将是一个巨大的痛苦。

换句话说:在Java中,泛型是 语言 (这意味着他们适用 仅有的 对于 Java,而不是其他语言),在 .NET 中它们是 虚拟机 (这意味着它们适用于 全部 语言,而不仅仅是 C# 和 Visual Basic.NET)。

将此与 LINQ、lambda 表达式、局部变量类型推断、匿名类型和表达式树等 .NET 功能进行比较:这些都是 语言 特征。这就是 VB.NET 和 C# 之间存在细微差别的原因:如果这些功能是虚拟机的一部分,那么它们在 全部 语言。但 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。

我之前发帖的后续内容。

无论使用什么 IDE,模板都是 C++ 在智能感知方面严重失败的主要原因之一。由于模板专门化,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# 的泛型不仅仅是编译器的魔法 所以不可能 泛化 现有的库类,而不破坏向后兼容性。

例如,在Java中现有的 馆藏框架 曾是 完全通用化. 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 的非常清楚 确切原因的解释 是这样的。本质上,这意味着 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");

由于在将泛型引入 C# 语言之前,Enum 已经存在于 C# 中,因此在不破坏现有代码的情况下无法更改定义。因此,与集合一样,它仍以这种遗留状态保留在核心库中。

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

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

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

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

public void Foo(Collection<?> thing)

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

通配符能做的事,命名类型参数也能做(C++ 和 C# 中总是这样做的)。

维基百科有很好的文章对两者进行了比较 Java/C# 泛型Java 泛型/C++ 模板。这 关于泛型的主要文章 看起来有点混乱,但里面确实有一些很好的信息。

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

仿制药是通过类型擦除实现的:通用类型信息仅在编译时间存在,之后由编译器删除。

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

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

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

请注意,“a”的类型是数组列表,而不是字符串列表。因此,香蕉列表的类型等于猴子列表。

可以这么说。

看起来,除了其他非常有趣的建议之外,还有一个关于改进泛型和打破向后兼容性的建议:

当前,使用擦除实现仿制药,这意味着仿制类型信息在运行时不可用,这使得某种代码难以编写。对仿制药的实施方式进行了支持,以支持与较旧的非传家代码的向后兼容性。REDIFIDER通用物将使仿制类型信息在运行时可用,这将破坏旧式非传统代码。但是,Neal Gafter仅在指定时才提出可将类型进行改组,以免使后向兼容性。

Alex Miller 关于 Java 7 提案的文章

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

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

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top