C#和Java中的泛型有什么区别?和模板C++?[已关闭]
我主要使用Java和泛型是相对较新的。我一直读到Java做出了错误的决定,或者.NET有更好的实现等等。
那么,泛型中的C++,C#,Java之间的主要区别是什么?每种方法的优缺点?
我主要使用Java和泛型是相对较新的。我一直读到Java做出了错误的决定,或者.NET有更好的实现等等。
那么,泛型中的C++,C#,Java之间的主要区别是什么?每种方法的优缺点?
我会在噪音中加入我的声音,并尝试使事情变得清晰:
List<Person> foo = new List<Person>();
然后编译器将阻止您将未放入列表中的内容。
在幕后,C#编译器只是放入.NET dll文件中,但在运行时,JIT编译器会构建一组新的代码,就好像您编写了一个特殊的列表类,仅用于包含人员 - 类似于。Person
List<Person>
ListOfPerson
这样做的好处是它使它变得非常快。没有强制转换或任何其他东西,并且由于dll包含这是列表的信息,因此以后使用反射查看它的其他代码可以告诉它包含对象(因此您可以获得智能感知等)。Person
Person
这样做的缺点是旧的C# 1.0和1.1代码(在他们添加泛型之前)不理解这些新的,所以你必须手动将东西转换回普通的旧代码才能与它们进行互操作。这不是一个大问题,因为 C# 2.0 二进制代码不向后兼容。唯一一次发生这种情况是将一些旧的C# 1.0 / 1.1代码升级到C# 2.0List<something>
List
ArrayList<Person> foo = new ArrayList<Person>();
从表面上看,它看起来是一样的,而且在某种程度上是一样的。编译器还将阻止您将未放入列表中的内容。Person
不同之处在于幕后发生的事情。与C#不同,Java不会去构建一个特殊的 - 它只是使用Java中一直存在的普通旧版本。当你把东西从阵列中拿出来时,通常的选角舞仍然必须完成。编译器为您节省了按键时间,但速度命中/转换仍然像往常一样产生。
当人们提到“类型擦除”时,这就是他们所说的。编译器会为您插入强制转换,然后“擦除”这样一个事实,即它不仅仅是一个列表ListOfPerson
ArrayList
Person p = (Person)foo.get(1);
Person
Object
这种方法的好处是,不理解泛型的旧代码不必关心。它仍然像往常一样处理旧的。这在Java世界中更为重要,因为他们希望支持使用Java 5和泛型编译代码,并使其在旧的1.4或以前的JVM上运行,微软故意决定不打扰。ArrayList
缺点是我之前提到的速度命中,而且因为.class文件中没有伪类或类似的东西,所以稍后查看它的代码(通过反射,或者如果你从另一个集合中将其拉出,它已被转换为等等)无法以任何方式判断它意味着它只是一个包含而不是任何其他数组列表的列表。ListOfPerson
Object
Person
std::list<Person>* foo = new std::list<Person>();
它看起来像C#和Java泛型,它会做你认为它应该做的事情,但在幕后发生了不同的事情。
它与C#泛型最有共同之处,因为它构建了特殊的功能,而不是像java那样只是丢弃类型信息,但它是一个完全不同的鱼缸。pseudo-classes
C# 和 Java 都会生成专为虚拟机设计的输出。如果你编写一些包含类的代码,在这两种情况下,关于类的一些信息都会进入.dll或.class文件,JVM/CLR会用它来做一些事情。Person
Person
C++生成原始 x86 二进制代码。一切都不是对象,也没有底层虚拟机需要了解类。没有装箱或取消装箱,函数不必属于类,或者实际上任何东西。Person
因此,C++编译器对您可以使用模板执行的操作没有任何限制 - 基本上任何可以手动编写的代码,您都可以获得为您编写的模板。
最明显的例子是添加内容:
在 C# 和 Java 中,泛型系统需要知道哪些方法可用于类,并且需要将其传递给虚拟机。告诉它这一点的唯一方法是对实际类进行硬编码,或者使用接口。例如:
string addNames<T>( T first, T second ) { return first.Name() + second.Name(); }
该代码不会在C#或Java中编译,因为它不知道该类型实际上提供了一个名为Name()的方法。你必须告诉它 - 在C#中,像这样:T
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++模板不仅允许编译时类型参数,还允许编译时常量值参数:模板可以作为整数甚至函数签名给出。这意味着你可以在编译时做一些非常时髦的事情,例如计算:
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++模板的另一个显著特征,即模板专用化。该代码定义一个类模板,该模板具有一个值参数。它还为该模板定义了一个特化,每当参数的计算结果为 1 时,都会使用该专用化。这允许我在模板定义上定义递归。我相信这是安德烈·亚历山德斯库首先发现的。product
模板专用化对于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*.