什么是物化?

我知道 Java 通过擦除实现了参数多态(Generics) ,我知道什么是擦除。

我知道 C # 实现了具体化的参数多态,我知道这会让你写作

public void dosomething(List<String> input) {}
public void dosomething(List<Int> input) {}

或者你可以在运行时知道一些参数化类型的类型参数是什么,但是我不明白它是什么

  • 什么是物化类型?
  • 什么是具体化的价值?
  • 当类型/值被具体化时会发生什么?
16274 次浏览

具体化是一个面向对象的建模概念。

Reify 是一个动词,意思是 “把抽象的东西变成现实”

当你进行面向对象的编程时,将现实世界中的对象建模为软件组件(例如窗口、按钮、人、银行、车辆等)是很常见的

将抽象概念具体化为组件也是很常见的(例如,WindowListener、 Broker 等)

物化就是把抽象的东西变成具体的东西的过程。

在 C # 泛型中,术语 物化指的是将一个 泛型类型定义和一个或多个 泛型类型参数(抽象的东西)组合起来创建一个新的 通用型(具体的东西)的过程。

换句话说,它是采用 List<T>int的定义并生成具体的 List<int>类型的过程。

为了进一步了解它,请比较以下方法:

  • 在 Java 泛型中,泛型类型定义基本上被转换为一个具体的泛型类型,在所有允许的类型参数组合中共享。因此,多个(源代码级别)类型映射到一个(二进制级别)类型-但结果是 有关实例的类型参数的信息在该实例中被丢弃(类型擦除)

    1. 作为这种实现技术的副作用,本地允许的唯一泛型类型参数是那些可以共享其具体类型的二进制代码的类型; 这意味着那些存储位置具有可互换表示的类型; 这意味着引用类型。将值类型用作泛型类型参数需要装箱它们(将它们放在一个简单的引用类型包装器中)。
    2. 为了以这种方式实现泛型,不会复制任何代码。
    3. 可能在运行时(使用反射)可用的类型信息将丢失。反过来,这意味着泛型类型的专门化(对任何特定的泛型参数组合使用专门化 源代码的能力)受到很大限制。
    4. 这种机制不需要执行期函式库的支持。
    5. Java 程序或基于 JVM 的语言可以使用一些 保留类型信息的工作区
  • 在 C # 泛型中,泛型类型定义在运行时维护在内存中。每当需要一个新的具体类型时,执行期函式库就会结合泛型类型定义和类型参数,创建新的类型(具体化)。因此,我们得到了类型参数的每个组合的新类型 在运行时

    1. 这种实现技术允许实例化任何类型的类型参数组合。将值类型用作泛型类型参数不会导致装箱,因为这些类型有自己的实现。(当然是 在 C # 中仍然存在拳击,但它发生在其他情况下,而不是本次。)
    2. 代码复制可能是一个问题——但实际上并非如此,因为足够智能的实现(这包括微软.NET单核细胞增多症)可以为某些实例共享代码。
    3. 通过使用反射检查类型参数,可以维护类型信息,从而在一定程度上允许专门化。然而,专门化的程度是有限的,由于编译了一个泛型类型定义 之前,所以任何具体化都会发生(这是由 根据类型参数的约束编译定义完成的,因此是 即使没有特定的类型参数,编译器也必须能够“理解”定义)。
    4. 这种实现技术在很大程度上依赖于运行时支持和 JIT 编译(这就是为什么您经常听到动态代码生成受到限制的 C # 泛型在像 iOS 这样的平台上有一些限制)。
    5. 在 C # 泛型的上下文中,具体化是由执行期函式库来完成的。但是,如果您想更直观地理解泛型类型定义和具体泛型类型 您总是可以使用 System.Type类自己执行具体化之间的区别(即使您正在实例化的特定泛型类型参数组合没有直接出现在源代码中)。
  • 在 C + + 模板中,模板定义在编译时保存在内存中。每当源代码中需要一个模板类型的新实例化时,编译器就会组合模板定义和模板参数并创建新类型。因此,对于每个模板参数组合,我们得到一个唯一的类型 在编译时

    1. 这种实现技术允许实例化任何类型的类型参数组合。
    2. 众所周知,这会复制二进制代码,但是一个足够智能的工具链仍然可以检测到这一点,并为某些实例共享代码。
    3. 模板定义本身不是“编译的”-只有它的具体实例实际上被编译。这将减少对编译器的约束,并允许更高的 模板专门化度。
    4. 因为模板实例化是在编译时执行的,所以这里也不需要运行时支持。
    5. 这个过程最近被称为 单态,特别是在 Rust 社区中。这个词与 参数多态形成了对比,参数多态是泛型概念的名称。

作为 Duffymo 已经注意到了,“物化”并不是关键区别。

在 Java 中,泛型基本上是用来改进编译时支持的——它允许你在代码中使用强类型,例如集合,并为你处理类型安全问题。然而,这只存在于编译时——编译后的字节码不再有泛型的概念; 所有的泛型类型都被转换为“具体”类型(如果泛型类型是无界的,则使用 object) ,根据需要添加类型转换和类型检查。

进去。NET,泛型是 CLR 的一个不可或缺的特性。编译泛型类型时,它在生成的 IL 中保持泛型。它不像 Java 那样只是转换为非泛型代码。

这对泛型在实践中的工作方式有几个影响,例如:

  • Java 有 SomeType<?>,允许您传递给定泛型类型的任何具体实现。C # 不能这样做-每个特定的(具体化了)泛型类型都是它自己的类型。
  • Java 中无界泛型类型意味着它们的值存储为 object。在这样的泛型中使用值类型时,这可能会对性能产生影响。在 C # 中,当您在泛型类型中使用值类型时,它仍然是值类型。

为了给出一个示例,假设您有一个带有一个泛型参数的 List泛型类型。在 Java 中,List<String>List<Int>在运行时最终将是完全相同的类型——泛型类型只存在于编译时代码中。所有对例如 GetValue的调用将分别转换为 (String)GetValue(Int)GetValue

在 C # 中,List<string>List<int>是两种不同的类型。它们是不可互换的,并且它们的类型安全性在运行时也是强制执行的。无论你做什么,new List<int>().Add("SomeString")永远不会的工作-在 List<int>的底层存储是 真的一些整数数组,而在 Java 中,它必须是一个 object数组。在 C # 中,没有涉及到强制类型转换,没有装箱等等。

这也说明了为什么 C # 不能像 Java 那样使用 SomeType<?>。在 Java 中,所有“派生自”SomeType<?>的泛型类型最终都是完全相同的类型。在 C # 中,所有不同的特定 SomeType<T>都是它们自己独立的类型。删除编译时检查,就可以通过 SomeType<Int>而不是 SomeType<String>(实际上,SomeType<?>的意思是“忽略给定泛型类型的编译时检查”)。在 C # 中,这是不可能的,即使对于派生类型也是如此(也就是说,即使 string是从 object派生的,也不能执行 List<object> list = (List<object>)new List<string>();)。

两种实现各有利弊。有好几次,我希望能够在 C # 中只允许使用 SomeType<?>作为参数——但是 C # 泛型的工作方式完全没有意义。

具体化通常意味着(在计算机科学之外)“使某事物成为现实”。

在编程中,如果我们能够通过语言本身访问关于 具体化了的信息,那么它就是 具体化了

对于两个完全与泛型无关的 C # 具体化了和没有具体化的例子,让我们看看方法和内存访问。

OO 语言通常有 方法,(许多语言没有类似的 功能,尽管没有绑定到类)。因此,您可以用这种语言定义一个方法,调用它,或者重写它,等等。并非所有这样的语言都允许您将方法本身作为程序的数据进行实际处理。C # (真的,。NET 而不是 C #)可以让你使用表示方法的 MethodInfo对象,所以在 C # 中方法是具体化的。C # 中的方法是“第一类对象”。

所有的实用语言都有一些访问计算机内存的方法。在像 C 这样的低级语言中,我们可以直接处理计算机所使用的数字地址之间的映射,所以像 int* ptr = (int*) 0xA000000; *ptr = 42;这样的语言是合理的(只要我们有充分的理由怀疑以这种方式访问内存地址 0xA000000不会把事情搞砸)。在 C # 中这是不合理的(我们可以强制它。NET,但使用。NET 内存管理移动东西不太可能是有用的)。C # 没有具体的内存地址。

因此,作为 重申的意思是“使真实的”一个“物化的类型”是一个类型,我们可以“谈论”在问题的语言。

在泛型中,这意味着两件事。

一个是 List<string>是一种类型,就像 stringint是。我们可以比较这种类型,得到它的名称,并询问有关它:

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

其结果是,我们可以在方法本身内“讨论”泛型方法(或泛型类的方法)的参数类型:

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类型和不同行为的不同类型进行大量的比较(通常这表明你根本不应该使用泛型) ,但是对于可以是 null类型的代码路径(如果没有找到元素,应该返回 null,如果被比较的元素之一是 null,则不能进行比较以找到最小值)和不能是 null类型的代码路径(如果没有找到元素,应该抛出,并且不必担心 null元素的可能性)进行比较。

因为 TSource在方法中是“真实的”,所以这种比较可以在运行时进行,也可以在抖动时进行(通常在抖动时进行,当然上面的例子会在抖动时进行,而不会为没有采用的路径生成机器代码) ,对于每种情况,我们都有一个单独的“真实的”方法版本。(尽管作为一种优化,机器代码可以为不同的引用类型参数的不同方法共享,因为它可以不影响这一点,因此我们可以减少机器代码抖动的数量。)

(在 C # 中谈论泛型类型的具体化并不常见,除非你也处理 Java,因为在 C # 中,我们只是把这种具体化视为理所当然; 所有类型都是具体化的。在 Java 中,非泛型类型被称为 具体化了,因为这是它们与泛型类型之间的区别。)