覆盖方法上的C#可选参数


74

似乎在.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();
    }
}

6
可选参数不好,mmkay!TBH,我什至从未使用过它们。
leppie 2012年

40
@leppie,这是非常实用的功能。在所有情况下拒绝功能都是不明智的。
gdoron支持Monica 2012年

1
我很好奇是否也发生在VB.Net中,并且不是特定于C#编译器..将需要检查..
VSS

2
这听起来像是Mono中的错误。
乔恩·汉娜

4
“我没有说过'我在所有情况下都拒绝使用该功能'”-当然,您同意了。“不确定您是如何得出这个结论的”-嗯,“可选参数不好,嗯!”
Jim Balter

Answers:


25

这里需要注意的一件事是,每次都会调用覆盖的版本。将替代更改为:

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()如果找不到方法匹配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有一个错误。经考虑,我看不到任何一个如何比另一个更好地实现指定的行为。

尽管如此,尽管如此,在保持个人水平的同时,我绝不会将可选参数与抽象,虚拟或重写方法一起使用,并且如果要覆盖其他人使用的参数,我会匹配它们。


+1好观察。当两个默认值相同时会发生什么?
leppie 2012年

2
@leppie当两个默认值都相同时,差异就可以忽略不计,因为它们中的任一个具有相同的结果,并且语法糖的味道与您期望的一样甜。我注意到,至少有一种工具建议这样做。就个人而言,我将完全不使用虚拟的默认设置。
乔恩·汉娜

我添加了一个参考规范的答案...想法?
Marc Gravell

1
我仍然认为您的发现几乎表明这是一个真正的错误。
leppie 2012年

1
您对“这个”的想法。改变解决的方法……不是很明显。请提供规格参考以支持此参数。它与7.5.1.1和7.5.1.2中规定的规范不同。
马克·格雷夫

37

您可以通过以下方式消除歧义:

this.MyMethod();

(在中MyMethod2()

它是否是一个漏洞是棘手的。它看起来确实不一致。Resharper会警告您根本不要在覆盖中更改默认值,如果这样做会有所帮助; p当然,resharper还会告诉您this.多余,并愿意为您删除它……这会改变行为-因此,resharper也并不完美。

看起来确实资格成为编译器错误,我将授予您。我需要非常仔细地检查以确保...当您需要他时,埃里克在哪里,是吗?


编辑:

这里的重点是语言规范;让我们看看第7.5.3节:

例如,方法调用的候选集不包括标记为重写的方法(第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有一个错误,并且应该使用派生版本(“ bbb bbb”),除非它被限制(通过base.或强制转换为基本类型)查看基本方法声明(第7.6.8节)。 )。


顺便说一句,这种行为是否仅在存在可选参数的情况下才暴露出来?
leppie 2012年

@leppie,是的,因为它纯粹是关于编译时对可选参数的处理,所以它仅在存在可选参数的情况下才公开。如果您认为这是一个错误,那么您可以在规范中找到某处不应该发生的错误吗?无论如何我都找不到任何东西。我将编辑答案,以添加为什么这对我来说似乎更直观。
乔恩·汉娜

@JonHanna:我怀疑运行时无法匹配方法签名,因为可选参数的编译时常数不同。听起来合理吗?
leppie 2012年

@leppie Runtime总是调用派生的,那部分显然是正确的。值得商over的是它应该如何称呼它。
乔恩·汉娜

@leppie-添加了规格参考;有什么想法吗?
Marc Gravell

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中的三个调用。前两个调用的规范的重要部分是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类型的默认值;它始终只将默认值用于参数类型。)

与base.M(),它表示,该规范交易部分7.6.8 ,以及作为非虚拟行为,表达被视为((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();
    }
}

关于“明确规定”-您能看一下我对答案所做的微小修改,还是第7.5.1.2节中的措词-看起来确实有点am昧...?
Marc Gravell

@MarcGravell:我不这么认为-谈到“缺失”参数的那一部分专门说,该值取自7.5.1.1中指定的相应参数。因此,函数成员是基本声明,但是此调用的相应参数列表在覆盖中指定。至少,我认为这是一种合理的阅读方式。
乔恩·斯基特

是的; 假设“方法声明”是用于参数列表(7.5.1.1)的声明,而不是用于重载解析的声明(7.4),也许是正确的。无论哪种方式,with / with的行为this.,以及您突出显示的必需/可选的奇数:都是严重的奇数并且看起来很破损。
Marc Gravell

这个可能的错误的含义对我来说是模糊的。我想为您提供“最佳实践”的一般建议(针对不太先进的开发人员),以避免在此领域出现任何潜在的陷阱。我在这里发布了一个有关此问题的问题: stackoverflow.com/questions/9381850/…。您愿意对这个问题发表答复吗?
kmote 2012年

@kmote:我相信埃里克·利珀特(Eric Lippert)已经针对可选参数提出了一系列建议-请参阅他的博客。我不能说我已经足够使用它们来发现所有相关的陷阱。
乔恩·斯基特

10

你有没有尝试过:

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

因此,您实际上告诉程序使用重写的方法。


this是暗含的。因此,您的答案是多余的。
leppie 2012年

如果是这个原因,那么我们确实有bug,因为它是默认的不带base
Christian.K

5
@leppie它确实确实会改变结果,但是; p
Marc Gravell

2
使用this关键字必须是最佳实践<always>
VSS

3
@MarcGravell:那是一个巨大的错误!
leppie 2012年

9

这种行为肯定很奇怪。我不清楚这是否实际上是编译器中的错误,但可能是。

昨晚校园里下了很多雪,西雅图在处理雪方面不是很好。我的公交车今天早上没有开动,所以我无法去办公室比较C#4,C#5和Roslyn关于这种情况的说法以及他们是否不同意。回到办公室后,我将在本周晚些时候尝试发布分析报告,并可以使用适当的调试工具。


借口,借口,借口:p(开个玩笑)期待结果。
leppie 2012年

……他穿上滑雪板靴前往山上时说道。
Fernando

关于这个问题非常有趣的讨论.. :)
VSS

1
看起来特别奇怪的是this.MyMethod()和MyMethod()之间的区别。祝你好运与雪。
马克·格雷夫

2
平安!您是否为此写过博客,但我错过了?无论如何,从这个答案到您的分析的链接将非常好。
Ben Voigt 2012年

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:子标签2:基本标签3:ITag

但是请记住确保您:

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

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


1
尽管您当然可以选择自己的准则和最佳实践,但是我不希望使用(否则)不必要的关键字来填充我的代码(尽管这种特殊情况也使我感到不安)。
Christian.K

@ Christian.KI宁愿告诉自己,他人(读取的代码)和方法/总是被使用属性,而不是让任何人去思考和挑编译器,并创造一个架空现在的不确定性..
VSS

2
将冗余代码称为“最佳实践”是相当容易的。您不需要告诉编译器(或阅读器)您要在哪个对象上调用该方法,因为已经暗示了,您已经执行此操作了this(不要介意这个怪癖)。
康拉德·鲁道夫2012年

@KonradRudolph我认为您有时需要冗余以使您的代码更具可读性和易于理解..但不确定,我仍然在学习..
VSS 2012年

1
我发现频繁使用的额外冗长性this.使代码的可读性和理解度降低。某些工具已经认识到,更改覆盖上的可选参数并不是最佳实践,这似乎是一个更明智的选择(实际上,抽象,虚拟或覆盖方法上的可选参数对我来说似乎是不明智的)。
乔恩·汉娜

1

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

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

是的,它们在编译时已固定。问题是,为什么编译器不考虑自己类的重载,而是使用基类。
Christian.K

我不认为这是有问题的。问题是,为什么编译器会选择方法组的那一部分,并因此选择默认值?仅仅说“选择基组”是不够的,因为这与显示“ bbb”的第一个用法冲突。
Marc Gravell

@MarcGravell原谅我,但这是对我的最后回应。如果是这样,我不是很明白:-)
Christian.K

@ Christian.K不,是user6130
Marc Gravell

我同意,这一观察本来应该是评论,而不是答案
chandmk 2012年


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
    

两种方法之间有很多区别,但是我们首先来看看.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. 使用反射时,必须始终提供默认值。

所有这些都足以证明.Net采用了第二种实现,因此,至少根据.Net而言,OP所看到的行为是正确的。

.Net方法的问题

但是,.Net方法确实存在问题。

  1. 一致性

    • 如在OP的问题中,当在继承的方法中覆盖默认值时,结果可能是不可预测的

    • 当更改默认值的原始植入时,并且由于不必重新编译调用方,我们可能会得到不再有效的默认值

    • 反射要求您提供调用者不必知道的默认值
  2. 破解代码

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

    • 类似的情况将会发生,如果我们以后不带参数的函数被删除,那么所有调用将自动路由到带默认参数的函数,再次没有通知或警告!尽管这可能不是程序员的意图

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

摘要:远离可选参数,并使用间接超载(如.NET框架本身那样)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.