我正在阅读Herbert Schildt的C#完整参考书。他说,关于泛型,
只有一个版本的泛型类可以处理所有 type参数是引用类型的情况。这是因为 所有引用的大小(以字节为单位)是相同的。因此,只有一个 需要版本来处理所有类型的引用。这种优化 也减少了代码膨胀。
有人可以解释为什么所有引用类型只有一个版本的泛型类? C#如何仅使用一个版本进行管理?例如:如果是值类型,本书说,C#为import tkinter as tk
root = tk.Tk()
vars = {}
frame = tk.Frame(root)
frame.pack(side="top", fill="x", padx=10, pady=10)
for color in ("red", "orange", "green", "blue", "indigo", "violet"):
checkbutton = tk.Checkbutton(frame, onvalue=1, offvalue=0,
text=color, indicatoron=False,
width=6)
checkbutton.pack(side="left", fill="x", expand=True)
root.mainloop()
和Generic<int>
创建了一个单独的类。那么,为什么不为Generic<double>
和Generic<MyClass>
创建单独的类?什么没有创建-seperate类的泛型引用类型与类的大小(如文中所述)?
答案 0 :(得分:8)
首先,正如我在评论中所说:得到一本更好的书。
其次,您需要了解如何为泛型生成代码。 C#编译器生成IL - 一种&#34;中间语言&#34; - 对于每个泛型类,IL与原始源代码一样,是真正通用的。在运行时,抖动(&#34;及时&#34;编译器)为您调用的每个方法生成机器代码;它将IL转换为您正在运行的任何机器的代码。
假设您有一个带有方法的泛型类:
class C<T>
{
public static T[] One(T item)
{
return new T[] { item };
}
}
请注意,无论T在运行时是什么,这种方法都是类型安全的。
当您调用C<string>.One("hello");
时,抖动会生成创建一个字符串引用数组的代码,将给定的字符串引用复制到数组的第一个位置,并返回对该数组的引用。
现在假设您致电C<string>.One("goodbye")
。抖动不需要重新生成代码,因为它已经在内存中有机器代码。
现在,如果你调用C<Task<Giraffe>>.One(giraffeTask)
,那么抖动 不需要重新生成代码,因为它已经在内存中创建了一个方法来创建一个引用大小的数组事物,将item
中的引用复制到数组中,并返回对该数组的引用。无处 - 除了T []创建 - 该方法使用的事实是T实际上是一个字符串或实际上是产生长颈鹿的任务。如果我们确保数组构造函数也没有利用知道类型是什么 - 为什么要这样做? - 然后我们就可以重新使用生成的代码。
但是,当调用C<int>.One
时,我们无法重复使用为C<double>.One
生成的代码,因为第一个将4字节整数复制到数组中;第二个复制8字节双。通过占用额外的空间来制作两个略有不同版本的机器代码,抖动可以更好地优化代码。
此优化是一个实现细节,但它是有助于了解的细节之一,因为它为您提供了一个很好的心理模型,说明了该功能如何工作,为什么它以这种方式工作,以及它与类似功能的不同之处不同的语言。 Java(带有擦除)和C ++(带有模板)与C#的真正的运行时泛型类型系统略有不同,这会导致语义和性能差异。
答案 1 :(得分:4)
我会说引用的文字不太准确。作者似乎试图解释,对于引用类型,类的代码只需要存在一次。 不是真的,你实际上只得到一个类的副本。
对于值类型,类中给定类型参数的元素所需的存储可能因类型而异,因为值类型的变量是实际存储数据的位置。这意味着必须针对所使用的每种值类型重新编译通用代码,以确保使用正确的存储大小。
对于参考类型,所需的存储只是一个参考。实际数据存储在别处(即堆中),并且无论类型如何,引用的大小都相同。所以代码的一个版本就足够了。
从这个意义上讲,引用的文字是正确的。
但是:泛型类型仍然知道它们的类型参数,运行时仍然会对值进行运行时类型检查。此外,具有静态字段的类将需要为该类使用的每个类型参数组合的那些字段的新实例。所以我不会说,即使对于引用类型,“只有一个版本的泛型类也是如此。