SAR*_*ARI 71 .net c# overriding optional-parameters
似乎在.NET Framework中,覆盖方法时可选参数存在问题.下面代码的输出是:"bbb""aaa".但我期待的输出是:"bbb""bbb".有解决方案.我知道它可以用方法重载解决,但想知道原因.此外,代码在Mono中运行良好.
class Program
{
class AAA
{
public virtual void MyMethod(string s = "aaa")
{
Console.WriteLine(s);
}
public virtual void MyMethod2()
{
MyMethod();
}
}
class BBB : AAA
{
public override void MyMethod(string s = "bbb")
{
base.MyMethod(s);
}
public override void MyMethod2()
{
MyMethod();
}
}
static void Main(string[] args)
{
BBB asd = new BBB();
asd.MyMethod();
asd.MyMethod2();
}
}
Run Code Online (Sandbox Code Playgroud)
Mar*_*ell 35
你可以通过调用来消除歧义:
this.MyMethod();
Run Code Online (Sandbox Code Playgroud)
(中MyMethod2())
是否是一个bug是棘手的; 但它确实看起来不一致.如果有帮助的话,Resharper警告你根本不要更改覆盖中的默认值; p当然,resharper 也告诉你这this.是多余的,并提议为你删除它...这改变了行为 - 所以resharper也并不完美.
看起来它确实可以作为编译器错误,我会授予你.我需要看真仔细,以确保...这里的埃里克·当你需要他,是吧?
编辑:
这里的关键点是语言规范; 让我们看看§7.5.3:
例如,方法调用的候选集不包括标记为override的方法(第7.4节),如果派生类中的任何方法适用(第7.6.5.1节),则基类中的方法不是候选方法.
(事实上§7.4明确忽略了override考虑的方法)
这里有一些冲突....它声明如果派生类中有适用的方法,则不使用基本方法 - 这将导致我们使用派生方法,但同时,它表示标记override的方法不是考虑.
但是,§7.5.1.1则说明:
对于在类中定义的虚方法和索引器,参数列表是从函数成员的最具体的声明或覆盖中选取的,从接收器的静态类型开始,并搜索其基类.
然后§7.5.1.2解释了在调用时如何评估值:
在函数成员调用(第7.5.4节)的运行时处理期间,参数列表的表达式或变量引用按从左到右的顺序进行计算,如下所示:
......(中略)...
当从具有相应可选参数的函数成员中省略参数时,将隐式传递函数成员声明的默认参数.因为它们总是不变的,所以它们的评估不会影响其余参数的评估顺序.
这明确强调它正在查看参数列表,该列表先前在§7.5.1.1中定义为来自最具体的声明或覆盖.这是§7.5.1.2中引用的"方法声明"似乎是合理的,因此传递的值应该来自最多派生的静态类型.
这表明:csc有一个bug,它应该使用派生版本("bbb bbb"),除非它被限制(通过base.或转换为基类型)来查看基本方法声明(§7.6.8) ).
Jon*_*nna 24
这里值得注意的一点是,每次调用被覆盖的版本.将覆盖更改为:
public override void MyMethod(string s = "bbb")
{
Console.Write("derived: ");
base.MyMethod(s);
}
Run Code Online (Sandbox Code Playgroud)
输出是:
derived: bbb
derived: aaa
Run Code Online (Sandbox Code Playgroud)
类中的方法可以执行以下一个或两个:
它可能不会同时做到这两点,因为抽象方法只做前者.
在BBB调用MyMethod()调用方法定义在AAA.
因为有一个覆盖BBB,调用该方法会导致调用实现BBB.
现在,定义AAA通知调用两个东西的代码(好吧,其他几个也没关系).
void MyMethod(string)."aaa",因此在编译表单代码时,MyMethod()如果找不到方法匹配MyMethod(),则可以用对"MyMethod("aaa")的调用替换它.所以,这就是调用的BBB作用:编译器看到一个调用MyMethod(),找不到方法,MyMethod()但确实找到了一个方法MyMethod(string).它还会看到在定义它的地方有一个默认值"aaa",因此在编译时它会将其更改为调用MyMethod("aaa").
从内部来看BBB,AAA被认为AAA是定义方法的地方,即使被覆盖BBB,也可以被覆盖.
在运行时,MyMethod(string)使用参数"aaa"调用.因为有一个被覆盖的表单,即被调用的表单,但它不是用"bbb"调用的,因为该值与运行时实现无关,而是与编译时定义无关.
添加this.检查定义的更改,从而更改调用中使用的参数.
编辑:为什么这对我来说更直观.
就个人而言,既然我说的是直观的,它只能是个人的,我发现这更直观,原因如下:
如果我正在编码,BBB那么无论是打电话还是覆盖MyMethod(string),我都会认为这是"做事AAA" - 这是BBB"做事AAA",但它正在做的AAA事情都是一样的.因此无论是调用还是覆盖,我都会意识到这是AAA定义的事实MyMethod(string).
如果我正在调用使用的代码BBB,我会想到"使用BBB东西".我可能不太了解最初定义的内容AAA,我可能认为这只是一个实现细节(如果我没有使用AAA附近的接口).
编译器的行为符合我的直觉,这就是为什么当我第一次阅读这个问题时,我认为Mono有一个bug.经过考虑,我看不出如何比另一个更好地满足指定的行为.
但就此而言,在保持个人层面的同时,我永远不会使用抽象,虚拟或重写方法的可选参数,如果覆盖其他人的那些,我会匹配他们的.
Jon*_*eet 15
对我来说这看起来像个错误.我相信它已被明确指定,并且它应该以与使用显式this前缀调用方法相同的方式运行.
我已经简化了示例,只使用了一个虚拟方法,并显示了调用哪个实现以及参数值是什么:
using System;
class Base
{
public virtual void M(string text = "base-default")
{
Console.WriteLine("Base.M: {0}", text);
}
}
class Derived : Base
{
public override void M(string text = "derived-default")
{
Console.WriteLine("Derived.M: {0}", text);
}
public void RunTests()
{
M(); // Prints Derived.M: base-default
this.M(); // Prints Derived.M: derived-default
base.M(); // Prints Base.M: base-default
}
}
class Test
{
static void Main()
{
Derived d = new Derived();
d.RunTests();
}
}
Run Code Online (Sandbox Code Playgroud)
所以我们需要担心的是RunTests中的三个调用.前两个调用的规范的重要部分是7.5.1.1节,它讨论了在查找相应参数时要使用的参数列表:
对于在类中定义的虚方法和索引器,参数列表是从函数成员的最具体的声明或覆盖中选取的,从接收器的静态类型开始,并搜索其基类.
第7.5.1.2节:
当从具有相应可选参数的函数成员中省略参数时,将隐式传递函数成员声明的默认参数.
"相应的可选参数"是将7.5.2与7.5.1.1联系起来的位.
对于这两个M()和this.M(),即参数列表应该是一个在Derived静态类型的接收机是Derived,事实上,你可以告诉编译器将作为参数列表早些时候编译,因为如果你做参数强制性的Derived.M(),两者的调用失败 - 因此M()调用要求参数具有默认值Derived,但忽略它!
实际上,情况会变得更糟:如果您为参数提供默认值Derived但强制要求Base,则调用
M()最终将使用null作为参数值.如果没有别的,我会说这证明这是一个错误:null价值不能来自任何有效的.(这是null因为它是该string类型的默认值;它总是只使用参数类型的默认值.)
规范的第7.6.8节涉及base.M(),它表示
除了非虚拟行为之外,表达式被视为((Base) this).M(); 所以用于确定有效参数列表的基本方法是完全正确的.这意味着最后一行是正确的.
只是为了让任何想要看到上面描述的奇怪错误的人更容易,其中使用了未在任何地方指定的值:
using System;
class Base
{
public virtual void M(int x)
{
// This isn't called
}
}
class Derived : Base
{
public override void M(int x = 5)
{
Console.WriteLine("Derived.M: {0}", x);
}
public void RunTests()
{
M(); // Prints Derived.M: 0
}
static void Main()
{
new Derived().RunTests();
}
}
Run Code Online (Sandbox Code Playgroud)
bas*_*sti 10
你有没有尝试过:
public override void MyMethod2()
{
this.MyMethod();
}
Run Code Online (Sandbox Code Playgroud)
所以你实际上告诉你的程序使用覆盖方法.
这种行为绝对很奇怪; 我不清楚它是否实际上是编译器中的一个错误,但它可能是.
昨晚校园得到了相当多的积雪,西雅图对雪的处理并不是很好.我的公共汽车今天早上没有运行,所以我不能进入办公室来比较C#4,C#5和Roslyn对这个案子的看法,以及他们是否不同意.一旦我回到办公室并且可以使用适当的调试工具,我将在本周晚些时候尝试发布分析.
可能是由于歧义,编译器优先考虑基类/超类.通过添加对this关键字的引用,下面更改了BBB类的代码,输出'bbb bbb':
class BBB : AAA
{
public override void MyMethod(string s = "bbb")
{
base.MyMethod(s);
}
public override void MyMethod2()
{
this.MyMethod(); //added this keyword here
}
}
Run Code Online (Sandbox Code Playgroud)
它暗示的一点是,您应该始终在this调用当前类实例上的属性或方法时使用关键字作为最佳实践.
如果基础和子方法中的这种歧义甚至没有引发编译器警告(如果不是错误),我会担心,但如果确实如此,那么我认为这是看不见的.
================================================== ================
编辑:请考虑下面这些链接的示例摘录:
陷阱:可选参数值是编译时在使用可选参数时,只记住一件事和一件事.如果你记住这一点,你可能很好理解并避免使用它们的任何潜在缺陷:有一点是这样的:可选参数是编译时,语法糖!
陷阱:谨防继承和接口实现中的默认参数
现在,第二个潜在的陷阱与继承和接口实现有关.我将用一个谜题来说明:
1: public interface ITag
2: {
3: void WriteTag(string tagName = "ITag");
4: }
5:
6: public class BaseTag : ITag
7: {
8: public virtual void WriteTag(string tagName = "BaseTag") { Console.WriteLine(tagName); }
9: }
10:
11: public class SubTag : BaseTag
12: {
13: public override void WriteTag(string tagName = "SubTag") { Console.WriteLine(tagName); }
14: }
15:
16: public static class Program
17: {
18: public static void Main()
19: {
20: SubTag subTag = new SubTag();
21: BaseTag subByBaseTag = subTag;
22: ITag subByInterfaceTag = subTag;
23:
24: // what happens here?
25: subTag.WriteTag();
26: subByBaseTag.WriteTag();
27: subByInterfaceTag.WriteTag();
28: }
29: }
Run Code Online (Sandbox Code Playgroud)
怎么了?好吧,即使每种情况下的对象都是SubTag,其标签是"SubTag",你会得到:
1:SubTag 2:BaseTag 3:ITag
但请记住确保:
不要在现有的一组默认参数的中间插入新的默认参数,这可能会导致不可预测的行为,这可能不一定会引发语法错误 - 添加到列表末尾或创建新方法.如何在继承层次结构和接口中使用默认参数时要非常小心 - 根据预期用法选择最合适的级别来添加默认值.
================================================== ========================
| 归档时间: |
|
| 查看次数: |
19970 次 |
| 最近记录: |