什么是物化?
我知道Java使用擦除来实现参数化多态(generics)。 我明白什么是删除。
我知道C#通过实现实现参数化多态性。 我知道,可以让你写
public void dosomething(List<String> input) {} public void dosomething(List<Int> input) {}
或者你可以在运行时知道某些参数化types的types参数是什么,但我不明白它是什么。
- 什么是物化types?
- 什么是物化价值?
- 当一个types/值被通用时会发生什么?
物化是抽象的东西,创造具体的东西的过程。
C#generics中的术语泛化是指将genericstypes定义和一个或多个genericstypes参数 (抽象事物)相结合以创build新的genericstypes (具体事物)的过程。
用不同的方式来描述,就是把List<T>
和int
定义为一个具体的List<int>
types的过程。
要进一步了解它,请比较以下方法:
-
在Javagenerics中,genericstypes定义被转换为基本上跨所有允许的types参数组合共享的一个具体genericstypes。 因此,多个(源代码级别)types被映射到一个(二进制级别)types – 但是作为结果, 关于实例的types参数的信息在该实例中被丢弃(types擦除) 。
- 作为这种实现技术的副作用,原生允许的唯一通用types参数是那些可以共享其具体types的二进制代码的types; 这意味着那些存储位置可以互换的types; 这意味着引用types。 使用值types作为genericstypes参数需要装箱 (将它们放在一个简单的引用types包装中)。
- 没有代码是重复的,以此来实现generics。
- 在运行时(使用reflection)可能已经可用的types信息丢失。 这反过来又意味着genericstypes的专门化(对于任何特定的generics参数组合使用专门的源代码的能力)是非常有限的。
- 该机制不需要运行时环境的支持。
- 有几种解决方法可以保留 Java程序或基于JVM的语言可以使用的types信息 。
-
在C#generics中,genericstypes定义在运行时保存在内存中。 无论何时需要新的具体types,运行时环境都会将genericstypes定义和types参数结合起来,并创build新的types(实例)。 所以我们在运行时为每个types参数的组合获得一个新types。
- 这种实现技术允许任何种类的参数组合被实例化。 使用值types作为genericstypes参数不会导致装箱,因为这些types得到自己的实现。 (当然, 拳击仍然存在于C#中 ,但是在其他场景中却不是这样)。
- 代码重复可能是一个问题 – 但实际上并不是这样,因为足够智能的实现( 包括Microsoft .NET和Mono )可以共享某些实例的代码。
- 维护types信息,通过使用reflection来检查types参数,从而在一定程度上允许专业化。 然而,专业化的程度是有限的,这是由于genericstypes定义在任何泛化发生之前被编译的结果(这是通过针对types参数的约束编译定义来完成的 – 因此, 编译器必须能够即使没有特定types的参数,也要“理解”定义 )。
- 这种实现技术在很大程度上依赖于运行时支持和JIT编译(这就是为什么你经常听说C#generics在像iOS这样的dynamic代码生成受到限制的平台上有一些限制)。
- 在C#generics的上下文中,通过运行时环境为您完成具体化。 但是,如果您想更直观地理解genericstypes定义和具体genericstypes之间的区别,则可以始终使用
System.Type
类自行执行泛化(即使特定的genericstypes参数组合实例化没有直接出现在你的源代码中)。
-
在C ++模板中,模板定义在编译时保存在内存中。 只要源代码中需要新的实例化模板types,编译器就会将模板定义和模板参数结合起来,并创build新的types。 所以我们在编译时为模板参数的每个组合获得一个唯一的types。
- 这种实现技术允许任何种类的参数组合被实例化。
- 这是众所周知的复制二进制代码,但一个足够聪明的工具链仍然可以检测到这个和共享代码的一些实例。
- 模板定义本身不是“编译”的 – 只有其具体的实例才被编译 。 这对编译器的限制较less,并允许更大程度的模板专门化 。
- 由于模板实例是在编译时执行的,所以在这里也不需要运行时支持。
- 这个过程最近被称为monomorphization ,特别是在Rust社区。 这个词与参数多态相反,这是generics来自于这个概念的名称。
物化一般意味着(在计算机科学之外)“做出真正的事情”。
在编程中,如果我们能够在语言本身中获得关于它的信息,那么就是一个事实。
对于两个完全与非generics有关的C#所做的事情并没有具体化的例子,让我们来看看方法和内存访问。
面向对象的语言一般都有方法 ,(许多没有类似的function ,虽然没有绑定到一个类)。 因此,您可以用这样的语言定义一个方法,调用它,也许覆盖它,等等。 并不是所有这些语言都可以让你真正把这个方法本身作为数据处理到程序中。 C#(实际上,.NET而不是C#)确实可以让你使用表示方法的MethodInfo
对象,所以在C#中方法被实现了。 C#中的方法是“头等对象”。
所有实用的语言都有一些手段来访问计算机的内存。 在像C这样的低级语言中,我们可以直接处理计算机使用的数字地址之间的映射关系,所以int* ptr = (int*) 0xA000000; *ptr = 42;
int* ptr = (int*) 0xA000000; *ptr = 42;
是合理的(只要我们有一个很好的理由怀疑以这种方式访问内存地址0xA000000
不会吹起来的东西)。 在C#中,这是不合理的(我们可以用.NET来强制它,但是.NET内存pipe理移动的东西不太可能有用)。 C#没有指定内存地址。
所以,由于被重复的意思是“真实的”,“实体types”是我们可以用所谈论的语言“谈论”的一种types。
在generics中,这意味着两件事情。
一个是List<string>
是一个types,就像string
或int
是一样的。 我们可以比较这种types,得到它的名字,并询问它:
Console.WriteLine(typeof(List<string>).FullName); // System.Collections.Generic.List`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]] Console.WriteLine(typeof(List<string>) == (42).GetType()); // False Console.WriteLine(typeof(List<string>) == Enumerable.Range(0, 1).Select(i => i.ToString()).ToList().GetType()); // True Console.WriteLine(typeof(List<string>).GenericTypeArguments[0] == typeof(string)); // True
这样做的结果是我们可以在方法本身中“谈论”generics方法(或generics类的方法)参数types:
public static void DescribeType<T>(T element) { Console.WriteLine(typeof(T).FullName); } public static void Main() { DescribeType(42); // System.Int32 DescribeType(42L); // System.Int64 DescribeType(DateTime.UtcNow); // System.DateTime }
通常这样做太“臭”了,但是它有很多有用的例子。 例如,看看:
public static TSource Min<TSource>(this IEnumerable<TSource> source) { if (source == null) throw Error.ArgumentNull("source"); Comparer<TSource> comparer = Comparer<TSource>.Default; TSource value = default(TSource); if (value == null) { using (IEnumerator<TSource> e = source.GetEnumerator()) { do { if (!e.MoveNext()) return value; value = e.Current; } while (value == null); while (e.MoveNext()) { TSource x = e.Current; if (x != null && comparer.Compare(x, value) < 0) value = x; } } } else { using (IEnumerator<TSource> e = source.GetEnumerator()) { if (!e.MoveNext()) throw Error.NoElements(); value = e.Current; while (e.MoveNext()) { TSource x = e.Current; if (comparer.Compare(x, value) < 0) value = x; } } } return value; }
这不会在TSource
types和不同types的行为之间进行大量的比较(通常是一个你不应该使用generics的标志),而是在可以为null
types的代码path之间进行分割如果找不到元素,则返回null
,如果其中一个比较元素为null
,则不能进行比较以查找最小值)以及不能为null
types的代码path(如果找不到元素,则应该抛出,不必担心关于null
元素的可能性)。
因为TSource
在方法中是“真实的”,所以这个比较可以在运行时或者是在时间上进行(一般是在时间上的,当然上面的情况在时间点上是这样做的,而不是为未采用的path产生机器码)一个单独的“真实”版本的方法为每个案件。 (虽然作为一种优化,机器代码为不同参考types参数的不同方法共享,因为它可以不受影响,因此我们可以减less机器代码的数量)。
(在C#中讨论泛化types的泛化是很常见的,除非你也处理Java,因为在C#中,我们只是把这个泛化看作是理所当然的;所有types都是被泛化的,在Java中,非generics被认为是泛化的,是它们和generics的区别)。
正如duffymo已经指出 ,“物化”不是关键的区别。
在Java中,generics基本上可以提高编译时支持 – 它允许您在代码中使用强types的集合,并为您处理types安全。 但是,这只存在于编译时 – 编译的字节码不再有任何generics的概念; 所有genericstypes都被转换成“具体”types(如果genericstypes是无界的,则使用object
),根据需要添加types转换和types检查。
在.NET中,generics是CLR的一个完整function。 在编译genericstypes时,它在生成的IL中保持通用。 它不仅仅是像Java一样被转换成非generics的代码。
这对仿制药在实践中的工作有几个影响。 例如:
- Java具有
SomeType<?>
以允许您传递给定genericstypes的任何具体实现。 C#不能做到这一点 – 每一个具体的(指定的)genericstypes都是它自己的types。 - Java中无限generics意味着它们的值被存储为一个
object
。 在这种generics中使用值types时,这可能会对性能产生影响。 在C#中,当在genericstypes中使用值types时,它保持值types。
举一个例子,假设你有一个带有一个generics参数的List
genericstypes。 在Java中, List<String>
和List<Int>
最终会在运行时是完全相同的types – genericstypes只对于编译时代码才存在。 所有对GetValue
调用都将分别转换为(String)GetValue
和(Int)GetValue
。
在C#中, List<string>
和List<int>
是两种不同的types。 它们是不可互换的,它们的types安全性也是在运行时执行的。 无论你做什么, new List<int>().Add("SomeString")
将永远不会工作 – List<int>
的底层存储实际上是一些整型数组,而在Java中,它必然是一个object
数组。 在C#中,没有涉及到演员,没有拳击等。
这也应该明白为什么C#不能像Java那样用SomeType<?>
做同样的事情。 在Java中,所有从“ SomeType<?>
派生的genericstypes最终都是完全相同的types。 在C#中,所有各种特定的SomeType<T>
是它们自己独立的types。 除去编译时检查,可以传递SomeType<Int>
而不是SomeType<String>
(实际上, SomeType<?>
意思是“忽略给定generics的编译时检查”)。 在C#中,这是不可能的,甚至不是派生types(也就是说,即使string
是从object
派生的),您也无法执行List<object> list = (List<object>)new List<string>();
)。
两种实施都有其优点和缺点。 曾经有几次,我曾经喜欢在C#中允许SomeType<?>
作为参数,但是C#generics的工作方式根本没有意义。
物化是一个面向对象的build模概念。
Reify是一个动词,意思是“使抽象真实” 。
当你进行面向对象编程时,将真实世界对象build模为软件组件(例如Window,Button,Person,Bank,Vehicle等)
将抽象概念也分解成组件也是很常见的(例如WindowListener,Broker等)