重写方法上的 C# 可选参数
似乎在 .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();
}
}
Seems like in .NET Framework there is an issue with optional parameters when you override the method. The output of the code below is:
"bbb"
"aaa"
. But the output I'm expecting is:
"bbb"
"bbb"
.Is there a solution for this. I know it can be solved with method overloading but wondering the reason for this. Also the code works fine in 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();
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(9)
您可以通过调用以下方法来消除歧义:(
在
MyMethod2()
中)是否是一个 bug 很棘手;不过,它看起来确实不一致。 ReSharper 会警告您不要更改覆盖中的默认值(如果这有帮助);p 当然,ReSharper 也 会告诉您
this.
是多余的,并提供为您删除它...这会改变行为 - 所以 ReSharper 也并不完美。我承认,它看起来确实可以被视为编译器错误。我需要非常仔细查看才能确定……当你需要埃里克时他在哪里,是吗?
编辑:
这里的关键点是语言规范;让我们看看§7.5.3:
(事实上,§7.4 明确地忽略了
override
方法)这里存在一些冲突......它指出,如果有适用的方法,则不使用 base 方法派生类 - 这将引导我们使用派生方法,但同时,它表示不考虑标记为
override
的方法。但是,§7.5.1.1 则指出:
然后 §7.5.1.2 解释了如何在调用时评估这些值:
这明确强调了它正在查看参数列表,该列表先前在 §7.5.1.1 中定义为来自最具体的声明或覆盖。这似乎是第 7.5.1.2 节中提到的“方法声明”,因此传递的值应该是从最派生的类型一直到静态类型。
这表明:csc 有一个错误,它应该使用派生版本(“bbb bbb”),除非它受到限制(通过
base.
,或转换为基类型)来查看基方法声明(第 7.6.8 节)。You can disambiguate by calling:
(in
MyMethod2()
)Whether it is a bug is tricky; it does look inconsistent, though. ReSharper warns you simply not to have changes to the default value in an override, if that helps ;p Of course, ReSharper also tells you the
this.
is redundant, and offers to remove it for you ... which changes the behaviour - so ReSharper also isn't perfect.It does look like it could qualify as a compiler bug, I'll grant you. I'd need to look really carefully to be sure... where's Eric when you need him, eh?
Edit:
The key point here is the language spec; let's look at §7.5.3:
(and indeed §7.4 clearly omits
override
methods from consideration)There's some conflict here.... it states that the base methods are not used if there is an applicable method in a derived class - which would lead us to the derived method, but at the same time, it says that methods marked
override
are not considered.But, §7.5.1.1 then states:
and then §7.5.1.2 explains how the values are evaluated at the time of the invoke:
This explicitly highlights that it is looking at the argument list, which was previously defined in §7.5.1.1 as coming from the most specific declaration or override. It seems reasonable that this is the "method declaration" that is referred to in §7.5.1.2, thus the value passed should be from the most derived up-to the static type.
This would suggest: csc has a bug, and it should be using the derived version ("bbb bbb") unless it is restricted (via
base.
, or casting to a base-type) to looking at the base method declarations (§7.6.8).这里值得注意的一件事是每次都会调用重写的版本。将覆盖更改为:
输出为:
类中的方法可以执行以下一项或两项操作:
它可能不会同时执行两者,因为抽象方法只执行前者。
在
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
中被覆盖,以便它们可以被覆盖。在运行时,使用参数“aaa”调用
MyMethod(string)
。因为有一个被重写的形式,即被调用的形式,但它不是用“bbb”调用的,因为该值与运行时实现无关,而是与编译时定义有关。添加
this.
会更改检查的定义,从而更改调用中使用的参数。编辑:为什么这对我来说似乎更直观。
就我个人而言,由于我谈论的是直观的,所以它只能是个人的,我发现这更直观,原因如下:
如果我正在编码
BBB
,那么无论是调用还是重写MyMethod( string)
,我认为这是“做AAA
东西” - 这是BBB
的“做AAA
东西” “,但它正在做AAA
东西都是一样的。因此,无论是调用还是重写,我都会意识到AAA
定义了MyMethod(string)
。如果我调用使用
BBB
的代码,我会想到“使用BBB
stuff”。我可能不太清楚 AAA 最初定义的是哪个,我可能会认为这仅仅是一个实现细节(如果我没有同时使用 AAA ) > 接口附近)。编译器的行为符合我的直觉,这就是为什么当我第一次阅读这个问题时,我觉得 Mono 有一个错误。经过考虑,我看不出其中任何一个如何比另一个更好地实现指定的行为。
不过,就这一点而言,在保持个人水平的同时,我永远不会将可选参数与抽象、虚拟或重写方法一起使用,并且如果重写其他人的方法,我会匹配他们的参数。
One thing worth noting here, is that the overridden version is called each time. Change the override to:
And the output is:
A method in a class can do one or two of the following:
It may not do both, as an abstract method does only the former.
Within
BBB
the callMyMethod()
calls a method defined inAAA
.Because there is an override in
BBB
, calling that method results in an implementation inBBB
being called.Now, the definition in
AAA
informs calling code of two things (well, a few others too that don't matter here).void MyMethod(string)
."aaa"
and therefore when compiling code of the formMyMethod()
if no method matchingMyMethod()
can be found, you may replace it with a call to `MyMethod("aaa").So, that's what the call in
BBB
does: The compiler sees a call toMyMethod()
, doesn't find a methodMyMethod()
but does find a methodMyMethod(string)
. It also sees that at the place where it is defined there's a default value of "aaa", so at compile time it changes this to a call toMyMethod("aaa")
.From within
BBB
,AAA
is considered the place whereAAA
's methods are defined, even if overridden inBBB
, so that they can be over-ridden.At run-time,
MyMethod(string)
is called with the argument "aaa". Because there is a overridden form, that is the form called, but it is not called with "bbb" because that value has nothing to do with the run-time implementation but with the compile-time definition.Adding
this.
changes which definition is examined, and so changes what argument is used in the call.Edit: Why this seems more intuitive to me.
Personally, and since I'm talking of what is intuitive it can only be personal, I find this more intuitive for the following reason:
If I was coding
BBB
then whether calling or overridingMyMethod(string)
, I'd think of that as "doingAAA
stuff" - it'sBBB
s take on "doingAAA
stuff", but it's doingAAA
stuff all the same. Hence whether calling or overriding, I'm going to be aware of the fact that it wasAAA
that definedMyMethod(string)
.If I was calling code that used
BBB
, I'd think of "usingBBB
stuff". I might not be very aware of which was originally defined inAAA
, and I'd perhaps think of this as merely an implementation detail (if I didn't also use theAAA
interface nearby).The compiler's behaviour matches my intuition, which is why when first reading the question it seemed to me that Mono had a bug. Upon consideration, I can't see how either fulfils the specified behaviour better than the other.
For that matter though, while remaining at a personal level, I'd never use optional parameters with abstract, virtual or overridden methods, and if overriding someone else's that did, I'd match theirs.
这对我来说看起来像是一个错误。我相信它已经明确指定了,
并且它的行为应该与您调用相同的方式
带有显式
this
前缀的方法。我已将示例简化为仅使用单个虚拟
方法,并显示调用了哪个实现以及
参数值是什么:
所以我们需要担心的是 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
类型的值;它总是只使用默认值参数类型。)
规范第 7.6.8 节涉及 base.M(),其中表示
以及与非虚拟行为一样,该表达式也被视为
作为
((Base) this).M()
;所以对于基本方法来说这是完全正确的用于确定有效参数列表。这意味着
最后一行是正确的。
只是为了让那些想要看到上述真正奇怪的错误的人变得更容易,其中使用了未在任何地方指定的值:
This looks like a bug to me. I believe it is well specified,
and that it should behave in the same way as if you call the
method with the explicit
this
prefix.I've simplified the example to only use a single virtual
method, and show both which implementation is called and
what the parameter value is:
So all we need to worry about are the three calls within RunTests.
The important bits of the spec for the first two calls are section
7.5.1.1, which talks about the parameter list to be used when finding corresponding parameters:
And section 7.5.1.2:
The "corresponding optional parameter" is the bit that ties 7.5.2 to 7.5.1.1.
For both
M()
andthis.M()
, that parameter list should bethe one in
Derived
as static type of the receiver isDerived
,Indeed, you can tell that the compiler treats
that as the parameter list earlier in the compilation, as if you
make the parameter mandatory in
Derived.M()
, both of thecalls fail - so the
M()
call requires the parameter to havea default value in
Derived
, but then ignores it!Indeed, it gets worse: if you provide a default value for the
parameter in
Derived
but make it mandatory inBase
, the callM()
ends up usingnull
as the argument value. If nothing else,I'd say that proves it's a bug: that
null
value can't comefrom anywhere valid. (It's
null
due to that being the defaultvalue of the
string
type; it always just uses the default valuefor the parameter type.)
Section 7.6.8 of the spec deals with base.M(), which says that
as well as the non-virtual behaviour, the expression is considered
as
((Base) this).M()
; so it's entirely correct for the base methodto be used to determine the effective parameter list. That means
the final line is correct.
Just to make things easier for anyone who wants to see the really odd bug described above, where a value not specified anywhere is used:
您是否尝试过:
所以您实际上告诉您的程序使用重写方法。
Have you tried:
So you actually tell your program to use the overriden Method.
这种行为确实很奇怪;我不清楚这是否实际上是编译器中的错误,但可能是。
昨晚校园下了相当大的雪,西雅图不太擅长应对雪。我的公共汽车今天早上没有运行,因此我无法进入办公室比较 C# 4、C# 5 和 Roslyn 对于此案例的看法以及他们是否不同意。一旦我回到办公室并且可以使用适当的调试工具,我将在本周晚些时候尝试发布分析。
The behaviour is definitely very strange; it is not clear to me if it is in fact a bug in the compiler, but it might be.
The campus got a fair amount of snow last night and Seattle is not very good about dealing with snow. My bus is not running this morning so I'm not going to be able to get into the office to compare what C# 4, C# 5 and Roslyn say about this case and if they disagree. I'll try to post an analysis later this week once I'm back in the office and can use proper debugging tools.
这可能是由于歧义造成的,编译器优先考虑基类/超类。下面对 BBB 类的代码进行了更改,添加了对
this
关键字的引用,给出了输出“bbb bbb”:它暗示的一件事是您应该始终使用
this 关键字作为最佳实践。
我会担心基本方法和子方法中的这种歧义甚至没有引发编译器警告(如果不是错误),但如果确实如此,我想这是看不见的。
====== =================================================== ==========
编辑:请考虑以下链接中的示例摘录:
http://geekswithblogs.net/BlackRabbitCoder/archive/2011/07/28/c.net-little-pitfalls-default-parameters-are-compile-time-substitutions.aspx
<一个href="http://geekswithblogs.net/BlackRabbitCoder/archive/2010/06/17/c-Optional-parameters---pros-and-pitfalls.aspx" rel="nofollow">http://geekswithblogs.net /BlackRabbitCoder/archive/2010/06/17/c-可选参数---pros-and-pitfalls.aspx
陷阱:可选参数值是编译时的
使用可选参数时,需要记住一件事,并且只需要记住一件事。如果您牢记这一点,您很可能会很好地理解并避免使用它们的任何潜在陷阱:
有一件事是这样的:可选参数是编译时的语法糖!
陷阱:注意继承和接口实现中的默认参数
现在,第二个潜在的陷阱与继承和接口实现有关。我将用一个谜题来说明:
会发生什么?好吧,即使每种情况下的对象都是 SubTag,其标签是“SubTag”,你也会得到:
1: SubTag
2:基本标签
3:ITag
但请记住确保:
不要在现有默认参数集的中间插入新的默认参数,这可能会导致不可预测的行为,不一定会引发语法错误 - 添加到列表末尾或创建新方法。
在继承层次结构和接口中使用默认参数时要格外小心——根据预期用途选择最合适的级别来添加默认值。
=================================================== =======================
May be this is due to ambiguity and the compiler is giving priority to the base/super class. The below change to code of your class BBB with adding reference to
this
keyword, gives the output 'bbb bbb':One of the things it implies is you should always use the
this
keyword whenever you are calling properties or methods on the current instance of class as a best practice.I would be concerned if this ambiguity in base and child method didn't even raise a compiler warning (if not error), but if it does then that was unseen I suppose.
==================================================================
EDIT: Consider below sample excerpts from these links:
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
Pitfall: Optional parameter values are compile-time
There is one thing and one thing only to keep in mind when using optional parameters. If you keep this one thing in mind, chances are you may well understand and avoid any potential pitfalls with their usage:
That one thing is this: optional parameters are compile-time, syntactical sugar!
Pitfall: Beware of Default Parameters in Inheritance and Interface Implementation
Now, the second potential pitfalls has to do with inheritance and interface implementation. I’ll illustrate with a puzzle:
What happens? Well, even though the object in each case is SubTag whose tag is “SubTag”, you will get:
1: SubTag
2: BaseTag
3: ITag
But remember to make sure you:
Do not insert new default parameters in the middle of an existing set of default parameters, this may cause unpredictable behavior that may not necessarily throw a syntax error – add to end of list or create new method.
Be extremely careful how you use default parameters in inheritance hierarchies and interfaces – choose the most appropriate level to add the defaults based on expected usage.
==========================================================================
我认为这是因为这些默认值在编译时是固定的。如果您使用反射器,您将在 BBB 中看到 MyMethod2 的以下内容。
This I think is because these default values are fixed at the compile time. If you use reflector you will see the following for MyMethod2 in BBB.
总体同意@Marc Gravell 的观点。
不过,我想提一下,这个问题在 C++ 世界中已经足够老了(http://www .devx.com/tips/Tip/12737),答案看起来像“与在运行时解析的虚拟函数不同,默认参数是静态解析的,即在编译时解析的。”因此,这种 C# 编译器行为由于一致性而被故意接受,尽管它似乎是出乎意料的。
Agree in general with @Marc Gravell.
However, I'd like to mention that the issue is old enough in C++ world (http://www.devx.com/tips/Tip/12737), and the answer looks like "unlike virtual functions, which are resolved at run time, default arguments are resolved statically, that is, at compiled time." So this C# compiler behavior had rather been accepted deliberately due to consistency, despite its unexpectedness, it seems.
无论哪种方式都需要修复
我肯定会认为它是一个错误,要么因为结果是错误的,要么如果结果是预期的那么编译器不应该让你将它声明为“覆盖”,或者在至少提供警告。
我建议您向 Microsoft.Connect 报告此问题
这是对是错?
但是关于这是否是预期行为,让我们首先分析一下对此的两种观点。
考虑我们有以下代码:
有两种方法来实现它:
可选参数被视为重载函数,导致以下结果:
默认值嵌入到调用者中,从而产生以下代码:
两种方法之间有很多差异,但我们首先看一下 .Net 框架如何解释它。
在.Net中,您只能使用包含相同数量参数的方法来重写方法,但不能使用包含更多参数的方法来重写方法,即使它们都是可选的(这会导致导致调用与重写的方法具有相同的签名),例如您有:
您可以使用另一个不带参数的方法重载具有默认参数的方法,(这会产生灾难性的影响,因为我稍后会讨论),所以下面的代码是合法的:
当使用反射时必须始终提供默认值。
所有这些都足以证明.Net采取了第二次实现,因此OP看到的行为是正确的,至少根据.Net而言。
.Net 方法的问题
然而,.Net 方法确实存在问题。
一致性
就像OP的问题一样,当覆盖继承方法中的默认值时,结果可能是不可预测的
当默认值的原始植入发生更改时,并且由于调用者不必重新编译,我们最终可能会得到不再有效的默认值
破坏代码
当我们有一个带有默认参数的函数并且后来添加一个没有参数的函数时,所有调用现在都将路由到新函数,从而破坏所有现有代码,而不会发出任何通知或警告!
类似的情况也会发生,如果我们稍后去掉不带参数的函数,那么所有调用都会自动路由到带有默认参数的函数,同样不会有任何通知或警告!尽管这可能不是程序员的本意
此外,它不一定是常规实例方法,扩展方法也会出现同样的问题,因为不带参数的扩展方法将优先于带默认参数的实例方法!
总结:远离可选参数,并使用替代重载(正如 .NET 框架本身所做的那样)
Either Way It Needs A Fix
I would definitely regard it as a bug, either because the results is wrong or if the results are expected then the compiler should not let you declare it as "override", or at least provide a warning.
I would recommend you to report this to Microsoft.Connect
But Is It Right Or Wrong?
However regarding whether this is the expected behavior or not, let us first analyze the two views on it.
consider we have the following code:
There are two ways to implement it:
That optional arguments are treated like overloaded functions, resulting in the following:
That the default value is embedded in the caller, thus resulting in the following code:
There are many differences between the two approaches, but we will first take a look on how the .Net framework interprets it.
In .Net you can only override a method with a method that contains the same number of arguments, but you cannot override with a method containing more arguments, even if they are all optional (which would result in a call haveing the same signature as the overridden method), say for example you have:
You can overload a method with default arguments with another method with no arguments, (this has disastrous implications as I will discuss in a moments), so the folloing code is legal:
when using reflection one must always provide a default value.
All of which are enough to prove that .Net took the second implementation, so the behavior that the OP saw is right, at least according to .Net.
Problems With the .Net Approach
However there are real problems with the .Net approach.
Consistency
As in the OP's problem when overriding the default value in an inherited method, then results might be unpredictable
When the original implantation of the default value is changed, and since the callers don't have to get recompiled, we might end up with default values that are no longer valid
Breaking code
When we have a function with default arguments and latter we add a function with no arguments, all calls will now route to the new function, thus breaking all existing code, without any notification or warning!
Similar will happen, if we later take away the function with no arguments, then all calls will automatically route to the function with the default arguments, again with no notification or warning! although this might not be the intention of the programmer
Furthermore it does not have to be regular instance method, an extension method will do the same problems, since an extension method with no parameters will take precedence over an instance method with default parameters!
Summary: STAY AWAY FROM OPTIONAL ARGUMENTS, AND USE INSTEAD OVERLOADS (AS THE .NET FRAMEWORK ITSELF DOES)