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 

类中的方法可以执行以下一个或两个操作:

  1. 它为其他代码调用定义了一个接口。
  2. 它定义了一个被调用时执行的实现。

它可能不会这样做,因为抽象的方法只有前者。

BBB内调用MyMethod()调用AAA 定义的方法。

因为在BBB有一个覆盖,所以调用该方法会导致在BBB被调用的实现。

现在, AAA的定义通知调用两个东西的代码(以及其他一些在这里不重要的东西)。

  1. 签名void MyMethod(string)
  2. (对于那些支持它的语言),单个参数的默认值是"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值不能从任何地方有效。 (由于这是stringtypes的默认值,因此它是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关键字作为最佳实践

我会担心,如果基地和儿童方法的这种模糊性甚至没有提出编译器的警告(如果不是错误的话),但如果这样做是不可见的,我想。

================================================== ================

编辑:考虑下面这些链接的示例摘录:

http://geekswithblogs.net/BlackRabbitCoder/archive/2011/07/28/c.net-little-pitfalls-default-parameters-are-compile-time-substitutions.aspx

http://geekswithblogs.net/BlackRabbitCoder/archive/2010/06/17/c-optional-parameters—pros-and-pitfalls.aspx

陷阱:可选参数值是编译时使用可选参数时,只记住一件事和一件事。 如果你牢记这一点,你可能会很好地理解和避免任何潜在的使用陷阱:一个是这样的:可选参数是编译时,语法糖!

陷阱:小心在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 

有两种方式来实现它:

  1. 该可选参数被视为重载函数,导致以下结果:

     void myfunc(int optional){ /* Some code here*/ } //Function implementation void myfunc(){ myfunc(5); } //Default arguments implementation myfunc(); //Call using the default arguments 
  2. 默认值被embedded到调用者中,从而产生下面的代码:

     void myfunc(int optional){ /* Some code here*/ } //Function implementation myfunc(5); //Call and embed default arguments 

这两种方法之间有很多不同之处,但我们首先会看看.Net框架如何解释它。

  1. 在.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)} 
  2. 你可以使用另一个没有参数的方法来重载一个默认参数的方法(这有一个灾难性的影响,我将在下面讨论),所以下面的代码是合法的:

     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! 
  3. 当使用reflection时,必须始终提供默认值。

所有这些都足以certificate.Net采取了第二次实现,所以OP看到的行为是正确的,至less根据.Net。

.Net方法的问题

但是.Net方法存在真正的问题。

  1. 一致性

    • 就像在OP的问题中覆盖inheritance的方法中的默认值一样,结果可能是不可预知的

    • 当初始植入的默认值被改变,并且由于调用者不必重新编译,我们可能以不再有效的默认值

    • reflection需要您提供默认值,调用者不必知道
  2. 打破代码

    • 当我们有一个带默认参数的函数,而后者我们添加一个没有参数的函数时,所有的调用现在都会路由到新的函数,从而打破所有现有的代码,没有任何通知或警告!

    • 类似的情况会发生,如果我们稍后拿掉没有参数的函数,那么所有的调用都会自动路由到带有默认参数的函数,而没有通知或警告! 尽pipe这可能不是程序员的意图

    • 此外,它不一定是常规实例方法,扩展方法也会遇到同样的问题,因为没有参数的扩展方法将优先于具有默认参数的实例方法!

总结:远离可选的自variables,并使用临时过载(因为.NET框架本身)