重写方法上的 C# 可选参数
本文关键字:参数 方法 重写 | 更新日期: 2023-09-27 18:20:52
似乎在.NET Framework中,当您重写该方法时,可选参数存在问题。以下代码的输出为:"咔嚓"啊".但我期望的输出是:"咔嚓"咔嚓".有没有解决方案。我知道它可以通过方法重载来解决,但想知道其中的原因。此外,代码在单声道中运行良好。
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();
}
}
您可以通过调用以下内容来消除歧义:
this.MyMethod();
(MyMethod2()
(
它是否是一个错误是棘手的;不过,它看起来确实不一致。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 有一个错误,它应该使用派生版本("bbb bbb"(,除非它被限制(通过 base.
或强制转换为基类型(查看基方法声明 (§7.6.8(。
这里需要注意的一件事是,每次都会调用被覆盖的版本。将覆盖更改为:
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
对"做AAA
的事情"的看法,但它在做AAA
事情都是一样的。因此,无论是调用还是覆盖,我都会意识到定义MyMethod(string)
的是AAA
。
如果我调用使用BBB
的代码,我会想到"使用BBB
的东西"。我可能不太清楚最初在AAA
中定义了哪个,我可能会认为这只是一个实现细节(如果我没有使用附近的AAA
接口(。
编译器的行为符合我的直觉,这就是为什么当第一次阅读问题时,在我看来 Mono 有一个错误。经过考虑,我看不出它们如何比另一个更好地满足指定的行为。
就此而言,在保持个人水平的同时,我永远不会将可选参数与抽象、虚拟或覆盖的方法一起使用,如果覆盖其他人的参数,我会匹配他们的参数。
对我来说看起来像一个错误。我相信它很明确,并且它的行为方式应该与您调用具有显式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();
}
}
因此,我们只需要担心 RunTest 中的三个调用。规范中前两个调用的重要部分是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 节涉及基础。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();
}
}
你试过吗:
public override void MyMethod2()
{
this.MyMethod();
}
因此,您实际上告诉您的程序使用重写的方法。
这种行为肯定非常奇怪;我不清楚它是否实际上是编译器中的错误,但可能是。
昨晚校园下了相当多的雪,西雅图不太擅长处理雪。我的公共汽车今天早上没有运行,所以我无法进入办公室比较C#4,C#5和Roslyn对此案例的看法以及他们是否不同意。一旦我回到办公室,我将尝试在本周晚些时候发布分析,并且可以使用适当的调试工具。
这可能是由于歧义,编译器优先考虑基/超类。下面对类 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
关键字。
如果 base 和 child 方法中的这种歧义甚至没有引发编译器警告(如果不是错误(,我会担心,但如果它确实如此,那么我想那是看不见的。
====
======================================================================编辑:考虑以下来自这些链接的示例摘录:
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: }
会发生什么? 好吧,即使每种情况下的对象都是子标签,其标签是"子标签",你也会得到:
1:子标签 2:基本标签 3: 伊塔格
但请记住确保您:
不要在现有默认参数集的中间插入新的默认参数,这可能会导致不可预测的行为,这些行为不一定会引发语法错误 - 添加到列表末尾或创建新方法。在继承层次结构和接口中使用默认参数时要格外小心 – 选择最合适的级别以根据预期使用情况添加默认值。
===
这是因为这些默认值在编译时是固定的。 如果您使用反射器,您将在BBB中看到MyMethod2的以下内容。
public override void MyMethod2()
{
this.MyMethod("aaa");
}
总体上同意@Marc Gravell。
但是,我想提一下,这个问题在C++世界中已经足够古老了(http://www.devx.com/tips/Tip/12737(,答案看起来像"与在运行时解决的虚函数不同,默认参数是静态解决的,即在编译时。因此,由于一致性,这种 C# 编译器行为被故意接受,尽管它似乎出乎意料。
无论哪种方式都需要修复
我肯定会将其视为错误,要么是因为结果是错误的,要么是预期的结果,那么编译器不应该让您将其声明为"覆盖",或者至少提供警告。
我建议您向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 的问题一样,结果可能是不可预测的
当默认值的原始植入发生更改时,并且由于调用方不必重新编译,我们最终可能会得到不再有效的默认值
- 反射要求您提供默认值,调用方不必知道该默认值
破解代码
当我们有一个带有默认参数的函数,后来我们添加一个没有参数的函数时,所有调用现在都将路由到新函数,从而破坏所有现有代码,没有任何通知或警告!
类似的情况也会发生,如果我们稍后拿走没有参数的函数,那么所有调用将自动路由到带有默认参数的函数,同样没有通知或警告! 虽然这可能不是程序员的意图
此外,它不一定是常规实例方法,扩展方法将解决相同的问题,因为没有参数的扩展方法将优先于具有默认参数的实例方法!
摘要:远离可选参数,改用重载(就像 .NET 框架本身所做的那样(