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,可用于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]);
}
}
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)
所以在我们的案例中,我们有:
TResult其中相同X 1.IFunction<IChildElement, TResult>function值参数f类型的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.)
就本部分而言,U是Foo,V是IFunction<IChildElement, TResult>.但是,Foo实现IFunction<IChildElement, double>和IFunction<IGrandchildelement, double>.因此,即使在这两种情况下,我们最终会带U 2为double,本条款不满意.
有一两件事,确实在这让我感到吃惊的是,这并不依赖T于IFunction<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的界限(因为之前的下限推断没有帮助)所以我们最终在这一点上失败了类型推断.砰.这一切都取决于7.5.2.9中的唯一性部分.
当然,这可以修复.该规范的类型推断的部分可以变得更强大-麻烦的是,这也将使其更加复杂,从而导致:
这都是一种平衡行为.我认为C#团队已经做得很好 - 事实上它在像这样的角落情况下不起作用并不是一个问题,IMO.