C#和Java…中的泛型与C ++中的模板之间有什么区别?[关闭]


203

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

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

Answers:


364

我将把声音添加到噪音中,并努力使事情变得清晰:

C#泛型允许您声明这样的内容。

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

然后编译器将阻止您将不在Person列表中的内容放入列表中。
在后台,C#编译器只是将其List<Person>放入.NET dll文件中,但是在运行时,JIT编译器会继续构建新的代码集,就好像您已经编写了一个用于容纳人员的特殊列表类一样(例如)ListOfPerson

这样做的好处是它使速度变得非常快。没有强制转换或其他任何内容,并且由于dll包含这是List的信息,Person因此稍后使用反射查看它的其他代码可以告诉它包含Person对象(因此您会得到intellisense等)。

缺点是旧的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列表中的内容放入列表中。

不同之处在于幕后发生的事情。与C#不同,Java并没有构建特殊的东西ListOfPerson-它只使用ArrayListJava中一直存在的普通样式。当您从阵列中取出东西时,Person p = (Person)foo.get(1);仍然必须执行通常的投射舞蹈。编译器可以节省您的按键操作,但仍然像以前一样,仍然会导致快速命中/广播。
当人们提到“类型删除”时,这就是他们在谈论的内容。编译器会为您插入强制类型转换,然后“擦除”以下事实:它Person不仅是列表Object

这种方法的好处是,不需要理解泛型的旧代码就不必在意了。它仍然ArrayList像以前一样处理旧的问题。这在Java世界中更为重要,因为他们希望使用带有泛型的Java 5支持编译代码,并使其在旧版1.4或以前的JVM上运行,Microsoft故意决定不使用它。

缺点是我前面提到的速度下降,也是因为没有ListOfPerson伪类或类似的东西进入.class文件,以后会对其进行查看的代码(带有反射,或者如果您将其拉出另一个集合)它在什么地方被转换为Object等等)无法以任何方式告诉它是仅包含Person而不是仅包含任何其他数组列表的列表。

C ++模板允许您声明这样的内容

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

所以你有它 :-)


8
为C#中的引用类型生成的伪类具有相同的实现,因此您将不会获得确切的ListOfPeople。退房blogs.msdn.com/ericlippert/archive/2009/07/30/...
彼得·Czapla

4
不,您不能使用泛型编译Java 5代码,并且不能在旧的1.4 VM上运行Java 5代码(至少Sun JDK不能实现这一点。某些第三方工具可以这样做。)您可以做的是使用以前编译的1.4 JAR 1.5 / 1.6代码。
finnw

4
我反对您不能用int addNames<T>( T first, T second ) { return first + second; }C#编写的声明。泛型类型可以限制为类而不是接口,并且有一种方法可以在其中声明带有+运算符的类。
Mashmagar

4
@AlexanderMalakhov这不是故意的。重点不是要教育C ++的习语,而是要说明每种语言如何以不同的方式处理看似相同的代码。这个目标本来很难实现更多不同的代码外观
Orion Edwards 2012年

3
@phresnel我原则上同意,但是如果我用惯用的C ++编写该代码段,那么C#/ Java开发人员将很难理解它,因此(我相信)在解释差异方面做得更糟。让我们不同意这一点:-)
Orion Edwards

61

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该模板具有一个value参数。它还为该模板定义了一种特殊化,只要参数的计算结果为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*.

有时我希望.net中的泛型功能可以允许将类型以外的内容用作键。如果值类型数组是框架的一部分(令我惊讶的是,鉴于需要与在结构中嵌入固定大小数组的较旧API进行交互的方式),则声明一个类,其中包含一些单独的项,然后是一个值类型数组,其大小是通用参数。实际上,最接近的一个类是拥有一个类对象,该类对象保存各个项目,然后还保存对包含数组的单独对象的引用。
2013年

@supercat如果您与旧版API交互,则其想法是使用编组(可通过属性进行注释)。CLR始终没有固定大小的数组,因此在这里使用非类型模板参数将毫无帮助。
康拉德·鲁道夫2013年

我想我感到困惑的是,看起来像固定大小的值类型数组应该不难,而且它允许许多数据类型按引用而不是按值编组。虽然按值编组在真正无法以其他任何方式处理的情况下很有用,但我认为几乎在所有可用的情况下按引用编组都比较好,因此允许此类情况包含固定结构大小的数组似乎是一个有用的功能。
2013年

顺便说一句,非类型通用参数将有用的另一种情况是表示量纲的数据类型。可以在表示数量的实例中包括尺寸信息,但是将这种信息包含在一种类型中将允许人们指定一个集合应该保存代表特定尺寸单位的对象。
2013年


18

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

正如已经解释的,主要区别是类型擦除,即Java编译器擦除通用类型并且它们不会以生成的字节码结尾的事实。但是,问题是:为什么有人会这样做?没道理!还是呢?

好吧,还有什么选择?如果不落实在语言的仿制药,在那里你实现它们?答案是:在虚拟机中。这破坏了向后兼容性。

另一方面,类型擦除允许您将通用客户端与非通用库混合使用。换句话说:在Java 5上编译的代码仍然可以部署到Java 1.4。

但是,Microsoft决定打破对泛型的向后兼容性。就是.NET泛型比Java泛型“更好” 原因。

当然,Sun不是白痴或co夫。他们之所以“大吃一惊”,是因为Java在引入泛型时比.NET明显更老,而且更广泛。(它们在两个世界中几乎同时被引入。)打破向后兼容性将是一个巨大的痛苦。

换句话说,在Java中,泛型是语言的一部分(这意味着它们适用于Java,不适用于其他语言),在.NET中,它们是虚拟机的一部分(这意味着它们适用于所有语言,而不适用于所有语言)。只是C#和Visual Basic.NET)。

将此与LINQ,lambda表达式,局部变量类型推断,匿名类型和表达式树等.NET功能进行比较:这些都是语言功能。这就是VB.NET和C#之间存在细微差异的原因:如果这些功能是VM的一部分,则所有语言的功能都相同。但是CLR并没有改变:.NET 3.5 SP1中的CLR与.NET 2.0中的CLR相同。如果不使用任何.NET 3.5库,则可以编译将LINQ与.NET 3.5编译器一起使用的C#程序,并且仍可以在.NET 2.0上运行它。这将使用泛型和.NET 1.1的工作,但它与Java和Java 1.4的工作。


3
LINQ主要是一种库功能(尽管C#和VB还在其旁边添加了语法糖)。只需加载System.Core程序集,任何针对2.0 CLR的语言都可以充分利用LINQ。
理查德·伯格

是的,对不起,我应该更加清楚了。LINQ。我指的是查询语法,而不是单子标准查询运算符,LINQ扩展方法或IQueryable接口。显然,您可以使用任何.NET语言中的语言。
约尔格W¯¯米塔格

1
我在考虑Java的另一种选择。即使Oracle也不希望破坏向后兼容性,它们仍然可以做出一些编译器技巧,以避免擦除类型信息。例如,ArrayList<T>可以作为具有(隐藏)静态Class<T>字段的新的内部命名类型发出。只要新版本的通用库已使用1.5+字节代码进行部署,它将能够在1.4- JVM上运行。
Earth Engine

14

我以前的帖子的后续。

无论使用哪种IDE,模板都是C ++在智能感知中如此失败的主要原因之一。由于模板专门化,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不是函数而是类型,则该语句仍然有效,并执行特殊的强制转换(函数样式强制转换),通常是构造函数调用。编译器无法确定我们的意思,因此我们必须在这里消除歧义。


2
我非常同意。不过还是有希望的。自动完成系统和C ++编译器必须非常紧密地交互。我很确定Visual Studio永远不会有这样的功能,但是事情可能会在Eclipse / CDT或其他基于GCC的IDE中发生。希望!:)
伯努瓦

6

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


甚至C#的泛型不只是编译器魔术,编译器还可以做进一步的魔术来泛化现有的库。没有任何原因,他们需要重命名ArrayListList<T>,把它变成一个新的命名空间。事实是,如果在源代码中出现一个类,因为ArrayList<T>它将成为IL代码中由编译器生成的另一个类名,因此不会发生名称冲突。
Earth Engine

4

迟到了11个月,但我认为这个问题已经可以解决Java Wildcard的问题。

这是Java的语法功能。假设您有一个方法:

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

并且假设您不需要在方法主体中引用类型T。您先声明一个名称T,然后只使用一次,那么为什么还要考虑一个名称呢?相反,您可以编写:

public void Foo(Collection<?> thing)

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

使用通配符无法做的事,也不能使用命名类型参数来进行通配符(这就是在C ++和C#中始终执行这些操作的方式)。


2
再晚11个月...使用Java通配符可以执行某些操作,而使用命名类型参数则不能。您可以在Java中执行此操作:class Foo<T extends List<?>>并使用,Foo<StringList>但在C#中,您必须添加额外的类型参数:class Foo<T, T2> where T : IList<T2>并使用clunky Foo<StringList, String>
R. Martinho Fernandes



1

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


1

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

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

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

可以这么说。


1

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

当前,泛型使用擦除来实现,这意味着泛型类型信息在运行时不可用,这使得某种代码难以编写。泛型以这种方式实现,以支持与较早的非泛型代码的向后兼容性。修饰的泛型将使泛型类型信息在运行时可用,这将破坏旧的非泛型代码。但是,尼尔·戈夫特(Neal Gafter)提出了仅在指定类型时才使类型可验证,以免破坏向后兼容性。

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


0

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

与我永远不知道它来自何处的普遍看法相反,.net在不破坏向后兼容性的情况下实现了真正的泛型,为此他们花费了很多精力。您不必仅将非通用.net 1.0代码更改为通用即可在.net 2.0中使用。.Net Framework 2.0甚至在4.0之前,通用列表和非通用列表都仍然可用,正是出于向后兼容的原因。因此,仍然使用非通用ArrayList的旧代码仍然可以使用,并且使用与以前相同的ArrayList类。从1.0至今一直保持向后代码兼容性...因此,即使在.net 4.0中,如果选择这样做,您仍然必须选择使用1.0 BCL中的任何非泛型类。

因此,我认为Java不必打破向后兼容性即可支持真正的泛型。


人们谈论的不是那种向后兼容。这个想法是对运行时的向后兼容性:在.NET 2.0中使用泛型编写的代码不能在.NET Framework / CLR的较早版本上运行。同样,如果Java要引入“真正的”泛型,则新的Java代码将无法在较旧的JVM上运行(因为它需要中断对字节码的更改)。
tzaman 2010年

那是.net,不是通用名称。始终需要重新编译才能定位特定的CLR版本。有字节码兼容性,有代码兼容性。而且,我特别回答了有关将使用旧List的旧代码转换为使用新泛型List的需求,这根本不是真的。
羊皮的

1
我认为人们正在谈论向前兼容。即.net 2.0代码要在.net 1.1上运行,这会中断,因为1.1运行时对2.0“伪类”一无所知。难道不是因为“ Java没有实现真正的泛型是因为他们想保持向前的兼容性”吗?(而不是向后)
羊皮

兼容性问题很微妙。我认为问题不在于在Java中添加“真实”泛型会影响使用旧版Java的任何程序,但是使用“新改进”泛型的代码将很难与此类旧对象交换此类对象。对新类型一无所知。举例来说,假设某个程序ArrayList<Foo>要传递给的旧方法,该方法应该ArrayList使用实例填充Foo。如果ArrayList<foo>不是ArrayList,则该如何工作?
2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.