为什么不能从由同一泛型定义的参数作为委托方法的一部分来推断泛型?

时间:2019-10-28 13:52:52

标签: c# generics delegates type-inference

我不明白为什么无法推断出对GenericMethod<T>()的呼叫。我的实际问题包含更多通用参数,从而使调用代码非常冗长且难以阅读。

如果参数本身是'T',则可以在没有显式类型参数的情况下调用

GenericMethod<T>(),但是约束不适用于委托,一旦您输入了受约束的类型参数,您将返回方一。

delegate void GenericDelegate<T>(T t);

static void GenericMethod<T>(GenericDelegate<T> _) { }

static void IntMethod(int _) { }

static void CallingMethod()
{
    // The type arguments for method 'GenericMethod<T>(GenericDelegate<T>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.
    GenericMethod(IntMethod);
}

是否有另一种方法可以使调用代码像此处所述一样简单? GenericMethod<T>()是内部库的一部分。

3 个答案:

答案 0 :(得分:7)

  

我不明白为什么无法推断出对GenericMethod<T>()的呼叫。

这个问题有点含糊。让我尝试整理一下。

  

关于参数是方法组的泛型方法类型推断,C#规范怎么说?

推断分为两个阶段进行。在第一阶段,从已知参数类型的匿名函数参数和具有类型的参数进行推论。方法组没有类型,因此在第一阶段不对方法组进行推论。

在第二阶段,从 lambda和方法组进行推论,其中目标类型是已推断输入类型的委托类型

在您的示例中,输入类型为T,尚未推断出,因此不会进行推断。

这就是类型推断失败的原因。

该答案可能无法令人满意,但这是因为您提出了一个模糊的问题。让我们通过询问后续问题来再次尝试解决问题的清晰版本:

  

什么证明了设计决定的正确性,即在类型推断考虑方法组的证据之前必须先推断输入类型?

我们之所以做出此决定,是因为方法组中的证据是通过首先从用户打算参考的组中确定确切方法得出的。我们如何确定该方法?用相同的方法,我们可以确定在任何其他情况下方法组中要使用的方法。 我们对该方法组进行重载解析。但是重载解析需要知道参数的类型,这意味着在分析方法组时,输入类型必须

例如,如果我们有void M<A, B>(A a, Func<A, B> f){}string N(int y)以及M(123, N),则我们首先确定A是第一阶段的int,现在确定Func<A, B>中的输入是已知的,因此重载分辨率可以确定是否打算使用string N(int),因此Bstring

现在您的后续问题肯定是:

  

但是在我的情况下,方法组只有一个成员。编译器不能简单地跳过重载解析并确定我的意思是该组中唯一的方法吗?

您真正要说的是,您需要两个不同的规则,一个针对您的情况进行量身定制,而另一个通常适用。

首先,C#设计团队尝试避免为狭义的罕见情况创建规则的情况。类型推断的重点不是使用技巧和试探法进行所有可能的推断。目的是原则并尽可能使用现有算法。而且,推理算法已经很复杂。不要让它变得更复杂。

但是哦,情况变得更糟。假设C#设计团队决定实施规则“如果方法组包含一个方法,则跳过超载解析”。我想那会让您暂时满意,但是连锁效应是什么? 我们刚刚创建了一个炸弹,稍后将其添加到方法组中时会爆炸。不幸的是,添加第二种方法可能会在不相关的位置带来潜在的突破性变化。

此外,您可能不会很满意。 “我添加了第二种方法,但是它的含义有所不同,因此请再次更改重载分辨率,以丢弃错误的方法的方法,直到只剩下一个为止”……依此类推。再次,设计,指定,实现,测试,记录和解释算法变得越来越复杂,越来越难。当然,随着这些算法变得越来越复杂,它们变得更有可能按照用户的意图进行操作,但是当他们不这样做时,用户就会被一大堆规则理解。 (有人可能会说我们已经在那儿了,所以不要让它变得更糟。)

简而言之:原则上,当然可以进行您想要的演绎。编译器团队选择不这样做,以使推理算法为人类所理解。

  

为什么使用lambda会有什么不同?编译为:GenericMethod((int v) => IntMethod(v))

您现在应该拥有必要的信息,可以回答该问题,但需要说明:

  • lambda内IntMethod的重载分辨率知道v的类型为int,因此重载分辨率成功。

  • 现在过载解析成功了,我们知道lambda是int => void,并且可以对void GenericDelegate<T>(T t)进行推断。

如果您改为编写GenericMethod(v => IntMethod(v))类型推断,将会再次失败。在知道v的类型之前,无法分析lambda的主体,并且v的类型取决于推断的T的类型,这就是我们要尝试的工作首先,所以推理算法没有任何进展,但失败了。

  

是否有另一种方法可以使调用代码像此处所述一样简单?

不。只需插入<int>并完全跳过类型推断。或将参数强制转换为适当的GenericDelegate类型,这将使​​重载解析在给定通用委托中的类型的方法组上进行操作。

答案 1 :(得分:1)

看看埃里克·利珀特(Eric Lippert)关于类型推断的series

归结为任何语言功能的原因

  • 实施起来可能很昂贵(时间,资源)。
  • 必须仔细考虑,尤其是在重大变化中。

答案 2 :(得分:0)

Eric Lippert's answer提供了有关为何不起作用的所有详细信息,以及围绕它的更多设计理念。对于将来的Google员工,我将提供一种简短的方法来描述问题。在对问题代码稍作修改后,很明显为什么不能推断出类型:

delegate void GenericDelegate<T>(T t);

static void GenericMethod<T>(GenericDelegate<T> method) { }

static void Method(int _) { }
static void Method(float _) { }

static void CallingMethod()
{
    GenericMethod(Method);
}

当方法组Method有两个重载时,编译器无法推断T的类型。即使该方法组仅包含一个重载,问题实际上也完全相同!

现在,要回答我的另一个问题,调用代码是否可以像上述那样简单?好吧,并非没有重大重组。像这样的东西对我有用,

static void GenericMethod<T>(IMethod<T> method) { }

interface IMethod<T>
{
    void Method(T _);
}

class IntMethod: IMethod<int>
{
    public void Method(int _) { }
}

static void CallingMethod()
{
    GenericMethod(new IntMethod());
}

我承认,它与上面的代码示例有很大不同,但是实现了我的目标:一个干净的调用站点。此解决方案以稍微凌乱的声明为代价。可读性和使用性是成败是高度依赖用例。