.NET中的通用方法不能推断其返回类型。为什么?

时间:2010-07-08 12:42:17

标签: c# .net type-inference

假设:

static TDest Gimme<TSource,TDest>(TSource source) 
{ 
    return default(TDest); 
}

为什么我不能这样做:

string dest = Gimme(5);

没有收到编译器错误:

error CS0411: The type arguments for method 'Whatever.Gimme<TSource,TDest>(TSource)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

5可以推断为int,但是存在一个限制,即编译器不会/不能将返回类型解析为string。我在几个地方读过这是设计,但没有真正的解释。我在某处看到这可能会在C#4中发生变化,但事实并非如此。

任何人都知道为什么无法从泛型方法中推断出返回类型?这是其中一个问题,答案是如此明显,它正盯着你的脸?我希望不会!

7 个答案:

答案 0 :(得分:85)

这里的一般原则是类型信息仅从“单向”流动,从内部流向表达式的外部。你给出的例子非常简单。假设我们希望在对方法R G<A, R>(A a)进行类型推断时“双向”地输入类型信息,并考虑一些创建的疯狂场景:

N(G(5))

假设有十个不同的N重载,每个重载都有不同的参数类型。我们应该为R做十个不同的推论吗?如果我们这样做了,我们应该以某种方式选择“最好的”吗?

double x = b ? G(5) : 123;

G的返回类型应该被推断为什么? Int,因为条件表达式的另一半是int?或者加倍,因为最终这个东西会分配给双倍?现在也许你开始看到这是怎么回事;如果你要说你从外到内推理,你走了多远?在此过程中可能会有许多步骤。看看当我们开始结合这些时会发生什么:

N(b ? G(5) : 123)

现在我们该怎么办?我们有十个N的重载可供选择。我们说R是int吗?它可以是int或int可隐式转换为的任何类型。但是那些类型中哪些类型可以隐式转换为N的参数类型?我们自己编写了一个小程序,并要求prolog引擎解决R可能的所有可能的返回类型,以便满足N上的每个可能的重载,然后以某种方式选择最好的一个?

(我不是在开玩笑;有些语言本质上是编写一些prolog程序,然后使用逻辑引擎来计算出所有类型的东西。例如,F#就是这样做的比C#更复杂的类型推断.Haskell的类型系统实际上是Turing Complete;你可以在类型系统中编码任意复杂的问题并要求编译器解决它们。正如我们稍后将看到的,C#中的重载解析也是如此。 - 你不能像在Haskell中那样编码C#类型系统中的停机问题,但你可以将NP-HARD问题编码为重载解决问题。)

这仍然是一个非常简单的表达方式。假设你有像

这样的东西
N(N(b ? G(5) * G("hello") : 123));

现在我们必须多次为G解决这个问题,也可能为N解决这个问题,我们必须在组合中解决它们 。我们有五个需要解决的重载解决问题,并且所有它们是公平的,应该考虑它们的参数和它们的上下文类型。如果N有十种可能性,那么N(N(...))可能需要考虑一百种可能性,而N(N(N(...))可能有一千种可能性,很快就会让我们解决容易产生数十亿种可能组合的问题并使编译器变得非常慢。

这就是为什么我们有一个规则,即类型信息只以一种方式流动。它可以防止这些类型的鸡和蛋问题,你试图从内部类型确定外部类型,并从外部类型确定内部类型,并导致组合爆炸的可能性。

请注意,类型信息确实为lambdas双向流动!如果你说N(x=>x.Length)那么肯定,我们会考虑N的所有可能重载,它们的参数中包含函数或表达式类型,并尝试x的所有可能类型。当然,there are situations in which you can easily make the compiler try out billions of possible combinations可以找到有效的独特组合。类通用规则使得通用方法可以做到这一点非常复杂,甚至让Jon Skeet感到紧张。 This feature makes overload resolution NP-HARD

获取类型信息以便为lambdas双向流动,以便通用重载解析正常有效地工作了大约一年。这是一个如此复杂的特征,如果我们绝对肯定会对这笔投资有惊人的回报,我们只想接受它。使LINQ工作是值得的。但是没有像LINQ这样的相应功能可以证明一般来说这项工作需要付出巨大的代价。

答案 1 :(得分:7)

你必须这样做:

string dest = Gimme<int, string>(5);

您需要在泛型方法的调用中指定您的类型。怎么知道你想在输出中输入一个字符串?

System.String是一个不好的例子,因为它是一个密封的类,但是它说不是。如果您没有在调用中指定类型,编译器如何知道您不想要其中一个子类?

举个例子:

System.Windows.Forms.Control dest = Gimme(5);

编译器如何知道实际制作什么控件?您需要像这样指定它:

System.Windows.Forms.Control dest = Gimme<int, System.Windows.Forms.Button>(5);

答案 2 :(得分:5)

调用Gimme(5)忽略返回值是一个合法的陈述,编译器如何知道要返回哪种类型?

答案 3 :(得分:1)

我猜这是一个设计决定。我发现在用Java编程时它很有用。

与Java不同,C#似乎演变为函数式编程语言,你可以反过来获得类型推断,所以你可以拥有:

var dest = Gimme<int, string>(5);

将推断dest的类型。我想混合这个和java风格的推理可能会很难实现。

答案 4 :(得分:0)

如果函数应该返回少量类型中的一个,则可以让它返回一个具有已定义的扩展转换类的类。我认为不可能以通用方式执行此操作,因为扩展的ctype运算符不接受泛型类型参数。

答案 5 :(得分:0)

当我需要做类似的事情时,我会使用这种技术:

static void Gimme<T>(out T myVariable)
{
    myVariable = default(T);
}

并像这样使用它:

Gimme(out int myVariable);
Print(myVariable); //myVariable is already declared and usable.

请注意,自C#7.0

以来,可以使用out变量的内联声明

答案 6 :(得分:0)

public class ReturnString : IReq<string>
{
}

public class ReturnInt : IReq<int>
{
}

public interface IReq<T>
{
}

public class Handler
{
    public T MakeRequest<T>(IReq<T> requestObject)
    {
        return default(T);
    }
}

var handler = new Handler();
string stringResponse = handler.MakeRequest(new ReturnString());
int intResponse = handler.MakeRequest(new ReturnInt());