C#可选参数在重写的方法
在.NET Framework中看起来像覆盖该方法时可选参数存在问题。 下面的代码的输出是:“bbb”“aaa”。 但是我期望的输出是:“bbb”“bbb”。是否有解决scheme。 我知道它可以解决方法重载,但不知道这个原因。 此外代码工作正常在单声道。
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(); } }
有一点值得注意的是,每次调用被覆盖的版本。 将覆盖更改为:
public override void MyMethod(string s = "bbb") { Console.Write("derived: "); base.MyMethod(s); }
输出是:
derived: bbb derived: aaa
类中的方法可以执行以下一个或两个操作:
- 它为其他代码调用定义了一个接口。
- 它定义了一个被调用时执行的实现。
它可能不会这样做,因为抽象的方法只有前者。
在BBB
内调用MyMethod()
调用AAA
定义的方法。
因为在BBB
有一个覆盖,所以调用该方法会导致在BBB
被调用的实现。
现在, AAA
的定义通知调用两个东西的代码(以及其他一些在这里不重要的东西)。
- 签名
void MyMethod(string)
。 - (对于那些支持它的语言),单个参数的默认值是
"aaa"
,因此,当编译MyMethod()
forms的代码时,如果找不到匹配MyMethod()
方法,可以用`的MyMethod( “AAA”)。
所以,这就是BBB
所做的调用:编译器看到对MyMethod()
的调用,没有find方法MyMethod()
但find方法MyMethod(string)
。 它也看到,在它被定义的地方有一个默认值“aaa”,所以在编译时它将它改变为对MyMethod("aaa")
的调用。
从BBB
内部看, AAA
被认为是定义了AAA
的方法的地方,即使在BBB
被覆盖,以至于它们可以被覆盖。
在运行时,用参数“aaa”调用MyMethod(string)
。 因为有一个被覆盖的窗体,就是被调用的窗体,但是它并没有用“bbb”来调用,因为这个值与运行时实现无关,而是与编译时定义无关。
添加this.
改变哪个定义被检查,并且改变在调用中使用什么参数。
编辑:为什么这对我来说似乎更直观。
就个人而言,由于我谈论的是直觉,它只能是个人的,我觉得这更直观,原因如下:
如果我是编码BBB
那么无论是调用还是覆盖MyMethod(string)
,我都会认为这是“做AAA
东西” – BBB
采取“做AAA
东西”,但它同样做AAA
东西。 因此,无论是调用还是覆盖,我都会意识到定义MyMethod(string)
是AAA
。
如果我正在调用使用BBB
代码,我会想到“使用BBB
东西”。 我可能不知道最初在AAA
定义了哪一个,我可能会认为这只是一个实现细节(如果我不使用附近的AAA
接口)。
编译器的行为与我的直觉相符,这就是为什么当我第一次看到Mono出现问题时,我觉得这个问题。 经考虑后,我看不出如何比其他人更好地履行特定的行为。
对于这个问题,尽pipe在个人层面上,我绝不会使用抽象,虚拟或重写方法的可选参数,如果压倒其他人,我会匹配他们的。
你可以通过调用来消除歧义:
this.MyMethod();
(在MyMethod2()
)
无论是一个错误是棘手的, 它看起来不一致,但。 如果有帮助的话,Resharper警告你不要改变override的默认值; p当然,resharper 也会告诉你this.
是多余的,并提供给你删除它…这改变了行为 – 所以resharper也不完美。
它看起来像它可以作为一个编译器bug的资格,我会授予你。 我需要仔细看看,确定…当你需要他的时候,Eric在哪里?
编辑:
这里的关键是语言规范; 让我们看看§7.5.3:
例如,方法调用的候选集合不包括标记为override的方法(第7.4节),并且如果派生类中的任何方法适用(第7.6.5.1节),则基类中的方法不是候选方法。
(事实上§7.4显然忽略了override
方法的考虑)
这里有一些冲突……它指出,如果在派生类中有适用的方法,则不会使用基本方法 – 这会将我们导向派生方法,但是同时它表示方法标记为override
不考虑。
但是,§7.5.1.1则指出:
对于在类中定义的虚方法和索引器,参数列表是从最具体的声明或函数成员的覆盖中挑选出来的,从接收器的静态types开始,并search其基类。
然后第7.5.1.2节解释了在调用时如何评估这些值:
在函数成员调用(第7.5.4节)的运行时处理过程中,参数列表的expression式或variables引用按从左到右的顺序进行计算,如下所示:
……(中略)…
当从具有相应可选参数的函数成员中省略参数时,函数成员声明的缺省参数将被隐式传递。 因为这些总是不变的,所以他们的评估不会影响其余论点的评估顺序。
这明确强调了它正在查看参数列表,该列表先前在§7.5.1.1中定义为来自最具体的声明或覆盖 。 这似乎是合理的,这是在§7.5.1.2中提到的“方法声明”,因此,传递的值应该是从最高派生到静态types。
这将提示:csc有一个bug,除非受到限制(通过base.
或者转换为基types)来查看基方法声明,否则应该使用派生版本(“bbb bbb”)(§7.6 0.8)。
这看起来像是一个bug。 我相信这是明确的,它应该以同样的方式performance,就像你用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(); } }
所以我们需要担心的是RunTest中的三个调用。 前两次调用的规范的重要部分是7.5.1.1节,它讨论了查找相应参数时要使用的参数列表:
对于在类中定义的虚方法和索引器,参数列表是从最具体的声明或函数成员的覆盖中挑选出来的,从接收器的静态types开始,并search其基类。
和7.5.1.2节:
当从具有相应的可选参数的函数成员中省略参数时,函数成员声明的默认参数将被隐式传递。
“相应的可选参数”是7.5.2到7.5.1.1之间的位。
对于M()
和this.M()
,这个参数列表应该是Derived
的一个,因为接收者的静态types是Derived
。事实上,你可以告诉编译器把它作为编译之前的参数列表,如如果在Derived.M()
强制使用该参数,则两个调用都会失败 – 所以M()
调用要求参数在Derived
具有默认值,但是忽略它!
事实上,情况会变得更糟:如果您在Derived
为参数提供默认值,但在Base
中将其设为必需,则调用M()
以null
作为参数值。 如果没有别的,我会说,这certificate这是一个错误: null
值不能从任何地方有效。 (由于这是string
types的默认值,因此它是null
;它总是使用参数types的默认值。
该规范的第7.6.8节涉及base.M(),它表示和非虚拟行为一样,该expression式被认为是((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(); } }
你有没有尝试过:
public override void MyMethod2() { this.MyMethod(); }
所以你实际上告诉你的程序使用overriden方法。
这个行为绝对是很奇怪的; 我不清楚它是否是编译器中的一个错误,但可能是这样。
校园昨天晚上得到了相当数量的积雪,而西雅图在处理积雪方面也不太好。 今天早上我的公共汽车没有运行,所以我不能进入办公室去比较一下C#4,C#5和Roslyn对这种情况的说法,如果他们不同意的话。 一旦我回到办公室,我会尝试在本周晚些时候发布分析,并且可以使用适当的debugging工具。
可能这是由于含糊不清,编译器优先考虑base / super类。 以下更改为您的BBB类的代码并添加this
关键字的引用,给出输出“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 } }
其中暗示的一个问题是,无论何时调用当前类实例的属性或方法,都应该始终使用this
关键字作为最佳实践 。
我会担心,如果基地和儿童方法的这种模糊性甚至没有提出编译器的警告(如果不是错误的话),但如果这样做是不可见的,我想。
================================================== ================
编辑:考虑下面这些链接的示例摘录:
陷阱:可选参数值是编译时使用可选参数时,只记住一件事和一件事。 如果你牢记这一点,你可能会很好地理解和避免任何潜在的使用陷阱:一个是这样的:可选参数是编译时,语法糖!
陷阱:小心在inheritance和接口实现中的默认参数
现在,第二个潜在的陷阱就是inheritance和接口实现。 我会用一个难题来说明:
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: }
怎么了? 那么,即使在每种情况下的对象是SubTag的标签是“SubTag”,你会得到:
1:SubTag 2:BaseTag 3:ITag
但请记住确保:
不要在现有的一组默认参数的中间插入新的默认参数,这可能会导致不可预知的行为,可能不一定会引发语法错误 – 添加到列表的末尾或创build新的方法。 要非常小心如何在inheritance层次结构和接口中使用默认参数 – 根据预期用途select最合适的级别来添加默认值。
================================================== ========================
我认为这是因为这些默认值在编译时是固定的。 如果您使用reflection器,您将在BBB中看到MyMethod2的以下内容。
public override void MyMethod2() { this.MyMethod("aaa"); }
一般同意@Marc Gravell。
不过,我想提一下,在C ++世界里这个问题已经够老了( http://www.devx.com/tips/Tip/12737 ),答案看起来像“不像虚函数,在运行时解决时间,默认参数是静态parsing,也就是在编译时。 所以这个C#编译器的行为,由于一致性而被有意识地接受了,尽pipe它出乎意料。
无论哪种方式需要一个修复
我肯定会把它当成一个bug,要么是因为结果是错误的,要么是结果是预期的,那么编译器不应该让你声明它为“覆盖”,或者至less提供一个警告。
我build议你把这个报告给Microsoft.Connect
但是对还是错?
但是,关于这是否是预期的行为,我们先来分析一下这两个观点。
考虑我们有以下代码:
void myfunc(int optional = 5){ /* Some code here*/ } //Function implementation myfunc(); //Call using the default arguments
有两种方式来实现它:
-
该可选参数被视为重载函数,导致以下结果:
void myfunc(int optional){ /* Some code here*/ } //Function implementation void myfunc(){ myfunc(5); } //Default arguments implementation myfunc(); //Call using the default arguments
-
默认值被embedded到调用者中,从而产生下面的代码:
void myfunc(int optional){ /* Some code here*/ } //Function implementation myfunc(5); //Call and embed default arguments
这两种方法之间有很多不同之处,但我们首先会看看.Net框架如何解释它。
-
在.Net中,只能使用包含相同数量参数的方法覆盖方法,但不能使用包含更多参数的方法进行覆盖,即使它们全部是可选的(这会导致具有与重写的方法),例如说你有:
class bassClass{ public virtual void someMethod()} class subClass :bassClass{ public override void someMethod()} //Legal //The following is illegal, although it would be called as someMethod(); //class subClass:bassClass{ public override void someMethod(int optional = 5)}
-
你可以使用另一个没有参数的方法来重载一个默认参数的方法(这有一个灾难性的影响,我将在下面讨论),所以下面的代码是合法的:
void myfunc(int optional = 5){ /* Some code here*/ } //Function with default void myfunc(){ /* Some code here*/ } //No arguments myfunc(); //Call which one?, the one with no arguments!
-
当使用reflection时,必须始终提供默认值。
所有这些都足以certificate.Net采取了第二次实现,所以OP看到的行为是正确的,至less根据.Net。
.Net方法的问题
但是.Net方法存在真正的问题。
-
一致性
-
就像在OP的问题中覆盖inheritance的方法中的默认值一样,结果可能是不可预知的
-
当初始植入的默认值被改变,并且由于调用者不必重新编译,我们可能以不再有效的默认值
- reflection需要您提供默认值,调用者不必知道
-
-
打破代码
-
当我们有一个带默认参数的函数,而后者我们添加一个没有参数的函数时,所有的调用现在都会路由到新的函数,从而打破所有现有的代码,没有任何通知或警告!
-
类似的情况会发生,如果我们稍后拿掉没有参数的函数,那么所有的调用都会自动路由到带有默认参数的函数,而没有通知或警告! 尽pipe这可能不是程序员的意图
-
此外,它不一定是常规实例方法,扩展方法也会遇到同样的问题,因为没有参数的扩展方法将优先于具有默认参数的实例方法!
-
总结:远离可选的自variables,并使用临时过载(因为.NET框架本身)