C# 和 Java 中的泛型有什么区别?和 C++ 中的模板?[已结束]

What are the differences between Generics in C# and Java... and Templates in C++? [closed]

提问人:pek 提问时间:8/28/2008 最后编辑:Shog9pek 更新时间:9/26/2013 访问量:60139

问:

9年前关闭。
锁定。这个问题及其答案被锁定,因为这个问题偏离了主题,但具有历史意义。它目前不接受新的答案或互动。

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

那么,泛型中的C++,C#,Java之间的主要区别是什么?每个的优点/缺点?

C# Java C++ 泛型 模板

评论


答:

2赞 travis 8/28/2008 #1

维基百科有很好的文章比较了Java / C#泛型和Java 泛型/ C++模板。关于泛型的主要文章似乎有点杂乱无章,但它确实有一些很好的信息。

35赞 jfs 8/28/2008 #2

Anders Hejlsberg自己在这里描述了“C#,Java和C++中的泛型”。

评论

0赞 Johannes Schaub - litb 4/12/2009
我真的很喜欢那次采访。它让像我这样的非 C# 人员清楚地了解 C# 泛型是怎么回事。
1赞 Matt Cummings 8/28/2008 #3

最大的抱怨是字体擦除。在这种情况下,泛型不会在运行时强制执行。这里有一个链接,指向一些关于这个主题的 Sun 文档

泛型按类型实现 擦除:泛型类型信息是 仅在编译时出现,之后 它被编译器擦除。

1赞 On Freund 8/28/2008 #4

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

61赞 Konrad Rudolph 8/28/2008 #5

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

评论

0赞 supercat 1/7/2013
我有时希望 .net 中的泛型功能可以允许将类型以外的东西用作键。如果值类型数组是框架的一部分(我很惊讶它们在某种程度上不是,因为需要与在结构中嵌入固定大小数组的旧 API 进行交互),那么声明一个包含几个单独项的类,然后声明一个值类型数组,其大小是通用参数。事实上,最接近的方法是有一个类对象,该类对象包含单个项,然后还包含对保存数组的单独对象的引用。
0赞 Konrad Rudolph 1/7/2013
@supercat 如果您与旧版 API 交互,则想法是使用编组(可以通过属性进行注释)。无论如何,CLR 没有固定大小的数组,因此在这里使用非类型模板参数将无济于事。
0赞 supercat 1/7/2013
我想我感到困惑的是,拥有固定大小的值类型数组似乎并不难,并且它允许许多数据类型按引用而不是按值编组。虽然 marshal-by-value 在确实无法以任何其他方式处理的情况下很有用,但我认为 marshal-by-ref 在几乎所有可用的情况下都更胜一筹,因此允许此类情况包含具有固定大小数组的结构似乎是一个有用的功能。
0赞 supercat 1/7/2013
顺便说一句,非类型泛型参数有用的另一种情况是表示尺寸量的数据类型。可以在表示数量的实例中包含维度信息,但是在类型中包含此类信息将允许人们指定集合应该包含表示特定维度单位的对象。
1赞 izb 8/28/2008 #6

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

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

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

可以这么说。

14赞 Konrad Rudolph 8/28/2008 #7

跟进我之前的帖子。

模板是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_typeintba

错。在编译器尝试解析此语句时,它实际上还不知道!因此,它不知道这是一个类型。它可以是其他东西,例如成员函数或字段。这可能会引起歧义(尽管在本例中不是),因此编译器会失败。我们必须明确地告诉它,我们指的是一个类型名称:Y<int>::my_type

X<typename Y<int>::my_type> b;

现在,代码编译。若要了解这种情况如何产生歧义,请考虑以下代码:

Y<int>::my_type(123);

此代码语句完全有效,并告诉 C++ 执行对 的函数调用。但是,如果不是函数而是类型,则此语句仍然有效,并执行特殊的强制转换(函数样式强制转换),这通常是构造函数调用。编译器无法分辨我们的意思,因此我们必须在这里消除歧义。Y<int>::my_typemy_type

评论

2赞 Benoît 2/19/2009
我完全同意。不过,还是有一些希望的。自动完成系统和 C++ 编译器必须非常紧密地交互。我很确定 Visual Studio 永远不会有这样的功能,但是在 Eclipse/CDT 或其他基于 GCC 的 IDE 中可能会发生一些事情。希望!:)
6赞 serg10 8/28/2008 #8

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# 中,因此在不破坏现有代码的情况下无法更改定义。因此,与集合一样,它以这种旧状态保留在核心库中。

评论

0赞 Earth Engine 10/12/2012
甚至 C# 的泛型也不仅仅是编译器的魔术,编译器还可以做进一步的魔术来生成现有的库。他们没有理由需要重命名并将其放入新的命名空间中。事实是,如果源代码中出现了一个类,因为它将成为IL代码中不同的编译器生成的类名,因此不会发生名称冲突。ArrayListList<T>ArrayList<T>
362赞 Orion Edwards 8/28/2008 #9

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

C# 泛型允许您声明如下所示的内容。

List<Person> foo = new List<Person>();

然后编译器会阻止你把没有的东西放到列表中。
在后台,C# 编译器只是放入 .NET dll 文件中,但在运行时,JIT 编译器会生成一组新的代码,就好像您编写了一个专门用于包含人员的特殊列表类一样 - 类似于 .
PersonList<Person>ListOfPerson

这样做的好处是它使它变得非常快。没有强制转换或任何其他东西,并且由于 dll 包含这是 List 的信息,因此稍后使用反射查看它的其他代码可以告诉它包含对象(因此您可以获得智能感知等)。PersonPerson

这样做的缺点是旧的 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 中一直存在的普通旧版本。当你把东西从阵列中拿出来时,通常的选角舞仍然必须完成。编译器为您节省了按键时间,但速度命中/投射仍然像往常一样发生。
当人们提到“类型擦除”时,这就是他们在谈论的。编译器会为您插入强制转换,然后“删除”它不仅仅是一个列表的事实
ListOfPersonArrayListPerson p = (Person)foo.get(1);PersonObject

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

缺点是我之前提到的速度受到打击,也因为 .class 文件中没有伪类或类似的东西,稍后查看它的代码(通过反射,或者如果您将其从另一个已转换为的集合中提取)无法以任何方式告诉它意味着仅包含而不是任何其他数组列表。ListOfPersonObjectPerson

C++ 模板允许您声明如下内容

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

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

它与 C# 泛型最大的共同点在于它构建特殊而不是像 java 那样丢弃类型信息,但它是一个完全不同的鱼缸。pseudo-classes

C# 和 Java 都生成专为虚拟机设计的输出。如果您编写一些包含类的代码,在这两种情况下,有关类的一些信息都将进入 .dll 或 .class 文件,JVM/CLR 将对此执行操作。PersonPerson

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() 函数,它将编译。如果他们不这样做,它就不会。简单。

所以,你有它:-)

评论

8赞 Piotr Czapla 8/13/2009
在 C# 中为引用类型生成的伪函数共享相同的实现,因此你不会得到完全相同的 ListOfPeople。查看 blogs.msdn.com/ericlippert/archive/2009/07/30/...
4赞 finnw 2/4/2010
不可以,您不能使用泛型编译 Java 5 代码,也不能让它在旧的 1.4 VM 上运行(至少 Sun JDK 没有实现这一点。一些第三方工具可以。您可以做的是使用以前编译的 1.4/1.6 代码中的 1.5 JAR。
4赞 Mashmagar 7/20/2011
我反对你不能用 C# 编写的说法。泛型类型可以限制为类而不是接口,并且有一种方法可以声明一个带有运算符的类。int addNames<T>( T first, T second ) { return first + second; }+
4赞 Orion Edwards 6/5/2012
@AlexanderMalakhov这是故意的。重点不是要教育C++的习语,而是说明每种语言如何以不同的方式处理看起来相同的代码段。这个目标越难实现,代码看起来越不同
3赞 Orion Edwards 3/26/2013
@phresnel 我原则上同意,但是如果我用惯用C++编写该片段,则 C#/Java 开发人员将不太容易理解,因此(我相信)在解释差异方面会做得更糟。让我们同意不同意这一点:-)
18赞 Jörg W Mittag 8/29/2008 #10

关于差异是什么,已经有很多很好的答案,所以让我给出一个稍微不同的观点并添加原因

如前所述,主要区别在于类型擦除,即 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。

评论

3赞 Richard Berg 7/31/2009
LINQ 主要是一个库功能(尽管 C# 和 VB 也添加了语法糖)。任何面向 2.0 CLR 的语言都可以通过加载 System.Core 程序集来充分利用 LINQ。
0赞 Jörg W Mittag 7/31/2009
是的,对不起,我应该更清楚。LINQ.我指的是查询语法,而不是一元标准查询运算符、LINQ 扩展方法或 IQueryable 接口。显然,您可以使用任何 .NET 语言中的它们。
1赞 Earth Engine 10/12/2012
我正在考虑 Java 的另一种选择。即使 Oracle 不想破坏向后兼容性,他们仍然可以使用一些编译器技巧来避免类型信息被擦除。例如,可以作为具有(隐藏)静态字段的新内部命名类型发出。只要新版本的通用库已经部署了 1.5+ 字节码,它就可以在 1.4- JVM 上运行。ArrayList<T>Class<T>
1赞 pek 9/2/2008 #11

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

目前,泛型已实现 使用擦除,这意味着 泛型类型信息不是 在运行时可用,这使得一些 那种很难写的代码。泛 型 以这种方式实现以支持 向后兼容较旧的 非泛型代码。Reified 泛型 将使泛型类型 运行时可用的信息, 这将破坏传统的非通用性 法典。然而,尼尔·古夫特(Neal Gafter)有 建议使类型只能重新定义 如果指定,以免中断 向后兼容性。

Alex Miller 关于 Java 7 提案的文章中

4赞 Daniel Earwicker 7/10/2009 #12

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

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

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

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

public void Foo(Collection<?> thing)

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

对于通配符,您不能使用命名类型参数执行任何操作(这是 C++ 和 C# 中始终执行这些操作的方式)。

评论

2赞 R. Martinho Fernandes 6/4/2010
又晚了 11 个月......有些事情你可以用 Java 通配符做,而你不能用命名类型参数做。您可以在 Java: 和 use 中执行此操作,但在 C# 中,您必须添加额外的类型参数: 并使用 clunky .class Foo<T extends List<?>>Foo<StringList>class Foo<T, T2> where T : IList<T2>Foo<StringList, String>
0赞 Sheepy 8/3/2010 #13

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

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

因此,我不认为 java 必须打破向后兼容性来支持真正的泛型。

评论

0赞 tzaman 8/4/2010
这不是人们谈论的那种向后兼容性。这个想法是运行时的向后兼容性:在 .NET 2.0 中使用泛型编写的代码不能在旧版本的 .NET 框架/CLR 上运行。类似地,如果 Java 要引入“true”泛型,则较新的 Java 代码将无法在较旧的 JVM 上运行(因为它需要对字节码进行重大更改)。
0赞 Sheepy 8/4/2010
那是 .net,不是泛型。始终需要重新编译以面向特定的 CLR 版本。有字节码兼容性,有代码兼容性。而且,我特别回答了需要将使用旧 List 的旧代码转换为使用新泛型列表的问题,这根本不是真的。
1赞 Sheepy 8/4/2010
我认为人们正在谈论向前兼容性。即在 .net 1.1 上运行的 .net 2.0 代码,这将中断,因为 1.1 运行时对 2.0“伪类”一无所知。难道不应该是“java 没有实现真正的泛型,因为他们想保持向前兼容性”吗?(而不是落后)
0赞 supercat 1/7/2013
兼容性问题很微妙。我不认为问题在于向 Java 添加“真正的”泛型会影响任何使用旧版本 Java 的程序,而是使用“新改进”泛型的代码将很难将此类对象与对新类型一无所知的旧代码交换。例如,假设一个程序有一个它想要传递给一个较旧的方法,该方法应该用 的实例填充 。如果 an 不是 ,如何让它工作?ArrayList<Foo>ArrayListFooArrayList<foo>ArrayList