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

Posted

技术标签:

【中文标题】覆盖方法上的 C# 可选参数【英文标题】:C# optional parameters on overridden methods 【发布时间】:2012-02-13 03:40:44 【问题描述】:

似乎在 .NET Framework 中,当您覆盖该方法时,可选参数存在问题。下面代码的输出是: “bb” “啊” .但我期待的输出是: “bb” “bb” .是否有解决方案。我知道它可以通过方法重载来解决,但想知道这是为什么。代码在 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();
    

【问题讨论】:

可选参数不好,mmkay! TBH,我什至从来没有使用过它们。 @leppie,这是一个非常实用的功能。在所有情况下都拒绝一个功能是不明智的。 这听起来像是 Mono 中的一个错误。 @Jon 怎么样?这里我们有一个例子,其中 outside 使用类,overridden 版本被检查为默认值,inside 类是 base 版本被检查为默认值;这是不一致的 “我没有说'我在所有情况下都拒绝该功能'” - 你当然做到了。 “不知道你是怎么得出这个结论的”——呃,“可选参数不好,嗯!” 【参考方案1】:

您可以通过调用来消除歧义:

this.MyMethod();

(在MyMethod2()

是否是错误很棘手;不过,它看起来确实不一致。 ReSharper 警告您不要更改覆盖中的默认值,如果有帮助的话;p 当然,ReSharper 告诉您this. 是多余的,并提议为您删除它。 .. 这会改变行为 - 所以 ReSharper 也不完美。

它看起来确实可以被认为是一个编译器错误,我同意你。我需要真的仔细查看以确定...当你需要 Eric 时,他在哪里,嗯?


编辑:

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

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

(实际上第 7.4 节显然忽略了 override 方法)

这里有一些冲突......它指出如果派生类中有适用的方法,则不使用 base 方法 - 这将导致我们使用 派生 em> 方法,但同时表示不考虑标记为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,是的,因为它纯粹是关于在编译时处理可选参数,它只在存在可选参数的情况下暴露自己。如果您认为这是一个错误,您能在规范中找到不应该发生这种情况的地方吗?我在上面找不到任何东西。我将编辑我的答案以添加为什么这对我来说似乎更直观的行为。 @JonHanna:我怀疑由于可选参数的编译时间常数不同,运行时无法匹配方法签名。听起来合理吗? @leppie Runtime 总是调用派生的,那部分显然是正确的。有争议的一点是它应该用什么来称呼它。 @Fernando 是的,我们知道;从来没有人建议调用了错误的方法;一切都是关于编译器如何为可选值选择参数值【参考方案2】:

这里值得注意的一点是,每次都会调用被覆盖的版本。将覆盖更改为:

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()形式的代码时,如果找不到匹配MyMethod()的方法,您可以将其替换为调用 `MyMethod("aaa")。

所以,这就是BBB 中的调用所做的:编译器看到对MyMethod() 的调用,没有找到方法MyMethod(),但确实找到了方法MyMethod(string)。它还看到在定义它的地方有一个默认值“aaa”,因此在编译时它会将其更改为对MyMethod("aaa")的调用。

BBB 内部,AAA 被认为是定义AAA 的方法的地方,即使在BBB 中被覆盖,所以它们可以被覆盖。

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

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

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

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

如果我正在编写BBB 编码,那么无论是调用还是覆盖MyMethod(string),我都会认为这是“在做AAA 的事情”——它是BBBs 承担“做AAA 的事情”,但是它在做AAA 的东西都是一样的。因此,无论是调用还是覆盖,我都会意识到是 AAA 定义了 MyMethod(string)

如果我调用使用BBB 的代码,我会想到“使用BBB 的东西”。我可能不太清楚 AAA 中最初定义的是哪个,我可能会认为这仅仅是一个实现细节(如果我没有在附近使用 AAA 接口)。

编译器的行为符合我的直觉,这就是为什么当我第一次阅读这个问题时,我觉得 Mono 有一个错误。经过考虑,我看不出哪一个比另一个更好地满足指定的行为。

尽管如此,在保持个人层面的同时,我永远不会将可选参数与抽象、虚拟或重写方法一起使用,如果重写其他人的方法,我会匹配他们的。

【讨论】:

+1 良好的观察力。当两个默认值相同时会发生什么? @leppie 当两个默认值相同时,差异就变得微不足道了,因为两者的结果相同,而且语法糖的味道与您期望的一样甜。我注意到至少有一个工具有此建议。就个人而言,我会完全远离虚拟的默认设置。 我添加了一个引用规范的答案......想法? 我仍然认为您的发现几乎表明这是一个真正的错误。 您对“这个”的看法。改变解决的方法是……不明显。请提供规范参考以支持此论点。它不符合 7.5.1.1 和 7.5.1.2 中所述的规范。【参考方案3】:

这对我来说似乎是一个错误。我相信它明确指定的, 并且它的行为方式应该与您调用 带有显式 this 前缀的方法。

我已将示例简化为仅使用 single 虚拟 方法,并显示调用了哪个实现和 参数值是什么:

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()中的参数强制both 调用失败 - 所以 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();
    

【讨论】:

重新“详细说明” - 你能看看我对第 7.5.1.2 节中的措辞的回答所做的微小修改 - 看起来有点模棱两可......? @MarcGravell:我不这么认为——关于“缺失”参数的部分特别指出该值取自 对应参数,这是一个7.5.1.1 中规定。因此,函数成员是基声明,但在覆盖中指定了此调用的相应参数列表。至少,我认为这是一种合理的阅读方式。 啊是的;假设“方法声明”是用于参数列表(7.5.1.1)的声明,而不是用于重载解析(7.4)的声明,也许是正确的。无论哪种方式,有/无this. 的行为,以及您强调的必需/可选的奇怪之处:都非常奇怪且看起来非常破碎。 这个可能的错误的含义对我来说是模糊的。我想获得您对“最佳实践”(针对不太高级的开发人员)的一般建议,以避免该领域的任何潜在陷阱。我在这里发布了一个与此有关的问题:***.com/questions/9381850/…。你能回复这个问题吗? @kmote:我相信 Eric Lippert 已经针对可选参数提出了一系列建议 - 请参阅他的博客。我不能说我已经使用了足够多的方法来发现所有相关的陷阱。【参考方案4】:

你试过了吗:

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

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

【讨论】:

this 是隐含的。因此,您的回答是多余的。 @leppie 它确实改变了结果,尽管 ;p 使用 this 关键字必须是最佳实践 @Jon 它调用 的版本是独立的,因为它是方法上的 callvirt;但是,问题在于默认值的来源;目前,在 MyMethod2 中,它正在从基本版本获取 default ;与 Main 的用法相反,其中 default(用于 MyMethod)来自 derived 版本 @MarcGravell:那么这是一个巨大的错误!【参考方案5】:

这种行为肯定很奇怪;我不清楚它是否真的是编译器中的一个错误,但它可能是。

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

【讨论】:

借口,借口,借口:p(开个玩笑)期待结果。 ……他一边说一边穿上滑雪鞋,向山上走去。 关于这个问题的讨论非常有趣.. :) 看起来特别奇怪的是 this.MyMethod() 和简单的 MyMethod() 之间的区别。祝你下雪好运。 平!你在博客上写过这个,我错过了吗?无论如何,从这个答案指向您的分析的链接会非常好。【参考方案6】:

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

但请记住确保您:

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

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

【讨论】:

虽然您当然可以选择自己的指南和最佳实践,但我宁愿不要在我的代码中乱扔(否则)不必要的关键字(尽管这种特殊情况也让我感到不安)。 @Christian.KI 宁愿告诉自己、其他人(阅读代码)和编译器始终使用哪种方法/属性,而不是让任何人思考和选择,这确实会产生开销,现在模棱两可.. 将冗余代码称为“最佳实践”是相当可疑的。你不需要告诉编译器(或阅读器)你在哪个对象上调用方法,你已经正在这样做,因为this是隐含的(别介意这个怪癖)。跨度> @KonradRudolph 我认为您有时需要冗余以使您的代码更具可读性和易于理解..但不确定,我仍在学习.. 我发现频繁使用this. 的额外冗长使代码的可读性降低并且更难理解。一些工具已经认识到在覆盖上更改可选参数不是最佳实践,这似乎是禁止的更明智的事情(实际上,抽象、虚拟或覆盖方法上的可选参数对我来说似乎是不明智的)。【参考方案7】:

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

public override void MyMethod2()

    this.MyMethod("aaa");

【讨论】:

是的,它们在编译时是固定的。问题是,为什么编译器不考虑自己类的重载,而是使用基类。 我不认为这是有问题的;相反,问题是为什么编译器选择了方法组的那一部分,因此选择了默认值?说“它选择基组”是不够的,因为这与显示的 first 用法相冲突,其中打印了“bbb”。 @MarcGravell 请原谅我,但这是对我的回应的最后评论。如果是这样,我不太明白:-) @Christian.K 不,是给 user6130 我同意,这个观察应该是评论而不是答案【参考方案8】:

总体上同意@Marc Gravell。

但是,我想提一下,这个问题在 C++ 世界中已经足够老了 (http://www.devx.com/tips/Tip/12737),答案看起来像“与在运行时解析的虚函数不同,默认参数是静态解析的,即是,在编译时。”因此,出于一致性考虑,这种 C# 编译器行为宁可被故意接受,尽管它似乎出乎意料。

【讨论】:

【参考方案9】:

无论哪种方式都需要修复

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

我建议您将此报告给 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
    

    默认值嵌入在调用者中,从而产生如下代码:

    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!
    

    使用反射时,必须始终提供默认值。

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

.Net 方法的问题

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

    一致性

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

    当默认值的原始植入发生变化时,由于调用者不必重新编译,我们最终可能会得到不再有效的默认值

    反射要求您提供默认值,调用者不必知道该值

    破解密码

    1234563

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

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

总结:远离可选参数,并使用而不是重载(就像 .NET 框架本身所做的那样)

【讨论】:

以上是关于覆盖方法上的 C# 可选参数的主要内容,如果未能解决你的问题,请参考以下文章

在 C# 4.0 中是不是应该使用重载或可选参数声明方法?

C#基础 可选参数调用params无参静态构造函数

C# 4.0,可选参数和参数不能一起工作

设置 C# 可选参数的默认值

《C#零基础入门之百识百例》(三十四)方法参数 -- 可选参数和形参数组 -- N数的最大公约数

C#中可选参数和具名参数的使用