什么是具体化?


163

我知道Java会擦除地实现参数多态性(泛型)。我了解什么是擦除。

我知道C#通过实现实现了参数多态。我知道那可以让你写

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

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

  • 什么是改良型?
  • 什么是固定值?
  • 对类型/值进行整形后会发生什么?

这不是一个答案,但可能会有所帮助:beust.com/weblog/2011/07/29/erasure-vs-reification
heringer 2015年

@heringer似乎很好地回答了“什么是擦除”的问题,并且似乎基本上用“不擦除”回答了“什么是物化”-这是我最初在此处寻找答案之前发现的一个常见主题。
马丁

5
...还有就是我想重新ification是一个转换的过程中switch构建回的if/ else,当它以前一直从转换if/ elseswitch...
数字创伤

8
RES李嘉欣是拉丁文的事情,所以物化简直是thingification。就C#的使用而言,我没有什么贡献,但是他们使用它的事实本身使我微笑。
KRyan

Answers:


209

修正是获取抽象事物并创建具体事物的过程。

术语物化在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等平台上受到某些限制的原因,而iOS限制了动态代码的生成)。
    5. 在C#泛型的上下文中,运行时环境为您完成了验证。但是,如果您想更直观地了解泛型类型定义与具体泛型类型之间的区别,则始终可以使用System.Type类自行进行验证(即使您要实例化的特定泛型类型参数组合也没有)。不会直接出现在您的源代码中)。
  • 在C ++模板中,模板定义在编译时保留在内存中。每当源代码中需要模板类型的新实例化时,编译器就会将模板定义和模板参数组合在一起并创建新类型。因此,在编译时,模板参数的每个组合都会获得唯一的类型。

    1. 此实现技术允许实例化任何类型的类型参数组合。
    2. 众所周知,它可以复制二进制代码,但是足够聪明的工具链仍可以检测到该代码并共享某些实例化的代码。
    3. 模板定义本身不是“已编译”的- 只是实际编译其具体实例。这对编译器施加了较少的约束,并允许更高程度的模板专门化
    4. 由于模板实例化是在编译时执行的,因此此处也不需要运行时支持。
    5. 这个过程最近被称为单态化,特别是在Rust社区中。该词与参数多态性相反,后者是泛型起源的概念的名称。

7
与C ++模板的绝佳对比...它们似乎介于C#和Java的泛型之间。您可以使用不同的代码和结构来处理不同的特定泛型类型(例如C#),但是它们都像Java中的编译时完成。
罗安2015年

3
另外,在C ++中,这可以引入模板专业化,其中每种(或仅某些)具体类型可以具有不同的实现。显然在Java中是不可能的,但在C#中都不可能。
quetzalcoatl 2015年

@quetzalcoatl的使用原因之一是减少使用指针类型产生的代码量,而C#在后台做了与引用类型可比的事情。尽管如此,那只是使用它的一个原因,并且肯定有一些时候模板专业化会很好。
乔恩·汉纳

对于Java,您可能希望添加以下内容:在删除类型信息时,强制类型转换由编译器添加,从而使字节码与前泛型字节码无法区分。
Rusty Core

27

规范化通常(在计算机科学之外)表示“使某些东西成为现实”。

在编程中,如果我们能够以语言本身访问有关该信息的信息,那么这些东西就可以得到具体化

对于两个完全与非泛型无关的示例,C#确实已经实现了某些内容,而C#还没有实现这些内容,让我们来研究方法和内存访问。

OO语言通常具有方法,(许多语言虽然没有绑定到类,但没有相似的功能)。这样,您就可以用这种语言定义方法,进行调用,也许覆盖它,等等。并非所有此类语言都可以让您实际将方法本身作为程序数据处理。C#(实际上是.NET而不是C#)确实允许您使用MethodInfo表示方法的对象,因此在C#中方法被重新定义。C#中的方法是“一流的对象”。

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

因此,正如被剥夺的意思是“实现”,“被确定的类型”一样,我们可以用所讨论的语言“谈论”这种类型。

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

一个是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不同类型的行为进行多种比较(通常是一个迹象,表明您根本不应该使用泛型),但确实在可以使用的类型的代码路径之间进行了分割null(如果返回,null则返回没有找到元素,并且如果比较的元素之一是,则不能进行比较以找到最小值;如果不能找到null类型的代码路径,则不能进行比较null(如果没有找到元素,则应该抛出该错误,并且不必担心null元素的可能性))。

由于TSource该方法是“真实的”,因此可以在运行时或扩展时间(通常是扩展时间,当然上述情况会在扩展时间这样做,并且不为未采用的路径生成机器代码)进行此比较。针对每种情况分别使用该方法的“实际”版本。(尽管作为优化,机器代码对于不同的引用类型类型参数共享给不同的方法,因为它可以不影响它,因此我们可以减少机器代码的数量)。

(除非您也处理Java,否则在C#中谈论泛型类型的化是不常见的,因为在C#中,我们只是将这种化化视为理所当然;所有类型都是化化的。在Java中,非泛型类型被称为化化是因为,因为是它们和通用类型之间的区别)。


您不认为能够做Min以上有用的事情吗?否则很难实现其记录的行为。
乔恩·汉娜

我认为该错误是(未)记录的行为,并且该行为的含义是有用的(顺便说一句,的行为Enumerable.Min<TSource>有所不同,因为它不会在空集合上抛出非引用类型,但会返回默认值) (TSource),并且仅记录为“返回通用序列中的最小值。”我认为两者都应该抛出空集合,或者应该将“零”元素作为基线传递,并且比较器/比较函数应该始终传入)
Martijn

1
这将比当前的Min有用得多,后者与可为null的类型上常见的db行为匹配,而不会尝试为不可为null的类型上实现不可能的行为。(基线的想法并非不可能,但除非有一个可以让您知道永远不会出现的值,否则它就不会很有用)。
乔恩·汉纳

1
为此,Thingification会是一个更好的名字。:)
tchrist

@tchrist事情可能是不真实的。
乔恩·汉娜

15

正如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中,它必然是一个数组。在C#中,不涉及任何强制类型转换,不包含拳击等。object

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

两种实现都有其优缺点。曾经有几次我希望能够只允许将其SomeType<?>作为C#中的一个参数-但是,这根本对C#泛型的工作方式没有意义。


2
那么,你可以使用类型List<>Dictionary<,>所以在C#中,但和给定的具体列表或字典之间的差距需要相当多反射到桥梁。在某些情况下,接口的差异确实会有所帮助,在某些情况下,我们曾经想轻松地弥合这一差距,但并非全部。
乔恩·汉娜

2
@JonHanna您可以List<>用来实例化新的特定泛型类型-但这仍然意味着创建所需的特定类型。但是List<>,例如,您不能将其用作参数。但是,是的,至少这允许您使用反射弥合差距。
罗安2015年

.NET Framework具有三个硬编码的通用约束,它们不是存储位置类型。所有其他约束必须是存储位置类型。此外,泛型类型T只能满足Uwhen TUare相同的类型,或者U是可以保留对的实例的引用的类型T。有意义地具有类型的存储位置SomeType<?>将是不可能的,但是在理论上将具有该类型的通用约束是可能的。
2015年

1
编译后的Java字节码没有泛型的概念并不是真的。只是类实例没有泛型的概念。这是一个重要的区别;我以前写过这在programmers.stackexchange.com/questions/280169/...,如果你有兴趣。
ruakh 2015年

2

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

Reify是一个动词,意思是“使某些抽象成为现实”

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

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


2
标准化是“使某些东西成为现实”的一般概念,尽管它确实适用于您所说的面向对象的建模,但在泛型实现的上下文中也具有一定的意义。
乔恩·汉纳

2
因此,通过阅读这些答案对我进行了教育。我会修改答案。
duffymo

2
该答案无济于事,无法解决OP对泛型和参数多态性的兴趣。
Erick G. Hagstrom

此评论无助于解决任何人的兴趣或提高您的声望。我看到你什么都没提供。我的是第一个答案,它的确定义了更广泛的定义。
duffymo

1
您的答案可能是第一个,但是您回答了一个不同的问题,而不是OP提出的问题,从问题及其标签的内容中可以清楚地看出。也许您在编写答案之前没有彻底阅读问题,或者也许您不知道“泛化”一词在泛型上下文中具有确定的含义。无论哪种方式,您的答案都没有用。下注。
jcsahnwaldt恢复莫妮卡的时间
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.