似乎在.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();
}
}
答案 0 :(得分:34)
您可以通过致电:
来消除歧义this.MyMethod();
(在MyMethod2()
)
是否是一个bug很棘手;但它确实看起来不一致。如果有帮助的话,Resharper会警告你根本不要更改覆盖中的默认值; p当然,resharper 也告诉你this.
是多余的,并且提议删除它你...改变了行为 - 所以resharper也不完美。
看起来可能有资格作为编译器错误,我会授予你。我需要仔细看看真的以确定......当你需要他时,Eric会在哪里,呃?
编辑:
这里的关键点是语言规范;让我们看看§7.5.3:
例如,方法调用的候选集不包括标记为override的方法(第7.4节),如果派生类中的任何方法适用(第7.6.5.1节),则基类中的方法不是候选方法。
(事实上§7.4明确省略了override
方法的考虑)
这里存在一些冲突....它声明如果派生类中有适用的方法,则不会使用 base 方法 - 这将导致我们派生方法,但与此同时,它表示不考虑标记为override
的方法。
但是,§7.5.1.1然后声明:
对于在类中定义的虚方法和索引器,参数列表是从函数成员的最具体的声明或覆盖中选取的,从接收者的静态类型开始,并搜索其基类。
然后§7.5.1.2解释了在调用时如何计算值:
在函数成员调用(第7.5.4节)的运行时处理期间,参数列表的表达式或变量引用按从左到右的顺序进行评估,如下所示:
...(剪断)...
当从具有相应可选参数的函数成员中省略参数时,将隐式传递函数成员声明的默认参数。因为它们总是不变的,所以它们的评估不会影响其余参数的评估顺序。
这明确强调它正在查看参数列表,该列表先前在§7.5.1.1中定义为来自最具体的声明或覆盖。这是§7.5.1.2中引用的“方法声明”似乎是合理的,因此传递的值应该来自最多派生的静态类型。
这表明:csc有一个错误,它应该使用派生的版本(“bbb bbb”),除非它受到限制(通过base.
,或者转换为基础-type)查看基本方法声明(第7.6.8节)。
答案 1 :(得分:23)
有一点需要注意的是,每次调用被覆盖的版本。将覆盖更改为:
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
中被覆盖,也是可以< / em>被过度使用。
在运行时,使用参数“aaa”调用MyMethod(string)
。因为有一个被覆盖的表单,即被调用的表单,但它不是用“bbb”调用的,因为该值与运行时实现无关,而是与编译时定义无关。
添加this.
更改检查哪个定义,从而更改调用中使用的参数。
编辑:为什么这对我来说更直观。
就个人而言,既然我说的是直观的,它只能是个人的,我发现这更直观,原因如下:
如果我正在编码BBB
,那么无论是致电还是覆盖MyMethod(string)
,我都会将其视为“正在做AAA
内容” - BBB
采取的措施做AAA
东西“,但它正在做AAA
个东西。因此无论是调用还是覆盖,我都会意识到AAA
定义了MyMethod(string)
这一事实。
如果我调用使用BBB
的代码,我会想到“使用BBB
内容”。我可能不太了解最初在AAA
中定义的内容,我可能认为这只是一个实现细节(如果我没有使用附近的AAA
接口)。< / p>
编译器的行为符合我的直觉,这就是为什么当我第一次阅读这个问题时,我认为Mono有一个bug。经过考虑,我看不出如何比另一个更好地完成指定的行为。
但就此而言,在保持个人层面的同时,我永远不会使用抽象,虚拟或重写方法的可选参数,如果覆盖其他人,我会匹配他们的。
答案 2 :(得分: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中的三个调用。 前两个调用的规范的重要部分是section 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();
}
}
答案 3 :(得分:10)
你试过了吗?
public override void MyMethod2()
{
this.MyMethod();
}
所以你实际上告诉你的程序使用覆盖方法。
答案 4 :(得分:9)
这种行为绝对很奇怪;我不清楚它是否实际上是编译器中的一个错误,但它可能是。
昨晚校园得到了相当多的积雪,而西雅图对雪的处理并不是很好。我的公共汽车今天早上没有运行,所以我不能进入办公室来比较C#4,C#5和Roslyn对这个案子的看法,以及他们是否不同意。一旦我回到办公室并且可以使用适当的调试工具,我将在本周晚些时候尝试发布分析。
答案 5 :(得分: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
关键字强>
我会担心,如果基础和子方法中的这种歧义甚至没有引发编译器警告(如果不是错误),但如果确实如此,那么我认为这是看不见的。
=============================================== ===================
编辑:请考虑以下示例摘录:
陷阱:可选参数值是编译时 使用可选参数时,只需要记住一件事和一件事。如果你记住这一点,你很可能会理解并避免使用它们的任何潜在缺陷: 有一点是这样的:可选参数是编译时,语法糖!
陷阱:谨防继承和接口实施中的默认参数
现在,第二个潜在的陷阱与继承和接口实现有关。我将用一个谜题来说明:
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:SubTag 2:BaseTag 3:ITag
但请记住确认:
不要在现有的一组默认参数的中间插入新的默认参数,这可能会导致不可预测的行为,这可能不一定会引发语法错误 - 添加到列表末尾或创建新方法。 如何在继承层次结构和接口中使用默认参数时要非常小心 - 根据预期用法选择最合适的级别来添加默认值。
=============================================== ===========================
答案 6 :(得分:1)
我认为这是因为这些默认值在编译时是固定的。如果您使用反射器,您将在BBB中看到MyMethod2的以下内容。
public override void MyMethod2()
{
this.MyMethod("aaa");
}
答案 7 :(得分:0)
无论哪种方式都需要修复
我肯定会将它视为一个错误,或者因为结果错误或者结果是预期的,那么编译器不应该让你将其声明为“覆盖”,或者至少提供警告。
我建议您将此报告给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中,您只能使用包含相同数量参数的方法覆盖方法,但不能使用包含 more 参数的方法覆盖,即使它们都是可选的(这会导致调用与重写方法具有相同的签名),比如你有:
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框架本身那样)
答案 8 :(得分:0)
与@Marc Gravell一致同意。
但是,我想提一下,这个问题在C ++世界(http://www.devx.com/tips/Tip/12737)中已经足够老了,答案看起来像“与运行时解析的虚函数不同,默认参数是静态解析的,就是在编译时。“所以这个C#编译器行为由于一致性而被故意接受,尽管看起来很意外。