c# - 使用实现层次结构的多个接口(interface)的类进行类型推断

标签 c# generics inheritance type-inference

例如,让我们使用具有各种类型元素的计算器、评估不同元素类型的函数以及用于存储元素和运行函数的上下文。接口(interface)是这样的:

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);
}

请注意,函数可能返回任意类型。一个虚拟的实现如下,我有一个名为 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]);
    }
}

一些示例执行:
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

如您所见,我的问题是我似乎需要硬输入对 Context.Evaluate 的调用。 .如果我不这样做,编译器会说它无法推断参数的类型。这对我来说特别引人注目,因为在这两种情况下 Foo函数返回 double .

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

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

在任何情况下,有什么方法可以避免对 Task.Evaluate 的所有调用进行硬输入?在现实世界中,我有几个函数,所以能够避免它会很棒。

赏金关于为什么添加 <double> 的合理解释帮助编译器。可以这么说,这是编译器太懒的问题吗?

旧更新:使用委托(delegate)

一种选择是使用委托(delegate)而不是 IFunction s 在 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]);
    }
}

这样做,我们不需要硬输入<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

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

最佳答案

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

让我们从大幅简化代码开始。这是一个简短但完整的示例,它仍然演示了问题,但删除了所有不相关的内容。我还更改了 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);
    }
}

这仍然有同样的问题:
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.

现在,至于为什么会发生……问题是类型推断,正如其他人所说。 C# 中的类型推断机制(从 C# 3 开始)非常好,但它并没有想象的那么强大。

让我们看看在方法调用部分发生了什么,引用 C# 5 语言规范。

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

The set of candidate methods for the method invocation is constructed. For each method F associated with the method group M:

  • If F is non-generic, F is a candidate when:
    • M has no type argument list, and
    • F is applicable with respect to A (§7.5.3.1).
  • If F is generic and M has no type argument list, F is a candidate when:
    • Type inference (§7.5.2) succeeds, inferring a list of type arguments for the call, and
    • Once the inferred type arguments are substituted for the corresponding method type parameters, all constructed types in the parameter list of F satisfy their constraints (§4.4.4), and the parameter list of F is applicable with respect to A (§7.5.3.1).
  • If F is generic and M includes a type argument list, F is a candidate when:
    • F has the same number of method type parameters as were supplied in the type argument list, and
    • Once the type arguments are substituted for the corresponding method type parameters, all constructed types in the parameter list of F satisfy their constraints (§4.4.4), and the parameter list of F is applicable with respect to 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)

方法调用描述为:
M(E1 ... Em)

所以在我们的例子中,我们有:
  • Tr 是 TResult与 X1 相同。
  • T1 是 IFunction<IChildElement, TResult>
  • x1 是 function , 一个值参数
  • E1 是 f , 类型为 Foo

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

    7.5.2.1 The first phase
    For each of the method arguments Ei:

    • If Ei is an anonymous function, an explicit parameter type inference (§7.5.2.7) is made from Ei to Ti
    • Otherwise, if Ei has a type U and xi is a value parameter then a lower-bound inference is made from U to Ti.
    • Otherwise, if Ei has a type U and xi is a ref or out parameter then an exact inference is made from U to Ti.
    • Otherwise, no inference is made for this argument.


    第二个要点在这里是相关的:E1 不是匿名函数,E1 有一个类型 Foo , x1 是一个值参数。所以我们最终得到了来自 Foo 的下限推断到T1。该下限推理在 7.5.2.9 中描述。这里的重要部分是:

    Otherwise, sets U1...Uk and V1...Vk are determined by checking if any of the following cases apply:

    • [...]
    • V is a constructed class, struct, interface or delegate type C<V1...Vk> and there is a unique type C<U1...Uk> such that U (or, if U is a type parameter, its effective base class or any member of its effective interface set) is identical to, inherits from (directly or indirectly), or implements (directly or indirectly) C<U1...Uk>. (The "uniqueness" restriction means that in the case interface C<T>{} class U: C<X>, C<Y>{}, then no inference is made when inferring from U to C<T> because U1 could be X or Y.)


    就本部分而言,UFoo , 和 VIFunction<IChildElement, TResult> .然而,Foo实现两者 IFunction<IChildElement, double>IFunction<IGrandchildelement, double> .因此,即使在这两种情况下,我们最终都会将 U2 设为 double ,该条款不满足。

    在这方面让我感到惊讶的一件事是这不依赖于 TIFunction<in T, TResult>是逆变的。如果我们删除 in,我们会遇到同样的问题。部分。我本来希望它能在这种情况下工作,因为不会有来自 IFunction<IGrandchildElement, TResult> 的转换。至 IFunction<IChildElement, TResult> .这部分可能是编译器错误,但更有可能是我误读了规范。但是,在实际给出的情况下,这无关紧要 - 因为 T 的逆变,有这样一个转换,所以这两个接口(interface)真的很重要。

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

    这就是第一阶段的全部内容。

    第二阶段是这样描述的:

    7.5.2.2 The second phase

    The second phase proceeds as follows:

    • All unfixed type variables Xi which do not depend on (§7.5.2.5) any Xj are fixed (§7.5.2.10).
    • If no such type variables exist, all unfixed type variables Xi are fixed for which all of the following hold:
      • There is at least one type variable Xj that depends on Xi
      • Xi has a non-empty set of bounds
    • If no such type variables exist and there are still unfixed type variables, type inference fails.
    • Otherwise, if no further unfixed type variables exist, type inference succeeds.
    • Otherwise, for all arguments Ei with corresponding parameter type Ti where the output types (§7.5.2.4) contain unfixed type variables Xj but the input types (§7.5.2.3) do not, an output type inference (§7.5.2.6) is made from Ei to Ti. Then the second phase is repeated.


    我不会复制所有的子条款,但在我们的情况下......
  • 类型变量 X1 不依赖于任何其他类型变量,因为没有任何其他类型变量。所以我们需要修复X1。 (这里的部分引用是错误的 - 它实际上应该是 7.5.2.11。我会让 Mads 知道。)

  • 我们对 X1 没有限制(因为之前的下限推断没有帮助)所以我们最终在这一点上失败了类型推断。砰。这一切都取决于 7.5.2.9 中的独特性部分。

    当然,这可以解决。规范的类型推断部分可以变得更强大 - 问题是这也会使它更复杂,导致:
  • 开发人员更难推理类型推断(这已经够难了!)
  • 没有间隙地正确指定更难
  • 正确实现更难
  • 很可能,它在编译时的性能更差(这在 Visual Studio 等交互式编辑器中可能是一个问题,它需要执行相同的类型推断才能使 Intellisense 等工作正常工作)

  • 这都是一种平衡行为。我认为 C# 团队做得很好——事实上它在像这样的极端情况下不起作用并不是什么大问题,IMO。

    关于c# - 使用实现层次结构的多个接口(interface)的类进行类型推断,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15697302/

    相关文章:

    c# - 修改注册表键值

    c# - Visual Studio Intellisense 是如何工作的?

    java - 如何避免使用来自不同包的生成类的重复代码

    ruby-on-rails - 单表继承查找问题

    c# - 为什么 IsAssignableFrom 和 GetInterface 给出不同的结果

    c# - IIDentity 不包含 'GetUserId' 的定义

    javascript - 将对象从 Ajax 传递到 C# WebApi

    java - 通过泛型参数实例化类 - 哪种方法更好

    c# - 在 ASP.NET MVC 2 Web 应用程序中输出具有 JSON 关系的 LINQ to SQL 实体时出现循环引用错误

    java - 从抽象类调用未声明的 super 方法时获取当前对象属性