24

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

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,可以同时用于IChildElementand IGrandchildElement,并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>对编译器有帮助。这是编译器懒得说的问题吗?

旧更新:使用委托

一个选项可能是使用委托而不是IFunctions in 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对象本身的事实。

4

3 回答 3

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

这仍然有同样的问题:

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<double>(f)调用成功的原因。

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

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

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

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

方法调用描述为:

M(E1 ... Em)

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

  • T rTResultX 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有一个 type Foo,而 x 1是一个值参数。Foo所以我们最终得到一个从到 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<U 1 ...U k >。(“唯一性”限制意味着在接口 C<T>{} 类 U: C<X>, C<Y>{} 的情况下,从 U 推断到 C<T> 时不会进行推断,因为 U 1可以是 X 或 Y。)

就本部分而言,UisFooVis IFunction<IChildElement, TResult>。但是,Foo同时实现IFunction<IChildElement, double>IFunction<IGrandchildelement, double>。因此,即使在这两种情况下我们最终都会得到 U 2 as double,但这个子句并不满足。

令我感到惊讶的一件事是,这并不T依赖于inIFunction<in T, TResult>是逆变的。如果我们删除in零件,我们会遇到同样的问题。我原以为它会在这种情况下工作,因为不会有从IFunction<IGrandchildElement, TResult>to的转换IFunction<IChildElement, TResult>。那部分可能是编译器错误,但更有可能是我误读了规范。但是,在实际给出的情况下,这是无关紧要的——因为 的逆变性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。然后重复第二阶段。

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

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

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

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

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

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

于 2013-04-27T08:41:53.523 回答
3

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

和之间存在歧义IFunction<double, IChildElement>IFunction<double, IGrandChildElement>因为它们都是可绑定的。

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

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

在您明确指定T绑定到double之后,不再有歧义,因为通过您的声明Tin绑定到并且通过显式类型参数绑定到。IChildElementContext<IChildElement>Toutdouble

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

但是,规范说:

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

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

于 2013-04-23T18:05:48.147 回答
2

发生这种情况的原因是因为Foo()实现IFunctionIChildElementIGrandchildElement。由于您的用法是 type IChildElement,因此它可能指的是一个IChildElementIGrandchildElement多个调用是模棱两可的,因为IFunction<double, IGrandchildElement>is a IFunction<double, IChildElement>。请注意,问题不是因为IChildElementand引起的IGrandchildElement,而是因为它实现了两种可能的IFunction类型,它甚至没有考虑返回类型double

// 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);
于 2013-03-29T04:42:30.413 回答