C#和Java ...中的泛型与C ++中的模板之间有什么区别?

时间:2020-03-05 18:44:14  来源:igfitidea点击:

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

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

解决方案

回答

维基百科比较Java / Cgenerics和Java泛型/ C ++模板都有不错的文章。关于泛型的主要文章似乎有些混乱,但确实有一些不错的信息。

回答

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

回答

最大的抱怨是类型擦除。因此,泛型不会在运行时强制执行。这是一些有关该主题的Sun文档的链接。

Generics are implemented by type
  erasure: generic type information is
  present only at compile time, after
  which it is erased by the compiler.

回答

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

回答

C ++很少使用泛型术语。而是使用单词模板,并且更加准确。模板描述了一种实现通用设计的技术。

C ++模板与两种Cand 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 ++模板的另一个显着特征,即模板专门化。该代码定义了一个类模板,即具有一个value参数的product。它还为该模板定义了一种特殊化,只要自变量的值为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*.

回答

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

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

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

可以这么说。

回答

我以前的帖子的后续。

无论使用哪种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 &lt;int> :: my_type解析为int,因此b应该与a具有相同的类型,对?

错误的。在编译器尝试解析此语句时,它实际上还不知道Y &lt;int> :: my_type!因此,它不知道这是一种类型。可能是其他情况,例如成员函数或者字段。这可能会引起歧义(尽管在当前情况下不是),因此编译器将失败。我们必须明确地告诉我们,我们引用的是类型名称:

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

现在,代码开始编译。要查看这种情况下如何产生歧义,请考虑以下代码:

Y<int>::my_type(123);

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

回答

Java和Cintroduced泛型在它们的第一种语言发布之后。但是,引入泛型时,核心库的更改方式有所不同。 C#的泛型不只是编译器魔术,因此在不破坏向后兼容性的情况下就不可能生成现有的库类。

例如,在Java中,现有的Collections Framework是完全通用的。 Java没有集合类的通用和遗留非通用版本。在某些方面,如果我们需要在Cthere中使用集合,这实际上会更清洁,实际上没有什么理由使用非泛型版本,但是那些旧类仍然存在,从而使情况更加混乱。

另一个显着区别是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#泛型允许我们声明这样的内容。

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

然后编译器将阻止我们将非"个人"的内容放入列表中。
在后台,Ccompiler只是将List &lt;Person>放到.NET dll文件中,但是在运行时,JIT编译器开始构建新的代码集,就好像我们已经编写了一个用于容纳人的特殊列表类一样。就像" ListOfPerson"一样。

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

缺点是旧的C1.0和1.1代码(在添加泛型之前)不理解这些新的List &lt;something>,因此我们必须手动将其转换回普通的旧List才能与它们进行互操作。这并不是什么大问题,因为C2.0二进制代码不向后兼容。只有将旧的C1.0 / 1.1代码升级到C2.0时,这种情况才会发生

Java泛型允许我们声明这样的内容。

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

从表面上看,它看起来是一样的。编译器还将阻止我们将不是" Person"的内容放入列表中。

不同之处在于幕后发生的事情。与C#不同,Java不会构建特殊的ListOfPerson,而只是使用Java中一直存在的普通的ArrayList。当我们从数组中取出内容时,仍然必须执行通常的" Person p =(Person)foo.get(1);"。编译器为我们节省了按键操作,但仍然像以前一样,仍然会产生快速命中/广播的速度。

当人们提到"类型删除"时,这就是他们在谈论的内容。编译器会为我们插入强制类型转换,然后"擦除"它是" Person"列表而不是" Object"的事实。

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

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

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

缺点是我前面提到的速度下降,也是因为没有ListOfPerson伪类或者.class文件中的类似内容,以后会对其进行查看的代码(带有反射,或者如果我们将其拉出,的另一个集合(已将其转换为"对象"等)无法以任何方式表明它是仅包含"人"而不是仅包含任何其他数组列表的列表。

它看起来像Cand Java泛型,并且会按照我们认为的方式工作,但是在幕后发生了许多事情。

它与Cgenerics的最共同之处在于,它构建特殊的"伪类",而不是像Java一样仅仅丢弃类型信息,但这是一条完全不同的鱼。

两种Cand Java均产生针对虚拟机设计的输出。如果我们编写了其中包含" Person"类的代码,则在两种情况下,有关" Person"类的一些信息都将进入.dll或者.class文件,而JVM / CLR将对此进行处理。

C ++生成原始的x86二进制代码。一切都不是对象,也没有底层虚拟机需要了解Person类。没有装箱或者拆箱,功能不必属于类,甚至不属于任何东西。

因此,C ++编译器对模板的操作没有任何限制,基本上可以手动编写任何代码,我们都可以为自己编写模板。
最明显的示例是添加内容:

string addNames<T>( T first, T second ) { return first.Name() + second.Name(); }

在Cand Java中,泛型系统需要知道可用于类的方法,并将其传递给虚拟机。告诉它的唯一方法是通过对实际的类进行硬编码或者使用接口。例如:

interface IHasName{ string Name(); };
string addNames<T>( T first, T second ) where T : IHasName { .... }

该代码不会在Cor Java中编译,因为它不知道类型T实际上提供了一个名为Name()的方法。我们必须像这样用C告诉它:

然后,我们必须确保传递给addNames的东西实现IHasName接口,依此类推。 Java语法不同(&lt;T扩展了IHasName>),但是它也遇到了同样的问题。

string addNames<T>( T first, T second ) { return first + second; }

此问题的"经典"情况是试图编写一个执行此操作的函数

我们实际上无法编写此代码,因为无法使用其中的" +"方法声明接口。你失败了。

C ++不会遭受这些问题的困扰。如果两个对象都具有.Name()函数,则编译器并不关心将类型传递给任何VM的情况,它将进行编译。如果他们不这样做,那就不会。简单的。

回答

所以你有它 :-)

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

正如已经说明的,主要区别是类型擦除,即Java编译器会擦除通用类型,并且它们不会最终出现在生成的字节码中。但是,问题是:为什么有人会这样做?没道理!还是呢?

好吧,还有什么选择?如果我们不使用该语言实现泛型,那么我们将在哪里实现它们呢?答案是:在虚拟机中。这破坏了向后兼容性。

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

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

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

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

回答

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

Currently, generics are implemented
  using erasure, which means that the
  generic type information is not
  available at runtime, which makes some
  kind of code hard to write. Generics
  were implemented this way to support
  backwards compatibility with older
  non-generic code. Reified generics
  would make the generic type
  information available at runtime,
  which would break legacy non-generic
  code. However, Neal Gafter has
  proposed making types reifiable only
  if specified, so as to not break
  backward compatibility.

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

回答

在Alex Miller的有关Java 7提案的文章中

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

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

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

public void Foo(Collection<?> thing)

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

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

回答

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

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

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

段落数量不匹配