例如,让我们使用具有各种类型元素的计算器、评估不同元素类型的函数以及用于存储元素和运行函数的上下文。接口(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
的函数。可以同时用于 IChildElement
和 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>
的合理解释帮助编译器。可以这么说,这是编译器太懒的问题吗?旧更新:使用委托(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)
所以在我们的例子中,我们有:
TResult
与 X1 相同。 IFunction<IChildElement, TResult>
function
, 一个值参数 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.)
就本部分而言,
U
是 Foo
, 和 V
是 IFunction<IChildElement, TResult>
.然而,Foo
实现两者 IFunction<IChildElement, double>
和 IFunction<IGrandchildelement, double>
.因此,即使在这两种情况下,我们最终都会将 U2 设为 double
,该条款不满足。在这方面让我感到惊讶的一件事是这不依赖于
T
在 IFunction<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 没有限制(因为之前的下限推断没有帮助)所以我们最终在这一点上失败了类型推断。砰。这一切都取决于 7.5.2.9 中的独特性部分。
当然,这可以解决。规范的类型推断部分可以变得更强大 - 问题是这也会使它更复杂,导致:
这都是一种平衡行为。我认为 C# 团队做得很好——事实上它在像这样的极端情况下不起作用并不是什么大问题,IMO。
关于c# - 使用实现层次结构的多个接口(interface)的类进行类型推断,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15697302/