我主要使用 Java,泛型相对较新。我一直在读到 Java 做出了错误的决定,或者 .NET 有更好的实现等等。
那么,C++、C#、Java 在泛型中的主要区别是什么?每个的优点/缺点?
我会在噪音中加入我的声音,并努力让事情变得清晰:
List<Person> foo = new List<Person>();
然后编译器会阻止你把不在Person
列表中的东西放进去。
在幕后,C# 编译器只是将List<Person>
.NET dll 文件放入其中,但在运行时 JIT 编译器会构建一组新代码,就好像您编写了一个特殊的列表类只是为了包含人一样——比如ListOfPerson
.
这样做的好处是它使它非常快。没有强制转换或任何其他内容,并且由于 dll 包含这是 List 的信息,Person
因此稍后使用反射查看它的其他代码可以判断它包含Person
对象(因此您获得智能感知等)。
这样做的缺点是旧的 C# 1.0 和 1.1 代码(在他们添加泛型之前)不理解这些新List<something>
的,所以你必须手动将东西转换回普通的旧代码List
才能与它们互操作。这不是什么大问题,因为 C# 2.0 二进制代码不向后兼容。唯一会发生这种情况的情况是,如果您将一些旧的 C# 1.0/1.1 代码升级到 C# 2.0
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
而不是任何其他数组列表。
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() 函数,它将编译。如果他们不这样做,就不会。简单的。
所以你有它 :-)
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 <= elements < end。
使用迭代器代替容器很有用,因为它允许对容器的一部分而不是整个容器进行操作。
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++ 中的泛型”中描述了这些差异。
关于不同之处已经有很多很好的答案,所以让我给出一个稍微不同的观点并添加原因。
正如已经解释的那样,主要区别在于类型擦除,即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 并没有改变:它在 .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。
跟进我之前的帖子。
模板是 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# 的泛型不仅仅是编译器的魔法,因此不可能在不破坏向后兼容性的情况下泛化现有的库类。
例如,在 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对为什么会这样的非常清晰的解释。本质上,这意味着 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# 中已经存在枚举,因此在不破坏现有代码的情况下无法更改定义。因此,与收藏一样,它仍以这种遗留状态保留在核心库中。
晚了 11 个月,但我认为这个问题已经为一些 Java 通配符的东西做好了准备。
这是 Java 的一个语法特性。假设你有一个方法:
public <T> void Foo(Collection<T> thing)
并且假设您不需要在方法体中引用类型 T。你声明了一个名字 T 然后只使用它一次,那么你为什么要为它想一个名字呢?相反,您可以编写:
public void Foo(Collection<?> thing)
问号要求编译器假装您声明了一个正常的命名类型参数,该参数只需要在该位置出现一次。
通配符没有什么可以做的,而命名类型参数也不能做(这就是这些事情在 C++ 和 C# 中总是做的)。
Wikipedia 有很好的文章比较Java/C# 泛型和Java 泛型/C++模板。关于泛型的主要文章似乎有点混乱,但它确实有一些很好的信息。
最大的抱怨是类型擦除。在这种情况下,泛型不会在运行时强制执行。 这里是一些 Sun 文档的链接。
泛型是通过类型擦除实现的:泛型类型信息仅在编译时出现,之后由编译器擦除。
C++ 模板实际上比它们的 C# 和 Java 模板更强大,因为它们在编译时进行评估并支持专业化。这允许模板元编程并使 C++ 编译器等效于图灵机(即在编译过程中,您可以计算任何可以用图灵机计算的东西)。
在 Java 中,泛型只是编译器级别的,所以你得到:
a = new ArrayList<String>()
a.getClass() => ArrayList
请注意,“a”的类型是数组列表,而不是字符串列表。所以香蕉列表的类型等于()猴子列表。
可以这么说。
看起来,在其他非常有趣的建议中,有一个关于改进泛型和打破向后兼容性的建议:
目前,泛型是使用擦除实现的,这意味着泛型类型信息在运行时不可用,这使得某些类型的代码难以编写。泛型以这种方式实现以支持与旧的非泛型代码的向后兼容性。具体化的泛型将使泛型类型信息在运行时可用,这将破坏遗留的非泛型代码。但是,Neal Gafter 建议仅在指定时才使类型可具体化,以免破坏向后兼容性。
注意:我没有足够的评论点,所以请随时将此作为评论移至适当的答案。
与我从不明白它来自哪里的普遍看法相反,.net 在不破坏向后兼容性的情况下实现了真正的泛型,并且他们为此付出了明确的努力。您不必为了在 .net 2.0 中使用而将非泛型 .net 1.0 代码更改为泛型。通用列表和非通用列表在 .Net 框架 2.0 中仍然可用,甚至直到 4.0,这完全是出于向后兼容性的原因。因此,仍然使用非泛型 ArrayList 的旧代码仍然可以工作,并使用与以前相同的 ArrayList 类。从 1.0 到现在一直保持向后代码兼容性......所以即使在 .net 4.0 中,如果您选择这样做,您仍然必须选择使用 1.0 BCL 中的任何非泛型类。
所以我不认为java必须打破向后兼容性来支持真正的泛型。