使用实现层次结构的多个接口的类来键入推断

Jul*_*ano 24 c# generics inheritance type-inference

作为一个例子,让我们使用类似计算器的东西,各种类型的元素,评估不同元素类型的函数,以及存储元素和运行函数的上下文.接口是这样的:

public interface IElement {
}
public interface IChildElement : IElement {
    double Score { get; }
}
public interface IGrandchildElement : IChildElement {
    int Rank { get; }
}

public interface IFunction<Tout, in Tin> where Tin : IElement {
    Tout Evaluate(Tin x, Tin y);
}

public interface IContext<Tin> where Tin : IElement {
    Tout Evaluate<Tout>(string x, string y, IFunction<Tout, Tin> eval);
}
Run Code Online (Sandbox Code Playgroud)

请注意,函数可能返回任意类型.一个虚拟实现如下,其中我有一个被调用的函数Foo,可用于IChildElementIGrandchildElement,并double在两种情况下返回:

public class ChildElement : IChildElement {
    public double Score { get; internal set; }
}
public class GrandchildElement : ChildElement, IGrandchildElement {
    public int Rank { get; internal set; }
}

public class Foo : IFunction<double, IChildElement>, IFunction<double, IGrandchildElement> {
    public double Evaluate(IChildElement x, IChildElement y) {
        return x.Score / y.Score;
    }
    public double Evaluate(IGrandchildElement x, IGrandchildElement y) {
        return x.Score * x.Rank / y.Score / y.Rank;
    }
}

public class Context<T> : IContext<T> where T : IElement {
    protected Dictionary<string, T> Results { get; set; }

    public Context() {
        this.Results = new Dictionary<string, T>();
    }

    public void AddElement(string key, T e) {
        this.Results[key] = e;
    }
    public Tout Evaluate<Tout>(string x, string y, IFunction<Tout, T> eval) {
        return eval.Evaluate(this.Results[x], this.Results[y]);
    }
}
Run Code Online (Sandbox Code Playgroud)

一些示例执行:

Context<IChildElement> cont = new Context<IChildElement>();
cont.AddElement("x", new ChildElement() { Score = 1.0 });
cont.AddElement("y", new ChildElement() { Score = 2.0 });
Foo f = new Foo();
double res1 = cont.Evaluate("x", "y", f); // This does not compile
double res2 = cont.Evaluate<double>("x", "y", f); // This does
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,我的问题是我似乎需要硬打电话给Context.Evaluate.如果我不这样做,编译器说它无法推断出参数的类型.这对我来说特别引人注目,因为在这两种情况下Foo函数都会返回double.

如果Foo只实现IFunction<double, IChildElement>IFunction<double, IGrandchildElement>我没有这个问题.但确实如此.

我不明白.我的意思是,添加<double>不区分IFunction<double, IGrandchildElement>IFunction<double, IChildElement>因为它们都返回double.据我所知,它没有为编译器提供任何额外的信息来区分.

在任何情况下,有什么方法可以避免硬键入所有的呼叫Task.Evaluate?在现实世界中,我有几个功能,所以能够避免它会很棒.

Bounty声明解释为什么添加<double>有助于编译器.这是编译器太懒的问题吗?

旧更新:使用委托

一个选项可能是使用代理而不是IFunctions IContext.Evaluate:

public interface IContext<Tin> where Tin : IElement {
    Tout Evaluate<Tout>(string x, string y, Func<Tin, Tin, Tout> eval);
}
public class Context<T> : IContext<T> where T : IElement {
    // ...
    public Tout Evaluate<Tout>(string x, string y, Func<T, T, Tout> eval) {
        return eval(this.Results[x], this.Results[y]);
    }
}
Run Code Online (Sandbox Code Playgroud)

这样做,我们<double>在调用时不需要硬键入IContext.Evaluate:

Foo f = new Foo();
double res1 = cont.Evaluate("x", "y", f.Evaluate); // This does compile now
double res2 = cont.Evaluate<double>("x", "y", f.Evaluate); // This still compiles
Run Code Online (Sandbox Code Playgroud)

所以编译器在这里按预期工作.我们避免了硬类型的需要,但我不喜欢我们使用IFunction.Evaluate而不是IFunction对象本身的事实.

Jon*_*eet 35

(我没有通过代表版本.我认为这个答案已经足够了......)

让我们从大大简化代码开始.这是一个简短但完整的例子,它仍然可以证明这个问题,但删除了所有不相关的内容.我也改变了类型参数的顺序,IFunction以匹配更多的常规约定(例如Func<T, TResult>):

// We could even simplify further to only have IElement and IChildElement...
public interface IElement {}
public interface IChildElement : IElement {}
public interface IGrandchildElement : IChildElement {}

public interface IFunction<in T, TResult> where T : IElement
{
    TResult Evaluate(T x);
}

public class Foo : IFunction<IChildElement, double>,
                   IFunction<IGrandchildElement, double>
{
    public double Evaluate(IChildElement x) { return 0; }
    public double Evaluate(IGrandchildElement x) { return 1; }
}

class Test
{
    static TResult Evaluate<TResult>(IFunction<IChildElement, TResult> function)
    {
        return function.Evaluate(null);
    }

    static void Main()
    {
        Foo f = new Foo();
        double res1 = Evaluate(f);
        double res2 = Evaluate<double>(f);
    }
}
Run Code Online (Sandbox Code Playgroud)

这仍然有同样的问题:

Test.cs(27,23): error CS0411: The type arguments for method
        'Test.Evaluate<TResult>(IFunction<IChildElement,TResult>)' cannot be
        inferred from the usage. Try specifying the type arguments explicitly.
Run Code Online (Sandbox Code Playgroud)

现在,至于它为什么会发生......问题是类型推断,正如其他人所说的那样.在C#中的类型推理机制(其形式为C#3)是相当不错的,但它不是那么强大,因为它可能是.

让我们看看方法调用部分发生了什么,参考C#5语言规范.

7.6.5.1(方法调用)是这里的重要部分.第一步是:

构造了方法调用的候选方法集.对于与方法组M关联的每个方法F:

  • 如果F是非泛型的,则F是以下情况的候选者:
    • M没有类型参数列表,和
    • F适用于A(§7.5.3.1).
  • 如果F是通用的且M没有类型参数列表,则F是以下情况的候选者:
    • 类型推断(第7.5.2节)成功,推断出调用的类型参数列表,以及
    • 一旦推断的类型参数替换相应的方法类型参数,F的参数列表中的所有构造类型都满足它们的约束(§4.4.4),并且F的参数列表适用于A(§7.5.3.1) ).
  • 如果F是通用的并且M包括类型参数列表,则F在以下情况下是候选者:
    • F具有与类型参数列表中提供的方法类型参数相同的数量,并且
    • 一旦类型参数替换了相应的方法类型参数,F参数列表中的所有构造类型都满足它们的约束(§4.4.4),并且F的参数列表适用于A(§7.5.3.1) .

现在,方法组M是一个带有单个方法的集合(Test.Evaluate) - 幸运的是,第7.4节(成员查找)很简单.所以我们只有一个F方法可以考虑.

通用的,M没有类型参数列表,所以我们直接在7.5.2节 - 类型推断中结束.请注意如何,如果有一个参数列表,这是完全忽略,而上面的第三个重大项目符号点是满意的-这就是为什么Evaluate<double>(f)调用成功.

所以,我们现在有一个很好的迹象表明问题在于类型推断.让我们深入研究它.(这是它变得棘手的地方,我很害怕.)

7.5.2本身大多只是描述,包括类型推断分阶段发生的事实.

我们试图调用的泛型方法描述为:

Tr M<X1...Xn>(T1 x1 ... Tm xm)
Run Code Online (Sandbox Code Playgroud)

并且方法调用描述为:

M(E1 ... Em)
Run Code Online (Sandbox Code Playgroud)

所以在我们的案例中,我们有:

  • Ť - [RTResult其中相同X 1.
  • T 1IFunction<IChildElement, TResult>
  • x 1function值参数
  • E 1f类型的Foo

现在让我们尝试将其应用于其余的类型推断......

7.5.2.1第一阶段
对于每个方法参数E i:

  • 如果E i是匿名函数,则从E i到T i进行显式参数类型推断(第7.5.2.7节)
  • 否则,如果E i具有类型U并且x i是值参数,则从U到T i进行下限推断.
  • 否则,如果E i具有类型U并且x i是ref或out参数,则从U到T i进行精确推断.
  • 否则,不会对此参数进行推断.

第二个要点与此相关:E 1不是匿名函数,E 1是类型Foo,x 1是值参数.因此,我们最终得到了从FooT 1到T 1的下界推论.7.5.2.9中描述了这种下限推断.这里的重要部分是:

否则,设置U 1 ... U k和V 1 ... V k通过检查是否适用以下任何一种情况来确定:

  • [...]
  • V是构造的类,结构,接口或委托类型C <V 1 ... V k >并且存在唯一类型C <U 1 ... U k >,使得U(或者,如果U是类型参数) ,其有效基类或其有效接口集的任何成员)与(直接或间接)继承,(直接或间接)C <U 1 ... U k >相同.("唯一性"限制意味着在接口C <T> {}类U:C <X>,C <Y> {}的情况下,当从U到C <T>推断时没有做出推断,因为U 1可以是X或Y.)

就本部分而言,UFoo,VIFunction<IChildElement, TResult>.但是,Foo实现IFunction<IChildElement, double>IFunction<IGrandchildelement, double>.因此,即使在这两种情况下,我们最终会带U 2double,本条款不满意.

有一两件事,确实在这让我感到吃惊的是,这并不依赖TIFunction<in T, TResult>被逆变.如果我们删除该in部分,我们会遇到同样的问题.我原以为它会在那种情况下工作,因为没有转换IFunction<IGrandchildElement, TResult>IFunction<IChildElement, TResult>.该部分可能是编译器错误,但更可能是我误读了规范.但是,这实际上给的情况下,这是不相关的-因为逆变的T,还有就是这种转换,所以两种接口真的是显著.

无论如何,这意味着我们实际上并没有从这个论点得到任何类型的推论!

这是整个第一阶段.

第二阶段描述如下:

7.5.2.2第二阶段

第二阶段进行如下:

  • 所有不固定的类型变量Xi不依赖于(§7.5.2.5)任何Xj是固定的(§7.5.2.10).
  • 如果不存在这样的类型变量,则固定所有未固定的类型变量Xi,以下所有变量都保持:
    • 至少有一个类型变量Xj依赖于Xi
    • Xi有一组非空的边界
  • 如果不存在此类型变量且仍存在未固定的类型变量,则类型推断将失败.
  • 否则,如果不存在其他未固定的类型变量,则类型推断成功.
  • 否则,对于具有相应参数类型Ti的所有参数Ei,其中输出类型(第7.5.2.4节)包含不固定类型变量Xj但输入类型(第7.5.2.3节)不包含,输出类型推断(第7.5.2.6节)是由Ei制成Ti.然后重复第二阶段.

我不打算复制所有子条款,但在我们的案例中......

  • 类型变量X 1不依赖于任何其他类型变量,因为没有任何其他类型变量.所以我们需要修复 X 1.(这里的部分引用是错误的 - 实际应该是7.5.2.11.我会让Mads知道.)

我们没有X 1的界限(因为之前的下限推断没有帮助)所以我们最终在这一点上失败了类型推断.砰.这一切都取决于7.5.2.9中的唯一性部分.

当然,这可以修复.该规范的类型推断的部分可以变得更强大-麻烦的是,这也将使其更加复杂,从而导致:

  • 开发人员更难以推断类型推断(这很难实现!)
  • 正确指定没有间隙更难
  • 正确实施起来比较困难
  • 很可能,它在编译时性能更差(这可能是交互式编辑器中的问题,例如Visual Studio,需要对Intellisense等工作执行相同的类型推断)

这都是一种平衡行为.我认为C#团队已经做得很好 - 事实上它在像这样的角落情况下不起作用并不是一个问题,IMO.