Reputation: 41470
In "The C# Programming Language" book Eric Lippert mentioned this:
A subtle point here is that an overridden virtual method is still considered to be a method of the class that introduced it, and not a method of the class that overrides it.
What is the significance of this statement? Why does it matter if the overridden virtual method is considered to be a method of the class that introduced it (or otherwise) since the overridden method will never be called unless you are dealing with the derived class?
Upvotes: 4
Views: 7264
Reputation: 55339
Here's the full quote from the book:
A subtle point here is that an overridden virtual method is still considered to be a method of the class that introduced it, and not a method of the class that overrides it. The overload resolution rules in some cases prefer members of more derived types to those in base types; overriding a method does not "move" where that method belongs in this hierarchy.
At the very beginning of this section, we noted that C# was designed with versioning in mind. This is one of those features that helps prevent "brittle base-class syndrome" from causing versioning problems.
The full quote makes it clear that Eric Lippert is talking specifically about method overloading, not just about how virtual methods work.
As an example, consider the following program:
class Base
{
public virtual void M2(int i) { }
}
class Derived : Base
{
public void M1(int i) { Console.WriteLine("Derived.M1(int)"); }
public void M1(float f) { Console.WriteLine("Derived.M1(float)"); }
public override void M2(int i) { Console.WriteLine("Derived.M2(int)"); }
public void M2(float f) { Console.WriteLine("Derived.M2(float)"); }
public static void Main()
{
Derived d = new Derived();
d.M1(1);
d.M2(1);
}
}
I think many developers would be surprised that the output is
Derived.M1(int) Derived.M2(float)
Why would d.M2(1)
invoke Derived.M2(float)
even though Derived.M2(int)
is a better match?
When the compiler is determining what the M1
in d.M1(1)
refers to, the compiler sees that both M1(int)
and M1(float)
are introduced in Derived
, so both overloads are applicable candidates. The compiler selects M1(int)
over M1(float)
as the best match for the integer argument 1
.
When the compiler is determining what the M2
in d.M2(1)
refers to, the compiler sees that M2(float)
is introduced in Derived
and is an applicable candidate. According to the overload resolution rules, "methods in a base class are not candidates if any method in a derived class is applicable". Because M2(float)
is applicable, this rule prevents M2(int)
from being a candidate. Even though M2(int)
is a better match for the integer argument and even though it's overridden in Derived
, it's still considered to be a method of Base
.
Upvotes: 5
Reputation: 81115
Understanding that an overridden virtual method belongs to the class which introduces it, rather than the class that overrides it, makes it easier to understand the way class members are bound. Except when using dynamic
objects, all bindings in C# are resolved at compile time. If a BaseClass
declares a virtual method foo
, and DerivedClass:BaseClass
overrides foo
, code which attempts to call foo
on a variable of type BaseClass
will always be bound to virtual method "slot" BaseClass.foo
, which will in turn point to the actual DerivedClass.foo
method.
This understanding can be especially important when dealing with generics, since in C#, unlike C++, members of generic types are bound according to the generics' constraints, rather than according to the concrete generic types. For example, suppose there were a SubDerivedClass:DerivedClass
which created a new virtual
method foo()
, and one defined a method DoFoo<T>(T param) where T:BaseClass {param.foo();}
. The param.foo()
call would be bound to BaseClass.foo
even if the method were invoked as DoFoo<SubDrivedClass>(subDerivedInstance)
. If the parameter were cast to SubDerivedClass
before invoking foo
, the call would be bound to SubDrivedClass.foo()
, but the compiler can't tell when producing DoFoo<T>
that T
will be anything more specific than BaseClass
, it can't bind to anything that doesn't exist in BaseClass
.
Incidentally, there are times it would be useful if a class could simultaneously override a base-class member and create a new one; for example, given an abstract base class ReadableFoo
with some abstract read-only property, it would be helpful if a class MutableFoo
could both provide an override for that property and define a read-write property with the same name. Unfortunately, .net does not allow that. Given such a restriction, the best approach may be for ReadableFoo
to provide a concrete non-virtual read-only property which calls an protected abstract
method with a different name to get the value. That way, a derived class could shadow the read-only property with a read-write one (that would call the same virtual method for reading, or a new (possibly virtual) method for writing.
(following is untested)
class BaseClass
{
public virtual void foo() {Console.WriteLine("BaseClass.Foo");
}
class DerivedClass:BaseClass
{
public override void foo() {Console.WriteLine("Derived.Foo");
}
class SubDerivedClass:DerivedClass
{
public new virtual void foo() {Console.WriteLine("SubDerived.Foo");
}
class MegaDerivedClass:SubDerivedClass
{
public override void foo() {Console.WriteLine("MegaDerived.Foo");
}
void DoFoo1<T>(T param) where T:BaseClass
{
param.foo();
}
void DoFoo1<T>(T param) where T:SubDerivedClass
{
param.foo();
}
void test(void)
{
var megaDerivedInstance = new MegaDerivedClass();
DoFoo1<MegaDerivedClass>(megaDerivedInstance);
DoFoo2<MegaDerivedClass>(megaDerivedInstance);
}
A SubDerivedClass has two virtual foo()
methods: BaseClass.foo()
and SubDerivedClass.foo()
. A MegaDerivedClass
has those same two methods. Note that classes derived from SubDerivedClass()
which attempt to override foo
will override SubDerivedClass.foo()
and will not affect BaseClass.foo()
; with the declarations as above, no derivative of SubDerivedClass
can override BaseClass.Foo
. Note also that casting an instance of SubDerivedClass
or a subclass thereof to DerivedClass
or BaseClass
will expose the BaseClass.foo
virtual method for calling.
Incidentally, if the method declaration in SubDerivedClass
had been friend new virtual void foo() {Console.WriteLine("SubDerived.Foo");
, it would be impossible for other classes within the same assembly to override BaseClass.foo()
(since any attempt to override foo()
would override SubDerivedClass.foo()
), but classes outside the assembly which derive from SubDerivedClass
wouldn't see SubDerivedClass.foo()
and could thus override BaseClass.foo()
.
Upvotes: 0
Reputation: 700152
It matters when you have a reference of one type pointing to an object of a different type.
Example:
public class BaseClass {
public virtual int SomeVirtualMethod() { return 1; }
}
public class DerivedClass : BaseClass {
public override int SomeVirtualMethod() { return 2; }
}
BaseClass ref = new DerivedClass();
int test = ref.SomeVirtualMethod(); // will be 2
Because the virtual method is a member of the base class, you can call the overriding method with a reference of the base class type. If it wasn't, you would need a reference of the derived type to call the overriding method.
When you are shadowing a method instead of overriding it, the shadowing method is a member of the derived class. Depending on the type of the reference you will be calling the original method or the shadowing method:
public class BaseClass {
public int SomeMethod() { return 1; }
}
public class DerivedClass : BaseClass {
public new int SomeMethod() { return 2; }
}
BaseClass ref = new DerivedClass();
int test = ref.SomeMethod(); // will be 1
DerivedClass ref2 = ref;
int test2 = ref2.SomeMethod(); // will be 2
Upvotes: 7