【问题标题】:Type inference with class implementing several interfaces of a hierarchy使用实现层次结构的多个接口的类进行类型推断
【发布时间】:2013-03-29 04:22:18
【问题描述】:

例如,让我们使用计算器之类的东西,其中包含各种类型的元素、评估不同元素类型的函数以及存储元素和运行函数的上下文。接口是这样的:

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&lt;double, IChildElement&gt;IFunction&lt;double, IGrandchildElement&gt; 我没有这个问题。但确实如此。

我不明白。我的意思是,添加&lt;double&gt; 不会区分IFunction&lt;double, IGrandchildElement&gt;IFunction&lt;double, IChildElement&gt;,因为它们都返回double。据我了解,它没有为编译器提供任何额外的信息来区分。

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

Bounty 可以很好地解释为什么添加 &lt;double&gt; 有助于编译器。这是编译器懒得说的问题吗?

旧更新:使用委托

一个选项可以是在IContext.Evaluate 中使用代表而不是IFunctions:

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

这样做,我们在调用IContext.Evaluate 时不需要硬输入&lt;double&gt;

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 对象本身这一事实。

【问题讨论】:

    标签: c# generics inheritance type-inference


    【解决方案1】:

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

    让我们从大幅简化代码开始。这是一个简短但完整的示例,它仍然演示了问题,但删除了所有不相关的内容。我还更改了IFunction 中类型参数的顺序,以匹配更正常的约定(例如Func&lt;T, TResult&gt;):

    // 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(方法调用)是这里的重要部分。第一步是:

    构造方法调用的候选方法集。对于与方法组 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&lt;double&gt;(f) 调用成功的原因。

    所以,我们现在已经很好地表明问题在于类型推断。让我们深入了解它。 (恐怕这就是棘手的地方。)

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

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

    Tr M<X1...Xn>(T1 x1 ... Tm xm)
    

    方法调用描述为:

    M(E1 ... Em)
    

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

    • TrTResult,与 X1 相同。
    • T1IFunction&lt;IChildElement, TResult&gt;
    • x1function,一个值参数
    • E1f,其类型为Foo

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

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

    • 如果 Ei 是匿名函数,则从 Ei 到 Ti进行显式参数类型推断 (§7.5.2.7) >
    • 否则,如果 Ei 具有类型 U 并且 xi 是值参数,则从 U 到 Ti。
    • 否则,如果 Ei 具有类型 U 并且 xi 是 ref 或 out 参数,则从 U 到 Ti。
    • 否则,不会对此参数做出任何推断。

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

    否则,设置 U1...Uk 和 V1...Vk 是通过检查是否适用以下任何一种情况来确定:

    • [...]
    • V 是一个构造类、结构、接口或委托类型 C1...Vk> 并且有一个唯一类型 C1...Uk> 使得 U(或者,如果 U 是类型参数,则其有效基类或其有效接口集的任何成员)相同,继承来自(直接或间接),或实现(直接或间接)C1...Uk>。 (“唯一性”限制意味着在接口 C{} 类 U: C, C{} 的情况下,从 U 推断到 C 时不会进行推断,因为 U1 可以是 X 或 Y。)

    就本部分而言,UFooVIFunction&lt;IChildElement, TResult&gt;。然而,Foo 实现了IFunction&lt;IChildElement, double&gt;IFunction&lt;IGrandchildelement, double&gt;。所以即使在这两种情况下我们最终都会得到 U2 作为double,但这个子句并不满足。

    确实让我感到惊讶的一件事是,这不依赖于 IFunction&lt;in T, TResult&gt; 中的 T 是逆变的。如果我们删除 in 部分,我们会遇到同样的问题。我原以为它会在这种情况下工作,因为不会有从IFunction&lt;IGrandchildElement, TResult&gt;IFunction&lt;IChildElement, TResult&gt; 的转换。 可能该部分是编译器错误,但更有可能是我误读了规范。然而,在实际给出的情况下,这是无关紧要的——因为T 的逆变性,有这样的转换,所以这两个接口真的很重要。

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

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

    第二阶段是这样描述的:

    7.5.2.2 第二阶段

    第二阶段进行如下:

    • 所有不依赖于 (§7.5.2.5) 任何 Xj 的未固定类型变量 Xi 都是固定的 (§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 制成。然后重复第二阶段。

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

    • 类型变量 X1 不依赖于任何其他类型变量,因为没有任何其他类型变量。所以我们需要fix X1。 (这里的部分参考是错误的 - 它实际上应该是 7.5.2.11。我会让 Mads 知道。)

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

    当然,这可以解决。规范的类型推断部分可以变得更强大 - 问题是这也会使其更复杂,导致:

    • 开发人员更难推理类型推断(这已经够难了!)
    • 很难正确指定没有间隙
    • 正确实施比较困难
    • 很可能,它在编译时性能较差(这可能是 Visual Studio 等交互式编辑器中的一个问题,它需要执行相同的类型推断才能使 Intellisense 等功能正常工作)

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

    【讨论】:

    • 我不同意,如果允许更灵活的规则推理,开发人员就更难推理类型推理。在我看来,类型推断就是猜测开发人员打算做什么。在这种特殊情况下,本可以做得更好。实际上,为什么规范不允许这种语法比反过来更难理解。
    • @MårtenWikström:但如果你假设总会有一些的情况,规则并没有真正给出答案,那么在某些时候,开发人员需要能够阅读规则。规则越复杂,就越难遵守。这就是我的观点。
    • 我同意这一点,但如果允许不同的绑定过程将一组候选方法“传递”到下一个过程,那么我认为规范将不那么复杂和更灵活。以这种方式,将为每个候选者考虑绑定的所有方面。这对我来说更有意义,并且可以避免这样的问题。
    • @MårtenWikström:我认为您必须先尝试提出一个完整的替代规范,然后才能真正说出……并确保它适用于重载、匿名函数等。我怀疑你会发现很难让它变得更强大和更简单。
    • 是的,你是对的。我有一个替代方法的想法,但它可能对我来说太复杂了,无法获得所有复杂的细节。
    【解决方案2】:

    类型推断失败,因为编译器无法将类型参数固定为唯一映射。

    IFunction&lt;double, IChildElement&gt;IFunction&lt;double, IGrandChildElement&gt; 之间存在歧义,因为它们都是可绑定的。

    当类型推断失败时,您必须明确指定类型参数。这符合 C# 语言规范。

    通过指定显式类型参数,您可以帮助编译器,因为它可以完全跳过类型推断。

    在您明确指定T 绑定到double 后,不再有歧义,因为Tin 通过您的Context&lt;IChildElement&gt; 声明绑定到IChildElement,并且Tout 绑定到@987654329 @ 通过显式类型参数。

    我同意您可能会争辩说编译器可能已经推断出这种用法,因为在这种情况下,类型参数实际上并没有提供任何额外的信息。

    但是,规范说:

    类型推断是作为绑定时处理的一部分发生的 方法调用(第 7.6.5.1 节)并在重载之前发生 调用的解析步骤

    ...所以我猜他们想把这些东西分开。其原因超出了我的范围。我猜这可能是规范的简单性或对未来扩展的支持,或者只是他们没有想到它:-)

    【讨论】:

      【解决方案3】:

      发生这种情况的原因是Foo()IChildElementIGrandchildElement 实现了IFunction。由于您的用法是IChildElement 类型,它可能指的是IChildElementIGrandchildElement,因此调用是模棱两可的,因为IFunction&lt;double, IGrandchildElement&gt;IFunction&lt;double, IChildElement&gt;。请注意,问题不是因为IChildElementIGrandchildElement 而引起的,而是因为它实现了两种潜在的IFunction 类型,它甚至没有考虑返回类型double。 p>

      // f is both an IFunction<double, IGrandchildElement>
      // and an IFunction<double, IChildElement>
      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
      

      因此,您需要以某种方式使其更具体,有两种使用强制转换的方法:

      double res3 = cont.Evaluate<double>("x", "y", f);
      double res4 = cont.Evaluate("x", "y", (IFunction<double, IChildElement>)f);
      

      您不想像您所说的那样每次都这样做,但最后一行的转换方法揭示了您的问题的潜在解决方案;将Foo 转换为所需接口的变量并在调用cont.Evaluate() 时使用该变量。

      IFunction<double, IChildElement> iFunc = f;
      double res5 = cont.Evaluate("x", "y", iFunc);
      

      【讨论】:

      • 我明白你的意思,但我仍然不明白为什么Evaluate 不起作用而Evaluate&lt;double&gt; 起作用。我的意思是,添加&lt;double&gt; 并不能真正区分IFunction&lt;double, IGrandchildElement&gt;IFunction&lt;double, IChildElement&gt;,因为它们都返回double。据我了解,它没有为编译器提供任何额外的信息来区分。
      • 事实上IFunction&lt;double, IGrandchildElement&gt;IFunction&lt;double, IChildElement&gt; 都是相关类型,这让编译器感到困惑。它想要一个返回类型,但不会费心去弄清楚它,因为它可能有两种类型。我同意你的观点,这很令人困惑,在写这个答案之前,我在你的类型上试验了大约 20 分钟。
      • “它不会费心去想办法”是的,而且不管有没有&lt;double&gt;,它都有相同的信息。对我来说似乎有点俗气。无论如何,这样的东西不是已经包含在 .net 4.5 中了吗?
      • 隐式类型推断可能是您在说的吗?这真的很酷,但这似乎是它不起作用的情况之一。
      • 您如何看待使用委托代替?请查看问题更新。
      猜你喜欢
      • 2021-02-21
      • 2019-05-10
      • 2011-01-21
      • 1970-01-01
      • 1970-01-01
      • 2010-09-26
      • 2015-05-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多