173

我知道Java通过擦除实现了参数多态性(泛型)。我明白什么是擦除。

我知道 C# 通过具体化实现了参数多态性。我知道这可以让你写作

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

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

  • 什么是物化类型?
  • 什么是物化价值?
  • 当类型/值被具体化时会发生什么?
4

4 回答 4

225

物化是一个抽象的东西,创造一个具体的东西的过程。

C# 泛型中的术语具体化是指将泛型类型定义和一个或多个泛型类型参数(抽象事物)组合以创建新泛型类型(具体事物)的过程。

换一种说法,它是定义List<T>int产生具体List<int>类型的过程。

要进一步理解它,请比较以下方法:

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

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

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

    1. 这种实现技术允许实例化任何类型的类型参数组合。
    2. 众所周知,这会复制二进制代码,但足够智能的工具链仍然可以检测到这一点,并为某些实例共享代码。
    3. 模板定义本身不是“编译的”——只有它的具体实例被实际编译。这对编译器的约束更少,并允许更大程度的模板专业化
    4. 由于模板实例化是在编译时执行的,因此这里也不需要运行时支持。
    5. 这个过程最近被称为单态化,尤其是在 Rust 社区中。该词用于与参数多态性形成对比,参数多态性是泛型来自的概念的名称。
于 2015-08-07T11:34:11.600 回答
27

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

在编程中,如果我们能够在语言本身中访问有关它的信息,那么某些东西就会被具体化。

对于 C# 所做和未具体化的两个完全与泛型无关的示例,让我们来看看方法和内存访问。

OO 语言通常具有方法,(并且许多没有类似但未绑定到类的函数)。因此,您可以用这种语言定义一个方法,调用它,或者覆盖它,等等。并非所有此类语言都允许您将方法本身作为程序的数据实际处理。C#(实际上是 .NET 而不是 C#)确实允许您使用MethodInfo表示方法的对象,因此在 C# 中方法被具体化。C# 中的方法是“第一类对象”。

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

因此,由于refied意味着“变得真实”,“具体类型”是我们可以用相关语言“谈论”的类型。

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

一个是那List<string>是一种string类型int。我们可以比较那个类型,得到它的名字,然后查询它:

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对不同行为的类型和各种类型进行大量比较(通常表明您根本不应该使用泛型),但它确实在可能的类型的代码路径之间拆分nullnull如果未找到元素,并且如果比较的元素之一是 ,则不得进行比较以找到最小值null)和无法找到的类型的代码路径null(如果未找到元素,则应抛出,并且不必担心null元素的可能性)。

因为TSource在方法中是“真实的”,所以这种比较可以在运行时或 jitting 时间进行(通常是 jitting 时间,当然上述情况会在 jitting 时间这样做,并且不会为未采用的路径生成机器代码),我们有一个每种情况下单独的“真实”版本的方法。(虽然作为一种优化,机器码是为不同的引用类型参数的不同方法共享的,因为它可以不影响这一点,因此我们可以减少机器码的数量)。

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

于 2015-08-07T11:52:39.217 回答
17

正如duffymo 已经指出的那样,“具体化”并不是关键区别。

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

在 .NET 中,泛型是 CLR 的一个组成部分。编译泛型类型时,它在生成的 IL 中保持泛型。它不只是像在 Java 中那样转换为非通用代码。

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

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

举个例子,假设您有一个List带有一个泛型参数的泛型类型。在 Java 中,List<String>最终List<Int>将在运行时成为完全相同的类型 - 泛型类型仅在编译时代码中真正存在。所有对 eg 的调用都GetValue将分别转换为(String)GetValue(Int)GetValue

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

这也应该清楚为什么 C# 不能用SomeType<?>. 在 Java 中,所有“派生自”的泛型类型SomeType<?>最终都是完全相同的类型。在 C# 中,所有各种特定SomeType<T>的 s 都是它们自己独立的类型。删除编译时检查,可以通过SomeType<Int>而不是SomeType<String>(实际上,这SomeType<?>意味着“忽略给定泛型类型的编译时检查”)。在 C# 中,这是不可能的,甚至对于派生类型也不行(也就是说,List<object> list = (List<object>)new List<string>();即使string派生自也不能这样做object)。

两种实现都有其优点和缺点。有几次我很想SomeType<?>在 C# 中只允许作为参数 - 但 C# 泛型的工作方式根本没有意义。

于 2015-08-07T11:34:26.437 回答
3

Reification 是一个面向对象的建模概念。

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

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

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

于 2015-08-07T11:20:51.167 回答