Answers:
我将把声音添加到噪音中,并努力使事情变得清晰:
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
ArrayList<Person> foo = new ArrayList<Person>();
从表面上看,它看起来是一样的。编译器还会阻止您将不在Person
列表中的内容放入列表中。
不同之处在于幕后发生的事情。与C#不同,Java并没有构建特殊的东西ListOfPerson
-它只使用ArrayList
Java中一直存在的普通样式。当您从阵列中取出东西时,Person p = (Person)foo.get(1);
仍然必须执行通常的投射舞蹈。编译器可以节省您的按键操作,但仍然像以前一样,仍然会导致快速命中/广播。
当人们提到“类型删除”时,这就是他们在谈论的内容。编译器会为您插入强制类型转换,然后“擦除”以下事实:它Person
不仅是列表Object
这种方法的好处是,不需要理解泛型的旧代码就不必在意了。它仍然ArrayList
像以前一样处理旧的问题。这在Java世界中更为重要,因为他们希望使用带有泛型的Java 5支持编译代码,并使其在旧版1.4或以前的JVM上运行,Microsoft故意决定不使用它。
缺点是我前面提到的速度下降,也是因为没有ListOfPerson
伪类或类似的东西进入.class文件,以后会对其进行查看的代码(带有反射,或者如果您将其拉出另一个集合)它在什么地方被转换为Object
等等)无法以任何方式告诉它是仅包含Person
而不是仅包含任何其他数组列表的列表。
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()函数,它将进行编译。如果他们不这样做,那就不会。简单。
所以你有它 :-)
int addNames<T>( T first, T second ) { return first + second; }
C#编写的声明。泛型类型可以限制为类而不是接口,并且有一种方法可以在其中声明带有+
运算符的类。
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*.
Anders Hejlsberg自己在这里描述了差异“ C#,Java和C ++中的泛型 ”。
关于区别是什么,已经有了很多很好的答案,所以让我给出一个略有不同的观点并添加原因。
正如已经解释的,主要区别是类型擦除,即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的工作。
ArrayList<T>
可以作为具有(隐藏)静态Class<T>
字段的新的内部命名类型发出。只要新版本的通用库已使用1.5+字节代码进行部署,它将能够在1.4- JVM上运行。
我以前的帖子的后续。
无论使用哪种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
不是函数而是类型,则该语句仍然有效,并执行特殊的强制转换(函数样式强制转换),通常是构造函数调用。编译器无法确定我们的意思,因此我们必须在这里消除歧义。
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,因此在不破坏现有代码的情况下不能更改该定义。因此,就像集合一样,它以这种旧状态保留在核心库中。
ArrayList
到List<T>
,把它变成一个新的命名空间。事实是,如果在源代码中出现一个类,因为ArrayList<T>
它将成为IL代码中由编译器生成的另一个类名,因此不会发生名称冲突。
迟到了11个月,但我认为这个问题已经可以解决Java Wildcard的问题。
这是Java的语法功能。假设您有一个方法:
public <T> void Foo(Collection<T> thing)
并且假设您不需要在方法主体中引用类型T。您先声明一个名称T,然后只使用一次,那么为什么还要考虑一个名称呢?相反,您可以编写:
public void Foo(Collection<?> thing)
问号要求编译器假装您声明了一个普通的命名类型参数,该参数只需要在该位置出现一次即可。
使用通配符无法做的事,也不能使用命名类型参数来进行通配符(这就是在C ++和C#中始终执行这些操作的方式)。
class Foo<T extends List<?>>
并使用,Foo<StringList>
但在C#中,您必须添加额外的类型参数:class Foo<T, T2> where T : IList<T2>
并使用clunky Foo<StringList, String>
。
Wikipedia具有比较Java / C#泛型和Java泛型/ C ++模板的出色文章。关于泛型的主要文章似乎有些混乱,但确实有一些不错的信息。
最大的抱怨是类型擦除。因此,泛型不会在运行时强制执行。 这是一些有关此主题的Sun文档的链接。
泛型通过类型擦除来实现:泛型类型信息仅在编译时出现,此后由编译器擦除。
看起来,在其他一些非常有趣的提议中,有一个关于完善泛型和打破向后兼容性的提议:
当前,泛型使用擦除来实现,这意味着泛型类型信息在运行时不可用,这使得某种代码难以编写。泛型以这种方式实现,以支持与较早的非泛型代码的向后兼容性。修饰的泛型将使泛型类型信息在运行时可用,这将破坏旧的非泛型代码。但是,尼尔·戈夫特(Neal Gafter)提出了仅在指定类型时才使类型可验证,以免破坏向后兼容性。
注意:我没有足够的意见要发表,所以请随时将其作为评论移至适当的答案。
与我永远不知道它来自何处的普遍看法相反,.net在不破坏向后兼容性的情况下实现了真正的泛型,为此他们花费了很多精力。您不必仅将非通用.net 1.0代码更改为通用即可在.net 2.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
,则该如何工作?