C# optional parameters on overridden methods
Solution 1:
You can disambiguate by calling:
this.MyMethod();
(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:
For example, the set of candidates for a method invocation does not include methods marked override (§7.4), and methods in a base class are not candidates if any method in a derived class is applicable (§7.6.5.1).
(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:
For virtual methods and indexers defined in classes, the parameter list is picked from the most specific declaration or override of the function member, starting with the static type of the receiver, and searching through its base classes.
and then §7.5.1.2 explains how the values are evaluated at the time of the invoke:
During the run-time processing of a function member invocation (§7.5.4), the expressions or variable references of an argument list are evaluated in order, from left to right, as follows:
...(snip)...
When arguments are omitted from a function member with corresponding optional parameters, the default arguments of the function member declaration are implicitly passed. Because these are always constant, their evaluation will not impact the evaluation order of the remaining arguments.
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).
Solution 2:
One thing worth noting here, is that the overridden version is called each time. Change the override to:
public override void MyMethod(string s = "bbb")
{
Console.Write("derived: ");
base.MyMethod(s);
}
And the output is:
derived: bbb
derived: aaa
A method in a class can do one or two of the following:
- It defines an interface for other code to call.
- It defines an implementation to execute when called.
It may not do both, as an abstract method does only the former.
Within BBB
the call MyMethod()
calls a method defined in AAA
.
Because there is an override in BBB
, calling that method results in an implementation in BBB
being called.
Now, the definition in AAA
informs calling code of two things (well, a few others too that don't matter here).
- The signature
void MyMethod(string)
. - (For those languages that support it) the default value for the single parameter is
"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 to MyMethod()
, doesn't find a method MyMethod()
but does find a method MyMethod(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 to MyMethod("aaa")
.
From within BBB
, AAA
is considered the place where AAA
's methods are defined, even if overridden in BBB
, 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 overriding MyMethod(string)
, I'd think of that as "doing AAA
stuff" - it's BBB
s take on "doing AAA
stuff", but it's doing AAA
stuff all the same. Hence whether calling or overriding, I'm going to be aware of the fact that it was AAA
that defined MyMethod(string)
.
If I was calling code that used BBB
, I'd think of "using BBB
stuff". I might not be very aware of which was originally defined in AAA
, and I'd perhaps think of this as merely an implementation detail (if I didn't also use the AAA
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.
Solution 3:
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:
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();
}
}
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:
For virtual methods and indexers defined in classes, the parameter list is picked from the most specific declaration or override of the function member, starting with the static type of the receiver, and searching through its base classes.
And section 7.5.1.2:
When arguments are omitted from a function member with corresponding optional parameters, the default arguments of the function member declaration are implicitly passed.
The "corresponding optional parameter" is the bit that ties 7.5.2 to 7.5.1.1.
For both M()
and this.M()
, that parameter list should be
the one in Derived
as static type of the receiver is Derived
,
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 the
calls fail - so the M()
call requires the parameter to have
a 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 in Base
, the call
M()
ends up using null
as the argument value. If nothing else,
I'd say that proves it's a bug: that null
value can't come
from anywhere valid. (It's null
due to that being the default
value of the string
type; it always just uses the default value
for 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 method
to 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:
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();
}
}
Solution 4:
Have you tried:
public override void MyMethod2()
{
this.MyMethod();
}
So you actually tell your program to use the overriden Method.