In this blog post, I will discuss some pitfall when implementing interfaces and how to avoid it.
Examining the problem
Create a new Console project (either .NET Framework or .NET Core, same difference).
Let's start with the ICar
interface. This interface has a single Accelerate
method.
interface ICar
{
void Accelerate();
}
And we have a Car
class implementing the ICar
interface.
class Car : ICar
{
public void Accelerate()
=> Console.WriteLine("Vroem!!");
}
We create an instance of the Car
class and call the Accelerate
method from both Car
and ICar
reference.
Console.WriteLine("Calling Accelerate method on Car.");
Car c = new Car();
ICar ic = c;
Console.Write("Car.Accelerate => ");
c.Accelerate();
Console.Write("ICar.Accelerate => ");
ic.Accelerate();
Running the application generates the expected output.
Calling Accelerate method on Car.
Car.Accelerate => Vroem!!
ICar.Accelerate => Vroem!!
Let's inherit a Tesla
class from the Car
class.
class Tesla : Car
{
}
We will extend our program calling the Tesla's Accelerate
method.
Console.WriteLine("Calling Accelerate method on Tesla.");
Tesla t = new Tesla();
Console.Write("Tesla.Accelerate => ");
t.Accelerate();
Console.Write("Car.Accelerate => ");
c = t;
c.Accelerate();
Console.Write("ICar.Accelerate => ");
ic = t;
ic.Accelerate();
We haven't overwritten the Accelerate
method (yet), so calling the methods on the Tesla instance gives us the same output, since Tesla
inherits the Accelerate
method.
Calling Accelerate method on Tesla.
Tesla.Accelerate => Vroem!!
Car.Accelerate => Vroem!!
ICar.Accelerate => Vroem!!
So far, so good... But Teslas don't make noise, so let's overwrite the Tesla.Accelerate
method:
class Tesla : Car
{
public void Accelerate()
=> Console.WriteLine("Zzzzzzzzz");
}
Running the program again now shows a different and weird result! Since you're calling the same method on the same object you would expect all invocations to return Zzzzzzzzz
!
Calling Accelerate method on Tesla.
Tesla.Accelerate => Zzzzzzzzz
Car.Accelerate => Vroem!!
ICar.Accelerate => Vroem!!
So why do we get different output? Look again at the Tesla
class. You'll see a twiggly line beneath the Accelerate
method, telling you that Tesla.Accelerate()
hides inherited member Car.Accelerate()
.
Actually, our implementation defaults to the new
keyword, so the code looks like this:
class Tesla : Car
{
public new void Accelerate()
=> Console.WriteLine("Zzzzzzzzz");
}
Now things make a little more sense. If you call the Tesla
instance using a Car
reference you'll call the Car.Accelerate
method, and if you call the instance using a Tesla
reference you'll call the Tesla.Accelerate
method! Our Tesla
instance has two Accelerate
methods, and you can choose which one to call using the reference type, the class or the interface.
If you want to learn a little more about the difference between the override
and new
keyword in polymorphism, you should read this article from Microsoft C# docs.
Now we're at the actual problem.
Imagine you have a method taking a Car
or ICar
argument, and you call it with a Tesla
instance. You'll end up with different behavior!
Imagine debugging this. When you step into the Accelerate
method using a Tesla
reference, you'll get a different implementation than when you step into the Accelerate
method in your method! This will baffle you for a while, and can result in subtle bugs!
Fixing the problem
The cause of all this confusion comes from the new
keyword. Let's replace it with the override
keyword!
class Tesla : Car
{
public override void Accelerate()
=> Console.WriteLine("Zzzzzzzzz");
}
In order for the override to compile, we need to make this method virtual:
class Car : ICar
{
public virtual void Accelerate()
=> Console.WriteLine("Vroem!!");
}
Now the program gives the expected output!
Calling Accelerate method on Car.
Car.Accelerate => Vroem!!
ICar.Accelerate => Vroem!!
Calling Accelerate method on Tesla.
Tesla.Accelerate => Zzzzzzzzz
Car.Accelerate => Zzzzzzzzz
ICar.Accelerate => Zzzzzzzzz
But hey! We have changed this method to be virtual! And virtual
calls are more expensive than normal calls!
The C# runtime uses virtual calls for interface methods
As it turns out, any method you write to implement an interface is called as a virtual method behind the scenes anyway.
Undo your changes so the Car.Accelerate
is no longer virtual and your code compiles.
Open ILSpy, and open your console application with it.
Now decompile Program.Main()
using IL with C#. Scroll down to the first call of the Accelerate
method.
// car.Accelerate();
IL_0025: ldloc.0
IL_0026: callvirt instance void ImplementingInterfaces.Car::Accelerate()
As you can see, the runtime uses a virtual call (callvirt
) to the Accelerate
method, even when it has not been declared to be virtual, since it's an interface method.
Overriding non-virtual interface methods
You could also argue that you don't want derived classes to override the interface's method behavior. But you can do this anyway. How? Look at this example, where I simply re-implement the ICar
interface:
class Car : ICar
{
public void Accelerate()
=> Console.WriteLine("Vroem!!");
}
class Tesla : Car, ICar
{
public new void Accelerate()
=> Console.WriteLine("Zzzzzzzzz");
}
Running the program results in this output. Calling Accelerate
using the ICar
interface now invokes the Tesla.Accelerate
method, effectively overriding the method in the ICar
interface.
Calling Accelerate method on Car.
Car.Accelerate => Vroem!!
ICar.Accelerate => Vroem!!
Calling Accelerate method on Tesla.
Tesla.Accelerate => Zzzzzzzzz
Car.Accelerate => Vroem!!
ICar.Accelerate => Zzzzzzzzz
So we were able to re-implement the interface method in the derived class. Again, making the base class interface method non-virtual does not protect against overriding the interface method in a derived class!
Conclusion
Implement your class's interface non-private methods to be virtual
, this will allow derived classes to override the method in a consistent way. Otherwise, you might end up with confusing behavior depending on the reference used to invoke the method.
In many cases, this will also allow you to easily fake these classes during unit testing since you can inherit from the actual class, and fake any interface method simply by overriding it.