提问人:pek 提问时间:8/28/2008 最后编辑:Shog9pek 更新时间:9/26/2013 访问量:60139
C# 和 Java 中的泛型有什么区别?和 C++ 中的模板?[已结束]
What are the differences between Generics in C# and Java... and Templates in C++? [closed]
问:
我主要使用 Java,泛型相对较新。我一直读到Java做出了错误的决定,或者.NET有更好的实现等等。
那么,泛型中的C++,C#,Java之间的主要区别是什么?每个的优点/缺点?
答:
维基百科有很好的文章比较了Java / C#泛型和Java 泛型/ C++模板。关于泛型的主要文章似乎有点杂乱无章,但它确实有一些很好的信息。
Anders Hejlsberg自己在这里描述了“C#,Java和C++中的泛型”。
评论
最大的抱怨是字体擦除。在这种情况下,泛型不会在运行时强制执行。这里有一个链接,指向一些关于这个主题的 Sun 文档。
泛型按类型实现 擦除:泛型类型信息是 仅在编译时出现,之后 它被编译器擦除。
C++ 模板实际上比 C# 和 Java 模板强大得多,因为它们在编译时进行评估并支持专业化。这允许模板元编程,并使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++ 模板的另一个显著功能,即模板专用化。该代码定义一个类模板,该模板具有一个值参数。它还为该模板定义了一个专用化,每当参数的计算结果为 1 时,就会使用该专用化。这允许我定义模板定义的递归。我相信这是安德烈·亚历山德雷斯库首先发现的。product
模板专用化对 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*.
评论
在 Java 中,泛型仅是编译器级别的,因此您可以获得:
a = new ArrayList<String>()
a.getClass() => ArrayList
请注意,“a”的类型是数组列表,而不是字符串列表。因此,香蕉列表的类型等于()猴子列表。
可以这么说。
跟进我之前的帖子。
模板是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 很难在这一点上说出成员是否以及成员拥有什么。对于其他语言,解析会很简单,但对于C++,需要事先进行相当多的评估。a
情况变得更糟。如果也在类模板中定义了呢?现在,它的类型将取决于另一个类型参数。在这里,即使是编译器也无法实现。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# 中,因此在不破坏现有代码的情况下无法更改定义。因此,与集合一样,它以这种旧状态保留在核心库中。
评论
ArrayList
List<T>
ArrayList<T>
我会把我的声音加入到噪音中,并尝试把事情说清楚:
C# 泛型允许您声明如下所示的内容。
List<Person> foo = new List<Person>();
然后编译器会阻止你把没有的东西放到列表中。
在后台,C# 编译器只是放入 .NET dll 文件中,但在运行时,JIT 编译器会生成一组新的代码,就好像您编写了一个专门用于包含人员的特殊列表类一样 - 类似于 .Person
List<Person>
ListOfPerson
这样做的好处是它使它变得非常快。没有强制转换或任何其他东西,并且由于 dll 包含这是 List 的信息,因此稍后使用反射查看它的其他代码可以告诉它包含对象(因此您可以获得智能感知等)。Person
Person
这样做的缺点是旧的 C# 1.0 和 1.1 代码(在它们添加泛型之前)无法理解这些新代码,因此您必须手动将内容转换回普通旧代码才能与它们进行互操作。这不是一个大问题,因为 C# 2.0 二进制代码不向后兼容。唯一会发生这种情况的情况是将一些旧的 C# 1.0/1.1 代码升级到 C# 2.0List<something>
List
Java 泛型允许您声明类似这样的东西。
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
C++ 模板允许您声明如下内容
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() 函数,它将编译。如果他们不这样做,它就不会。简单。
所以,你有它:-)
评论
int addNames<T>( T first, T second ) { return first + second; }
+
关于差异是什么,已经有很多很好的答案,所以让我给出一个稍微不同的观点并添加原因。
如前所述,主要区别在于类型擦除,即 Java 编译器擦除泛型类型,并且它们不会最终出现在生成的字节码中。然而,问题是:为什么有人会这样做?这没有意义!或者是吗?
那么,有什么选择呢?如果你没有在语言中实现泛型,你在哪里实现它们?答案是:在虚拟机中。这破坏了向后兼容性。
另一方面,类型擦除允许您将泛型客户端与非泛型库混合使用。换句话说:在 Java 5 上编译的代码仍然可以部署到 Java 1.4 中。
然而,Microsoft决定打破泛型的向后兼容性。这就是 .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 中相同。您可以编译将 LINQ 与 .NET 3.5 编译器一起使用的 C# 程序,并且仍然可以在 .NET 2.0 上运行它,前提是您不使用任何 .NET 3.5 库。这不适用于泛型和 .NET 1.1,但它适用于 Java 和 Java 1.4。
评论
ArrayList<T>
Class<T>
看起来,在其他非常有趣的建议中,有一个关于改进泛型和打破向后兼容性的建议:
目前,泛型已实现 使用擦除,这意味着 泛型类型信息不是 在运行时可用,这使得一些 那种很难写的代码。泛 型 以这种方式实现以支持 向后兼容较旧的 非泛型代码。Reified 泛型 将使泛型类型 运行时可用的信息, 这将破坏传统的非通用性 法典。然而,尼尔·古夫特(Neal Gafter)有 建议使类型只能重新定义 如果指定,以免中断 向后兼容性。
在 Alex Miller 关于 Java 7 提案的文章中
晚了 11 个月,但我认为这个问题已经为一些 Java 通配符的东西做好了准备。
这是 Java 的语法特性。假设你有一个方法:
public <T> void Foo(Collection<T> thing)
假设您不需要在方法主体中引用类型 T。你声明了一个名字 T,然后只用了一次,那么你为什么要为它想一个名字呢?相反,您可以编写:
public void Foo(Collection<?> thing)
问号要求编译器假装您声明了一个普通的命名类型参数,该参数只需要在该位置出现一次。
对于通配符,您不能使用命名类型参数执行任何操作(这是 C++ 和 C# 中始终执行这些操作的方式)。
评论
class Foo<T extends List<?>>
Foo<StringList>
class Foo<T, T2> where T : IList<T2>
Foo<StringList, String>
注意:我没有足够的观点来评论,所以请随时将其作为评论移至适当的答案。
与流行的看法相反,我永远不明白它来自哪里,.net 在不破坏向后兼容性的情况下实现了真正的泛型,他们为此付出了明确的努力。 您不必为了在 .net 2.0 中使用而将非泛型 .net 1.0 代码更改为泛型代码。泛型和非泛型列表在 .Net Framework 2.0 中仍然可用,直到 4.0 为止,这完全是出于向后兼容性的原因。因此,仍然使用非泛型 ArrayList 的旧代码仍然有效,并且使用与以前相同的 ArrayList 类。 从 1.0 到现在,向后代码兼容性始终保持......因此,即使在 .net 4.0 中,如果您选择使用 1.0 BCL 中的任何非泛型类,您仍然必须选择这样做。
因此,我不认为 java 必须打破向后兼容性来支持真正的泛型。
评论
ArrayList<Foo>
ArrayList
Foo
ArrayList<foo>
ArrayList
评论