重写方法的C#可选参数

时间:2012-01-18 12:05:51

标签: c# .net override 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();
    }
}

9 个答案:

答案 0 :(得分:34)

您可以通过致电:

来消除歧义
this.MyMethod();

(在MyMethod2()

是否是一个bug很棘手;但它确实看起来不一致。如果有帮助的话,Resharper会警告你根本不要更改覆盖中的默认值; p当然,resharper 告诉你this.是多余的,并且提议删除它你...改变了行为 - 所以resharper也不完美。

看起来可能有资格作为编译器错误,我会授予你。我需要仔细看看真的以确定......当你需要他时,Eric会在哪里,呃?


编辑:

这里的关键点是语言规范;让我们看看§7.5.3:

  

例如,方法调用的候选集不包括标记为override的方法(第7.4节),如果派生类中的任何方法适用(第7.6.5.1节),则基类中的方法不是候选方法。

(事实上§7.4明确省略了override方法的考虑)

这里存在一些冲突....它声明如果派生类中有适用的方法,则不会使用 base 方法 - 这将导致我们派生方法,但与此同时,它表示不考虑标记为override的方法。

但是,§7.5.1.1然后声明:

  

对于在类中定义的虚方法和索引器,参数列表是从函数成员的最具体的声明或覆盖中选取的,从接收者的静态类型开始,并搜索其基类。

然后§7.5.1.2解释了在调用时如何计算值:

  

在函数成员调用(第7.5.4节)的运行时处理期间,参数列表的表达式或变量引用按从左到右的顺序进行评估,如下所示:

     

...(剪断)...

     

当从具有相应可选参数的函数成员中省略参数时,将隐式传递函数成员声明的默认参数。因为它们总是不变的,所以它们的评估不会影响其余参数的评估顺序。

这明确强调它正在查看参数列表,该列表先前在§7.5.1.1中定义为来自最具体的声明或覆盖。这是§7.5.1.2中引用的“方法声明”似乎是合理的,因此传递的值应该来自最多派生的静态类型。

这表明:csc有一个错误,它应该使用派生的版本(“bbb bbb”),除非它受到限制(通过base.,或者转换为基础-type)查看基本方法声明(第7.6.8节)。

答案 1 :(得分:23)

有一点需要注意的是,每次调用被覆盖的版本。将覆盖更改为:

public override void MyMethod(string s = "bbb")
{
  Console.Write("derived: ");
  base.MyMethod(s);
}

输出是:

derived: bbb
derived: aaa

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

  1. 它为其他代码定义了一个接口。
  2. 它定义了一个在被调用时执行的实现。
  3. 它可能不会同时执行这两种操作,因为抽象方法只执行前者。

    BBB内,来电MyMethod()会在AAA中调用定义方法。

    由于BBB中存在覆盖,因此调用该方法会导致BBB中的实现被调用。

    现在,AAA中的定义通知调用代码两件事(嗯,还有一些其他事情在这里没关系)。

    1. 签名void MyMethod(string)
    2. (对于那些支持它的语言),单个参数的默认值为"aaa",因此在编译MyMethod()形式的代码时,如果找不到匹配MyMethod()的方法,您可以通过调用“MyMethod(”aaa“)来替换它。
    3. 那么,这就是BBB中的调用:编译器看到MyMethod()的调用,找不到方法MyMethod()但找到方法MyMethod(string) 。它还会看到在定义它的地方有一个默认值“aaa”,因此在编译时它会将此更改为对MyMethod("aaa")的调用。

      BBB开始,AAA被视为定义AAA方法的地方,即使在BBB中被覆盖,也是可以< / em>被过度使用。

      在运行时,使用参数“aaa”调用MyMethod(string)。因为有一个被覆盖的表单,即被调用的表单,但它不是用“bbb”调用的,因为该值与运行时实现无关,而是与编译时定义无关。

      添加this.更改检查哪个定义,从而更改调用中使用的参数。

      编辑:为什么这对我来说更直观。

      就个人而言,既然我说的是直观的,它只能是个人的,我发现这更直观,原因如下:

      如果我正在编码BBB,那么无论是致电还是覆盖MyMethod(string),我都会将其视为“正在做AAA内容” - BBB采取的措施做AAA东西“,但它正在做AAA个东西。因此无论是调用还是覆盖,我都会意识到AAA定义了MyMethod(string)这一事实。

      如果我调用使用BBB的代码,我会想到“使用BBB内容”。我可能不太了解最初在AAA中定义的内容,我可能认为这只是一个实现细节(如果我没有使用附近的AAA接口)。< / p>

      编译器的行为符合我的直觉,这就是为什么当我第一次阅读这个问题时,我认为Mono有一个bug。经过考虑,我看不出如何比另一个更好地完成指定的行为。

      但就此而言,在保持个人层面的同时,我永远不会使用抽象,虚拟或重写方法的可选参数,如果覆盖其他人,我会匹配他们的。

答案 2 :(得分: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();
    }
}

所以我们需要担心的是RunTests中的三个调用。 前两个调用的规范的重要部分是section 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();
    }
}

答案 3 :(得分:10)

你试过了吗?

 public override void MyMethod2()
    {
        this.MyMethod();
    }

所以你实际上告诉你的程序使用覆盖方法。

答案 4 :(得分:9)

这种行为绝对很奇怪;我不清楚它是否实际上是编译器中的一个错误,但它可能是。

昨晚校园得到了相当多的积雪,而西雅图对雪的处理并不是很好。我的公共汽车今天早上没有运行,所以我不能进入办公室来比较C#4,C#5和Roslyn对这个案子的看法,以及他们是否不同意。一旦我回到办公室并且可以使用适当的调试工具,我将在本周晚些时候尝试发布分析。

答案 5 :(得分:5)

可能是由于歧义,编译器优先考虑基类/超类。以下对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

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

陷阱:谨防继承和接口实施中的默认参数

现在,第二个潜在的陷阱与继承和接口实现有关。我将用一个谜题来说明:

   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

但请记住确认:

不要在现有的一组默认参数的中间插入新的默认参数,这可能会导致不可预测的行为,这可能不一定会引发语法错误 - 添加到列表末尾或创建新方法。 如何在继承层次结构和接口中使用默认参数时要非常小心 - 根据预期用法选择最合适的级别来添加默认值。

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

答案 6 :(得分:1)

我认为这是因为这些默认值在编译时是固定的。如果您使用反射器,您将在BBB中看到MyMethod2的以下内容。

public override void MyMethod2()
{
    this.MyMethod("aaa");
}

答案 7 :(得分:0)

无论哪种方式都需要修复

我肯定会将它视为一个错误,或者因为结果错误或者结果是预期的,那么编译器不应该让你将其声明为“覆盖”,或者至少提供警告。

我建议您将此报告给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. 默认值嵌入在调用者中,因此产生以下代码:

    void myfunc(int optional){ /* Some code here*/ } //Function implementation
    myfunc(5); //Call and embed default arguments
    
  3. 这两种方法之间存在很多差异,但我们首先会看一下.Net框架如何解释它。

    1. 在.Net中,您只能使用包含相同数量参数的方法覆盖方法,但不能使用包含 more 参数的方法覆盖,即使它们都是可选的(这会导致调用与重写方法具有相同的签名),比如你有:

      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. 使用反射时,必须始终提供默认值。

    4. 所有这些都足以证明.Net采取了第二次实施,因此OP看到的行为是正确的,至少根据.Net。

      .Net方法的问题

      然而,.Net方法确实存在问题。

      1. <强>一致性

        • 正如OP在覆盖继承方法中的默认值时的问题一样,结果可能无法预测

        • 如果更改了默认值的原始植入,并且由于调用者不必重新编译,我们最终可能会使用不再有效的默认值

        • 反射要求您提供默认值,调用者不必知道
      2. 打破密码

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

        • 类似的情况会发生,如果我们以后拿走没有参数的函数,那么所有调用都会自动使用默认参数路由到函数,同样没有通知或警告!虽然这可能不是程序员的意图

        • 此外,它不必是常规实例方法,扩展方法将执行相同的问题,因为没有参数的扩展方法将优先于具有默认参数的实例方法!

      3. 摘要:远离可选论点,并使用过时的过载(如同.NET框架本身那样)

答案 8 :(得分:0)

与@Marc Gravell一致同意。

但是,我想提一下,这个问题在C ++世界(http://www.devx.com/tips/Tip/12737)中已经足够老了,答案看起来像“与运行时解析的虚函数不同,默认参数是静态解析的,就是在编译时。“所以这个C#编译器行为由于一致性而被故意接受,尽管看起来很意外。