什么是具体化?
What is reification?
我知道 Java 通过擦除实现参数多态性(泛型)。我明白什么是擦除。
我知道 C# 通过具体化实现了参数多态性。我知道可以让你写
public void dosomething(List<String> input) {}
public void dosomething(List<Int> input) {}
或者你可以在运行时知道某些参数化类型的类型参数是什么,但我不明白它是什么是。
- 什么是具体化类型?
- 什么是物化值?
- 当 type/value 被具体化时会发生什么?
具体化是一种面向对象的建模概念。
Reify 是一个动词,意思是 "make something abstract real"。
当您进行面向对象编程时,通常将现实世界对象建模为软件组件(例如 Window、按钮、人员、银行、车辆等)
将抽象概念具体化为组件也很常见(例如 WindowListener、Broker 等)
具体化是把抽象的东西变成具体的东西的过程。
C#泛型中的术语具体化指的是generic type definition和一个或多个泛型类型参数的过程(抽象的东西)组合起来创建一个新的通用类型(具体的东西)。
换句话说,就是从List<T>
和int
的定义中产生一个具体的List<int>
类型的过程。
要进一步了解它,请比较以下方法:
在 Java 泛型中,泛型类型定义本质上被转换为一个具体的泛型类型,在所有允许的类型参数组合中共享。因此,多个(源代码级别)类型被映射到一个(二进制级别)类型 - 但结果是,information about the type arguments of an instance is discarded in that instance (type erasure).
- 作为这种实现技术的副作用,唯一允许的泛型类型参数是那些可以共享其具体类型的二进制代码的类型;这意味着那些存储位置具有可互换表示的类型;这意味着引用类型。 Using value types as generic type arguments requires boxing them(将它们放在一个简单的引用类型包装器中)。
- 没有重复代码以这种方式实现泛型。
- 本可以在运行时(使用反射)可用的类型信息丢失了。反过来,这意味着泛型类型的专门化(使用专门的 源代码 用于任何特定泛型参数组合的能力)非常受限。
- 该机制不需要运行时环境的支持。
- Java 程序或基于 JVM 的语言可以使用一些 workarounds to retain type information。
在 C# 泛型中,泛型类型定义在运行时在内存中维护。每当需要新的具体类型时,运行时环境都会结合通用类型定义和类型参数并创建新类型(具体化)。所以我们为类型参数的每个组合得到一个新类型,at runtime。
- 此实现技术允许实例化任何类型的参数组合。使用值类型作为泛型类型参数不会导致装箱,因为这些类型有自己的实现。 (Boxing still exists in C#,当然 - 但它发生在其他情况下,不是这个。)
- 代码重复可能是个问题 - 但实际上不是,因为足够智能的实现 (this includes Microsoft .NET and Mono) 可以为某些实例共享代码。
- 类型信息得到维护,通过使用反射检查类型参数,允许在一定程度上进行专业化。然而,专业化程度是有限的,因为泛型类型定义在之前任何具体化发生之前被编译(这是由compiling the definition against the constraints on the type parameters - thus, the compiler has to be able "understand" the definition even in the absence of specific type arguments完成的)。
- 此实现技术在很大程度上依赖于运行时支持和 JIT 编译(这就是为什么您经常听到 C# generics have some limitations on platforms like iOS,其中动态代码生成受到限制)。
- 在 C# 泛型的上下文中,具体化是由运行时环境为您完成的。但是,如果你想更直观地理解泛型类型定义和具体泛型类型之间的区别,you can always perform a reification on your own, using the
System.Type
class(即使你正在实例化的特定泛型类型参数组合没有直接出现在你的源代码中) .
在 C++ 模板中,模板定义在编译时保存在内存中。每当源代码中需要模板类型的新实例时,编译器就会组合模板定义和模板参数并创建新类型。所以我们为模板参数的每个组合得到一个唯一的类型,在编译时。
- 此实现技术允许实例化任何类型的参数组合。
- 已知这会复制二进制代码,但足够智能的工具链仍然可以检测到这一点并为某些实例共享代码。
- 模板定义本身不是 "compiled" - only its concrete instantiations are actually compiled. This places fewer constraints on the compiler and allows a greater degree of template specialization.
- 由于模板实例化是在编译时执行的,因此这里也不需要运行时支持。
- 这个过程最近被称为 monomorphization, especially in the Rust community. The word is used in contrast to parametric polymorphism,这是泛型的概念名称。
与 一样,"reification" 不是关键区别。
在Java中,泛型基本上是用来改进编译时支持的——它允许你使用强类型,例如您的代码中的集合,并为您处理类型安全。然而,这只存在于编译时——编译后的字节码不再有任何泛型的概念;所有泛型类型都转换为 "concrete" 类型(如果泛型类型是无界的,则使用 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>
中的底层存储是 really 一些整数数组,而在 Java 中,它必然是一个 object
数组。在 C# 中,没有涉及转换,没有装箱等。
这也应该清楚为什么 C# 不能用 SomeType<?>
做与 Java 相同的事情。在 Java 中,所有泛型类型 "derived from" SomeType<?>
最终都是完全相同的类型。在 C# 中,所有各种特定的 SomeType<T>
都是它们自己的独立类型。删除编译时检查,可以传递 SomeType<Int>
而不是 SomeType<String>
(实际上,SomeType<?>
意味着 "ignore compile-time checks for the given generic type")。在 C# 中,这是不可能的,即使对于派生类型也是如此(也就是说,即使 string
派生自 object
,您也不能执行 List<object> list = (List<object>)new List<string>();
)。
两种实施方式各有利弊。有几次我很想能够只允许 SomeType<?>
作为 C# 中的参数 - 但它根本没有意义 C# 泛型的工作方式。
Reification 一般表示(计算机科学之外)"to make something real".
在编程中,如果我们能够在语言本身中访问有关它的信息,那么它就是 具体化。
对于 C# 实现和未实现的两个完全非泛型相关的示例,让我们来看看方法和内存访问。
OO 语言通常有 方法 ,(还有许多没有 函数 的方法虽然没有绑定到 class).因此,您可以用这种语言定义一个方法,调用它,也许重写它,等等。并非所有此类语言都允许您实际将方法本身作为程序的数据来处理。 C#(实际上是 .NET 而不是 C#)确实允许您使用 MethodInfo
表示方法的对象,因此在 C# 中方法被具体化了。 C# 中的方法是 "first class objects".
所有实用语言都有一些访问计算机内存的方法。在像 C 这样的低级语言中我们可以直接处理计算机使用的数字地址之间的映射,所以 int* ptr = (int*) 0xA000000; *ptr = 42;
是合理的(只要我们有充分的理由怀疑访问内存地址0xA000000
这样不会炸毁东西)。在 C# 中,这是不合理的(我们几乎可以在 .NET 中强制执行它,但是随着 .NET 内存管理的移动,它不太可能有用)。 C# 没有具体化的内存地址。
因此,由于 refied 表示 "made real" "reified type" 是一种类型,我们可以 "talk about" 使用相关语言。
在泛型中,这意味着两件事。
一个是 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
这样做的结果是我们可以 "talk about" 方法本身内的泛型方法(或泛型方法 class)参数类型:
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
}
作为一项规则,这样做太多是 "smelly",但它有很多有用的案例。例如,查看:
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
的类型的路径(如果未找到元素,则应该 return null
,如果比较的元素之一是 null
,则不得进行比较以找到最小值)以及不能为 null
的类型的代码路径(如果没有找到元素应该抛出,并且不必担心 null
元素的可能性)。
因为TSource
在方法中是"real",这个比较既可以在运行时进行,也可以在jitting时进行(一般是jitting时,当然上述情况会在jitting时进行,不会产生未采用路径的机器代码),并且我们针对每种情况都有一个单独的 "real" 版本的方法。 (虽然作为一种优化,机器代码为不同的引用类型参数的不同方法共享,因为它可以在不影响这一点的情况下,因此我们可以减少机器代码的数量)。
(在 C# 中谈论泛型类型的具体化并不常见,除非您还处理 Java,因为在 C# 中我们只是认为这种具体化是理所当然的;所有类型都是具体化的。在 Java,非泛型类型被称为 reified,因为这是它们与泛型类型之间的区别)。
我知道 Java 通过擦除实现参数多态性(泛型)。我明白什么是擦除。
我知道 C# 通过具体化实现了参数多态性。我知道可以让你写
public void dosomething(List<String> input) {}
public void dosomething(List<Int> input) {}
或者你可以在运行时知道某些参数化类型的类型参数是什么,但我不明白它是什么是。
- 什么是具体化类型?
- 什么是物化值?
- 当 type/value 被具体化时会发生什么?
具体化是一种面向对象的建模概念。
Reify 是一个动词,意思是 "make something abstract real"。
当您进行面向对象编程时,通常将现实世界对象建模为软件组件(例如 Window、按钮、人员、银行、车辆等)
将抽象概念具体化为组件也很常见(例如 WindowListener、Broker 等)
具体化是把抽象的东西变成具体的东西的过程。
C#泛型中的术语具体化指的是generic type definition和一个或多个泛型类型参数的过程(抽象的东西)组合起来创建一个新的通用类型(具体的东西)。
换句话说,就是从List<T>
和int
的定义中产生一个具体的List<int>
类型的过程。
要进一步了解它,请比较以下方法:
在 Java 泛型中,泛型类型定义本质上被转换为一个具体的泛型类型,在所有允许的类型参数组合中共享。因此,多个(源代码级别)类型被映射到一个(二进制级别)类型 - 但结果是,information about the type arguments of an instance is discarded in that instance (type erasure).
- 作为这种实现技术的副作用,唯一允许的泛型类型参数是那些可以共享其具体类型的二进制代码的类型;这意味着那些存储位置具有可互换表示的类型;这意味着引用类型。 Using value types as generic type arguments requires boxing them(将它们放在一个简单的引用类型包装器中)。
- 没有重复代码以这种方式实现泛型。
- 本可以在运行时(使用反射)可用的类型信息丢失了。反过来,这意味着泛型类型的专门化(使用专门的 源代码 用于任何特定泛型参数组合的能力)非常受限。
- 该机制不需要运行时环境的支持。
- Java 程序或基于 JVM 的语言可以使用一些 workarounds to retain type information。
在 C# 泛型中,泛型类型定义在运行时在内存中维护。每当需要新的具体类型时,运行时环境都会结合通用类型定义和类型参数并创建新类型(具体化)。所以我们为类型参数的每个组合得到一个新类型,at runtime。
- 此实现技术允许实例化任何类型的参数组合。使用值类型作为泛型类型参数不会导致装箱,因为这些类型有自己的实现。 (Boxing still exists in C#,当然 - 但它发生在其他情况下,不是这个。)
- 代码重复可能是个问题 - 但实际上不是,因为足够智能的实现 (this includes Microsoft .NET and Mono) 可以为某些实例共享代码。
- 类型信息得到维护,通过使用反射检查类型参数,允许在一定程度上进行专业化。然而,专业化程度是有限的,因为泛型类型定义在之前任何具体化发生之前被编译(这是由compiling the definition against the constraints on the type parameters - thus, the compiler has to be able "understand" the definition even in the absence of specific type arguments完成的)。
- 此实现技术在很大程度上依赖于运行时支持和 JIT 编译(这就是为什么您经常听到 C# generics have some limitations on platforms like iOS,其中动态代码生成受到限制)。
- 在 C# 泛型的上下文中,具体化是由运行时环境为您完成的。但是,如果你想更直观地理解泛型类型定义和具体泛型类型之间的区别,you can always perform a reification on your own, using the
System.Type
class(即使你正在实例化的特定泛型类型参数组合没有直接出现在你的源代码中) .
在 C++ 模板中,模板定义在编译时保存在内存中。每当源代码中需要模板类型的新实例时,编译器就会组合模板定义和模板参数并创建新类型。所以我们为模板参数的每个组合得到一个唯一的类型,在编译时。
- 此实现技术允许实例化任何类型的参数组合。
- 已知这会复制二进制代码,但足够智能的工具链仍然可以检测到这一点并为某些实例共享代码。
- 模板定义本身不是 "compiled" - only its concrete instantiations are actually compiled. This places fewer constraints on the compiler and allows a greater degree of template specialization.
- 由于模板实例化是在编译时执行的,因此这里也不需要运行时支持。
- 这个过程最近被称为 monomorphization, especially in the Rust community. The word is used in contrast to parametric polymorphism,这是泛型的概念名称。
与
在Java中,泛型基本上是用来改进编译时支持的——它允许你使用强类型,例如您的代码中的集合,并为您处理类型安全。然而,这只存在于编译时——编译后的字节码不再有任何泛型的概念;所有泛型类型都转换为 "concrete" 类型(如果泛型类型是无界的,则使用 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>
中的底层存储是 really 一些整数数组,而在 Java 中,它必然是一个 object
数组。在 C# 中,没有涉及转换,没有装箱等。
这也应该清楚为什么 C# 不能用 SomeType<?>
做与 Java 相同的事情。在 Java 中,所有泛型类型 "derived from" SomeType<?>
最终都是完全相同的类型。在 C# 中,所有各种特定的 SomeType<T>
都是它们自己的独立类型。删除编译时检查,可以传递 SomeType<Int>
而不是 SomeType<String>
(实际上,SomeType<?>
意味着 "ignore compile-time checks for the given generic type")。在 C# 中,这是不可能的,即使对于派生类型也是如此(也就是说,即使 string
派生自 object
,您也不能执行 List<object> list = (List<object>)new List<string>();
)。
两种实施方式各有利弊。有几次我很想能够只允许 SomeType<?>
作为 C# 中的参数 - 但它根本没有意义 C# 泛型的工作方式。
Reification 一般表示(计算机科学之外)"to make something real".
在编程中,如果我们能够在语言本身中访问有关它的信息,那么它就是 具体化。
对于 C# 实现和未实现的两个完全非泛型相关的示例,让我们来看看方法和内存访问。
OO 语言通常有 方法 ,(还有许多没有 函数 的方法虽然没有绑定到 class).因此,您可以用这种语言定义一个方法,调用它,也许重写它,等等。并非所有此类语言都允许您实际将方法本身作为程序的数据来处理。 C#(实际上是 .NET 而不是 C#)确实允许您使用 MethodInfo
表示方法的对象,因此在 C# 中方法被具体化了。 C# 中的方法是 "first class objects".
所有实用语言都有一些访问计算机内存的方法。在像 C 这样的低级语言中我们可以直接处理计算机使用的数字地址之间的映射,所以 int* ptr = (int*) 0xA000000; *ptr = 42;
是合理的(只要我们有充分的理由怀疑访问内存地址0xA000000
这样不会炸毁东西)。在 C# 中,这是不合理的(我们几乎可以在 .NET 中强制执行它,但是随着 .NET 内存管理的移动,它不太可能有用)。 C# 没有具体化的内存地址。
因此,由于 refied 表示 "made real" "reified type" 是一种类型,我们可以 "talk about" 使用相关语言。
在泛型中,这意味着两件事。
一个是 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
这样做的结果是我们可以 "talk about" 方法本身内的泛型方法(或泛型方法 class)参数类型:
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
}
作为一项规则,这样做太多是 "smelly",但它有很多有用的案例。例如,查看:
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
的类型的路径(如果未找到元素,则应该 return null
,如果比较的元素之一是 null
,则不得进行比较以找到最小值)以及不能为 null
的类型的代码路径(如果没有找到元素应该抛出,并且不必担心 null
元素的可能性)。
因为TSource
在方法中是"real",这个比较既可以在运行时进行,也可以在jitting时进行(一般是jitting时,当然上述情况会在jitting时进行,不会产生未采用路径的机器代码),并且我们针对每种情况都有一个单独的 "real" 版本的方法。 (虽然作为一种优化,机器代码为不同的引用类型参数的不同方法共享,因为它可以在不影响这一点的情况下,因此我们可以减少机器代码的数量)。
(在 C# 中谈论泛型类型的具体化并不常见,除非您还处理 Java,因为在 C# 中我们只是认为这种具体化是理所当然的;所有类型都是具体化的。在 Java,非泛型类型被称为 reified,因为这是它们与泛型类型之间的区别)。