覆盖方法上的 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
的事情”——它是BBB
s 承担“做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# 可选参数的主要内容,如果未能解决你的问题,请参考以下文章