首先,让我们澄清两件事。这是一个泛型方法定义:
T M<T>(T x)
{
return x;
}
这是一个泛型类型定义:
class C<T>
{
}
如果我问你M 是什么,你很可能会说它是一个泛型方法,它接受T 并返回T。这是绝对正确的,但我提出了一种不同的思考方式——这里有两组参数。一个是类型T,另一个是对象x。如果我们把它们结合起来,我们知道这个方法总共需要两个参数。
The concept of currying 告诉我们,一个接受两个参数的函数可以转换为一个接受一个参数并返回另一个接受另一个参数的函数(反之亦然)。例如,这里有一个函数,它接受两个整数并产生它们的和:
Func<int, int, int> uncurry = (x, y) => x + y;
int sum = uncurry(1, 3);
这是一个等价的形式,我们有一个函数,它接受一个整数并生成一个函数,该函数接受另一个整数并返回上述整数的总和:
Func<int, Func<int, int>> curry = x => y => x + y;
int sum = curry(1)(3);
我们从一个接受两个整数的函数变成了一个接受一个整数的函数并创建函数。显然,这两者在 C# 中并不是完全一样的东西,但它们是同一件事的两种不同表达方式,因为传递相同的信息最终会得到相同的最终结果。
柯里化让我们更容易推理函数(推理一个参数比推理两个参数更容易),它让我们知道我们的结论仍然适用于任意数量的参数。
请考虑一下,在抽象层面上,这就是这里发生的事情。假设M 是一个“超级函数”,它采用T 类型并返回一个常规方法。返回的方法接受 T 值并返回 T 值。
例如,如果我们使用参数int 调用超函数M,我们会得到一个从int 到int 的常规方法:
Func<int, int> e = M<int>;
如果我们使用参数5 调用该常规方法,我们会得到一个5,正如我们预期的那样:
int v = e(5);
因此,请考虑以下表达式:
int v = M<int>(5);
您现在明白为什么这可以被视为两个单独的调用了吗?您可以识别对超级函数的调用,因为它的参数是在<> 中传递的。然后调用返回的方法,参数在() 中传递。类似于前面的例子:
curry(1)(3);
同样,泛型类型定义也是一个超函数,它接受一个类型并返回另一个类型。例如,List<int> 是对超函数 List 的调用,其参数 int 返回一个整数列表类型。
现在当 C# 编译器遇到常规方法时,它会将其编译为常规方法。它不会尝试为不同的可能参数创建不同的定义。所以,这个:
int Square(int x) => x * x;
按原样编译。它不会被编译为:
int Square__0() => 0;
int Square__1() => 1;
int Square__2() => 4;
// and so on
换句话说,C# 编译器不会评估此方法的所有可能参数,以便将它们嵌入到最终的可执行文件中——相反,它将方法保留为其参数化形式并相信结果将在运行时进行评估.
同样,当 C# 编译器遇到超函数(泛型方法或类型定义)时,它会将其编译为超函数。它不会尝试为不同的可能参数创建不同的定义。所以,这个:
T M<T>(T x) => x;
按原样编译。它不会被编译为:
int M(int x) => x;
int[] M(int[] x) => x;
int[][] M(int[][] x) => x;
// and so on
float M(float x) => x;
float[] M(float[] x) => x;
float[][] M(float[][] x) => x;
// and so on
同样,C# 编译器相信,当这个超级函数被调用时,它会在运行时被求值,而常规的方法或类型将由该求值产生。
这是 C# 受益于将 JIT 编译器作为其运行时的一部分的原因之一。当一个超级函数被求值时,它会产生一个全新的方法或编译时不存在的类型!我们称这个过程为reification。随后,运行时会记住该结果,因此不必再次重新创建它。那部分叫做memoization。
与不需要 JIT 编译器作为其运行时一部分的 C++ 相比。 C++ 编译器实际上需要在编译时评估超级函数(称为“模板”)。这是一个可行的选择,因为超级函数的参数仅限于在编译时可以评估的东西。
所以,回答你的问题:
class Foo
{
public void Bar()
{
}
}
Foo 是一个常规类型,并且只有一个。 Bar 是Foo 中的常规方法,只有一个。
class Foo<T>
{
public void Bar()
{
}
}
Foo<T> 是一个在运行时创建类型的超级函数。这些结果类型中的每一个都有自己的常规方法,名为Bar,并且只有一个(对于每种类型)。
class Foo
{
public void Bar<T>()
{
}
}
Foo 是一个常规类型,并且只有一个。 Bar<T> 是一个在运行时创建常规方法的超级函数。然后,这些结果方法中的每一个都将被视为常规类型 Foo 的一部分。
class Foo<Τ1>
{
public void Bar<T2>()
{
}
}
Foo<T1> 是一个在运行时创建类型的超级函数。这些结果类型中的每一个都有自己的一个名为Bar<T2> 的超级函数,它在运行时(稍后)创建常规方法。这些结果方法中的每一个都被认为是创建相应超函数的类型的一部分。
以上是概念性的解释。除此之外,可以实现某些优化以减少内存中不同实现的数量——例如在某些情况下,两个构造方法可以共享一个机器代码实现。请参阅Luaan's answer,了解 CLR 为何可以执行此操作以及何时执行此操作。