我主要使用Java,泛型相对较新。我一直读到Java做出了错误的决定,或者。net有更好的实现等等。
那么,c++, c#, Java在泛型上的主要区别是什么?利弊各有哪些?
我主要使用Java,泛型相对较新。我一直读到Java做出了错误的决定,或者。net有更好的实现等等。
那么,c++, c#, Java在泛型上的主要区别是什么?利弊各有哪些?
当前回答
跟进我之前的帖子。
模板是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中,现有的集合框架是完全泛化的。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");
因为Enum在引入泛型之前就已经存在于c#中,所以如果不改变定义就会破坏现有的代码。因此,像集合一样,它以这种遗留状态保留在核心库中。
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++并不适用于容器,而是适用于由两个迭代器定义的范围,它们分别指向容器的开始部分和结束部分。因此,整个内容都由迭代器限定: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*.
c++模板实际上比c#和Java模板强大得多,因为它们在编译时进行计算,并支持专门化。这允许模板元编程,并使c++编译器等价于图灵机(即在编译过程中,你可以计算任何可以用图灵机计算的东西)。
我将加入我的声音,试着把事情弄清楚:
c#泛型允许你声明这样的东西。
List<Person> foo = new List<Person>();
然后编译器会阻止你把Person以外的东西放到列表中。 在幕后,c#编译器只是把List<Person>放到。net dll文件中,但是在运行时,JIT编译器会构建一组新的代码,就好像你写了一个专门用来包含人的列表类——类似于ListOfPerson。
这样做的好处是速度很快。没有强制转换或任何其他东西,因为dll包含这是一个List of 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的东西放到列表中。
The difference is what happens behind the scenes. Unlike C#, Java does not go and build a special ListOfPerson - it just uses the plain old ArrayList which has always been in Java. When you get things out of the array, the usual Person p = (Person)foo.get(1); casting-dance still has to be done. The compiler is saving you the key-presses, but the speed hit/casting is still incurred just like it always was. When people mention "Type Erasure" this is what they're talking about. The compiler inserts the casts for you, and then 'erases' the fact that it's meant to be a list of Person not just Object
这种方法的好处是,不理解泛型的旧代码不必关心。它仍然和以前一样处理相同的数组列表。这在java世界中更为重要,因为他们希望支持使用带泛型的java 5编译代码,并让它运行在旧的1.4或以前的JVM上,而微软故意决定不去麻烦这些。
缺点是我前面提到的速度问题,也因为没有ListOfPerson伪类或任何类似的东西进入.class文件,稍后查看它的代码(使用反射,或者如果您从另一个集合中将它转换为Object等)无法以任何方式判断它是一个仅包含Person的列表,而不是任何其他数组列表。
c++模板允许你这样声明
std::list<Person>* foo = new std::list<Person>();
它看起来像c#和Java的泛型,它会做你认为它应该做的事情,但在幕后发生了不同的事情。
它与c#泛型最相似的地方在于,它构建了特殊的伪类,而不是像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扩展IHasName>),但也会遇到同样的问题。
这个问题的“经典”案例是尝试编写一个这样做的函数
string addNames<T>( T first, T second ) { return first + second; }
您不能实际编写这段代码,因为没有办法在接口中声明带有+方法的接口。你失败了。
c++没有这些问题。编译器并不关心将类型传递给任何VM -如果你的两个对象都有. name()函数,它会编译。如果他们不这样做,它就不会。简单。
所以,你有了它:-)
最大的抱怨是字体擦除。在这种情况下,不在运行时强制执行泛型。这里有一些关于这个主题的Sun文档的链接。
泛型是按类型实现的 Erasure:泛型类型信息为 之后,仅在编译时出现 它被编译器擦除。