重写方法的C#可选参数

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读完7.5.1.1后我提议单声道是对的 (2认同)

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)

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

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

它可能不会同时做到这两点,因为抽象方法只做前者.

BBB调用MyMethod()调用方法定义AAA.

因为有一个覆盖BBB,调用该方法会导致调用实现BBB.

现在,定义AAA通知调用两个东西的代码(好吧,其他几个也没关系).

  1. 签名void MyMethod(string).
  2. (对于那些支持它的语言),单个参数的默认值是"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.经过考虑,我看不出如何比另一个更好地满足指定的行为.

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

  • @leppie当两个默认值相同时,差异变得微不足道,因为它们具有相同的结果,并且语法糖的味道与您期望的一样甜.我注意到至少有一个工具有这个作为推荐.就个人而言,我完全远离虚拟默认值. (2认同)

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)

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

  • @leppie它确实改变了结果; p (5认同)
  • @MarcGravell:那是一个巨大的错误! (3认同)
  • 使用this关键字必须是<always>的最佳做法 (2认同)

Eri*_*ert 9

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

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

  • 你有没有设法在这上面找到任何东西? (7认同)
  • 平安!你有关于此的博客,我错过了吗?无论如何,从这个答案中得到分析的链接会非常好. (2认同)

VS1*_*VS1 5

可能是由于歧义,编译器优先考虑基类/超类.通过添加对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调用当前类实例上的属性或方法时使用关键字作为最佳实践.

如果基础和子方法中的这种歧义甚至没有引发编译器警告(如果不是错误),我会担心,但如果确实如此,那么我认为这是看不见的.

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

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

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: } 
Run Code Online (Sandbox Code Playgroud)

怎么了?好吧,即使每种情况下的对象都是SubTag,其标签是"SubTag",你会得到:

1:SubTag 2:BaseTag 3:ITag

但请记住确保:

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

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

  • 将冗余代码称为"最佳实践"是相当可疑的.您不需要告诉编译器(或读者)您正在调用该方法的对象,您已经*正在执行此操作,因为"this"是隐含的(更别提这个怪癖). (2认同)