Sunday, October 18, 2020

Abstract Classes vs. Interfaces in C# - What You Know is Probably Wrong

A little over a year ago, C# 8 changed a lot of things about interfaces. One effect is that the technical line between abstract classes and interfaces has been blurred. It's been a year, and I haven't seen much online about this, so let's take a closer look at how things have changed.

To show the changes, here is a slide I used to use to show the differences. 4 of the 5 differences no longer apply:

Side-by-side list of differences between interfaces and abstract classes with 4 of the 5 differences being crossed out.

A lot of folks are not aware of these changes, so let's go through what we used to know about the differences between abstraction classes and interfaces and see if it still holds up today.

Note: For more information on the changes to interfaces in C# 8, you can take a look at the code and articles in this repository: https://github.com/jeremybytes/csharp-8-interfaces. There are also links to videos that demonstrate the new features (and some of the pitfalls).


Difference #1

An abstract class may contain implementation code. An interface may not have implementation code, only declarations.

Status: No Longer True

Interfaces in C# 8 can have implementation code -- referred to as "default implementation". For example, an interface method can have a body that provides functionality. This functionality is used by default if the implementing class does not provide its own implementation.

An interface can also provide default implementation for properties, but due to limitations with property setters, this only makes sense in specific circumstances.

More information: C# 8 Interfaces: Properties and Default Implementation.


Difference #2

A class may only inherit from a single abstract class, but a class may implement any number of interfaces.

Status: Still True

C# still has single inheritance when it comes to classes. But because a class can implement any number of interfaces, and those interfaces may have implementation code, there's a type of multiple inheritance that is available through interfaces.

When calling default implementation members, the caller must use the interface type (just like when a class has an explicit implementation). This avoids the "diamond problem", which is where two base classes provide the same method. By specifying the interface, this avoids the runtime having to make decisions of which method to use.


Difference #3

Abstract class members can have access modifiers (public, private, protected, etc.). Interface members are automatically public and cannot be changed.

Status: No Longer True

In C# 8 interface members can have access modifiers. The default is public (so existing interfaces will still work as expected). But interfaces can now have private members that are only accessible from within the interface itself. These may be useful for breaking up larger methods that have default implementation.

Note: Protected members are also possible, but because of the weirdness of implementation, I have not found a practical use for them.

More information: C# 8 Interfaces: Public, Private, and Protected Members.


Difference #4

Abstract classes can contain fields, properties, constructors, destructors, methods, events, and indexers. Interfaces can contain properties, methods, events, and indexers (not fields, constructors, or destructors).

Status: Mostly True

This is still mostly true. When it comes to instance members, interfaces can contain only properties, methods, events, and indexers. Instance fields and constructors are still not allowed.

But, interfaces can contain static members (see "Difference #5" below). This means that interfaces can have static fields, static constructors, and static destructors.


Difference #5

Abstract classes may contain static members. Interfaces cannot have static members.

Status: No Longer True

As noted above, interfaces in C# 8 can have static members. A static method in an interface works exactly the same way as a static method on a class. 

The same is true for static fields. Yes, fields are allowed in an interface, but only if they are static. As a reminder, a static field belongs to the type itself (the interface) and not any of the instances (meaning, instances of a class that implement the interface). This means that it is a shared value, so you want to use it with care. Note that this is exactly how static fields work on classes, so if you've used them there, you already know the quirks.

More information: C# 8 Interfaces: Static Members.

Also, having a "static Main()" method is now allowed in an interface. If you want to drive your co-workers crazy, here's one way to misuse this: Misuing C#: Multiple Main() Methods.


Technical Differences vs. Usage Differences

So if we look at the 5 technical differences between abstract classes and interfaces, 3 of them are no longer true, and 1 of them has some new caveats.

This really blurs the technical line between abstract classes and interfaces. When it comes to actually using abstract classes and interfaces, I'm currently sticking with my previous approach.

If there is *no* shared code between implementations...
I use an interface. Interfaces are a good place to declare a set of capabilities on an object. I like to use interfaces in this case (rather than a purely abstract class) because it leaves the inheritance slot open (meaning, the class can descend from a different base class if necessary).

If there *is* shared code between implementations...
I use an abstract class. An abstract class is a good place to put shared implementation code so that it does not need to be repeated in each of the descending classes.

Why not use interfaces with defaults for shared implementation? Well, there are a lot of interesting things that come up with default implementation, such as needing to refer explicitly to the interface and not the concrete type. Using the interface type is a generally a good idea (and I recommend that), but when it is forced on you, it can be confusing.

Another limitation has to do with properties. Because interfaces cannot have instance fields, there is no way to set up real properties using default implementation. Abstract classes can have fields, so we can put full properties and automatic properties into an abstract class that are then inherited by descending classes.

My approach is a personal preference based on how interfaces and abstract classes have been used in the past. I'm all for language and technique evolving, but this is a slow process. And until certain practices become widespread, I like to use "the path of least surprise". In this case, it means keeping a logical separation between abstract classes and interfaces even though the technical separation has been blurred.

Mix Ins
One other way that default implementation can be used is for mix ins. This is where an interface *only* contains implemented members. This functionality can then be "mixed in" to a class by simply declaring that the class implements the interface. No other changes to the class are necessary.

This is an interesting idea, and I still have to explore it further. Personally, I wish this functionality got a new keyword. It muddies things up a bit when we try to define what an interface is.

Wrap Up

So if we look at the 5 differences between abstract classes and interfaces, we find that most of them are not longer true starting with C# 8.

If you'd like to see the implications of these changes, take a look at the articles available here: https://github.com/jeremybytes/csharp-8-interfaces or here: A Closer Look at C# 8 Interfaces

And if you'd like a video walkthrough with lots of code samples, you can take a look at this video on YouTube: What's New in C# 8 Interfaces (and how to use them effectively). This is from a talk I did for the Phoenix AZ area user groups.


Happy Coding!

1 comment:

  1. Jeremy, thank you for this explanation. The whole thing seemed like a bad idea, until you got to the part about mix ins. I can make use of that in my code, I suppose all of the other changes were needed to enable that type of usage. Agree with you, they should have picked another keyword.

    ReplyDelete