I'll start out by saying that I may be completely missing the point. So feel free to leave your comments. I'm looking for any help I can get at this point.
Code for this article (and further experimentation) is available on GitHub: jeremybytes/interfaces-in-csharp-8. Note: at the time of this writing, the code uses .NET Core 3.0 Preview 8.
Some of the new features include the following:
- Default implementation for interface members
- Access modifiers - public, private, protected, internal
- abstract members
- static members
Default Implementation of Interface Members
Default implementation has been the most widely-touted enhancement to interfaces in C# 8. Mads Torgersen wrote an article on the topic: https://devblogs.microsoft.com/dotnet/default-implementations-in-interfaces/. This article has the example that I've seen repeated when this topic is brought up.
The assumption is that we have an interface with a single member, and a class that implements that interface.
Then another member is added to the interface. To keep the existing implementations from breaking, we add default behavior to the new interface member.
Here is an ILogger interface that has 2 members. The second has a default implementation (this is in the ILogger.cs file in the GitHub project):
The original class only implements the first member (this is in the InitialLogger.cs file in the GitHub project):
Since the 2nd member (the Log method that takes an exception parameter) has a default implementation, this code builds just fine.
We can put together a little console application that shows that both methods work (this is in the Program.cs file in the GitHub project);
The output shows that calls to both of the Log methods work:
Of course, we are also free to provide an implementation of the 2nd member in the class. This would hide the default implementation.
When I first heard of this feature, I thought it was a bad idea. This blurs the line between an abstract class (that contains implementation) and an interface (no implementation).
My recommendation would be to avoid default implementation for a few reasons.
First, I have only seen trivial examples where a good default exists. Most of our code is more complicated than that. If I want to add "Save" functionality to my API, there is probably no good default for that. If I end up throwing a NotImplementedException as a default, then that doesn't accomplish anything other than moving compile-time errors to run-time errors.
Second, we already have a way to extend functionality of an interface -- through interface inheritance. By creating a new "extended" interface that inherits from the original, the existing interface implementations continue to work. And if a client needs the extended functionality, then the client can use the new extended interface (this also helps us follow the interface segregation principle).
Third, interfaces are a difficult enough concept as it is. When I started programming professionally, it took me a good 2 years before I really understood interfaces and what they were good for. This was such a frustration to me that I've spent the last 7-1/2 years helping developers understand interfaces (this includes 33 live presentations, various blog articles, and a Pluralsight course that has over 100,000 unique viewers). If nothing else, this points out that the topic can be a challenge for developers to grasp.
Messaging from Microsoft
Most of the messaging from Microsoft has been around this -- adding functionality to an existing API. The idea that if you have an existing interface, you can add to it without breaking the initial implementations.
But it turns out that this is not the only reason this feature was added to the language.
For this, we can turn to C# 8 Language Design Proposals. Here is the article for default implementation: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/default-interface-methods.
There are 3 reasons stated for this change: (1) extending APIs, (2) interoperability with Android (Java) and iOS (Swift), (3) supporting the "traits" language feature.
"Traits" are really a way of doing mix-ins with classes. The idea is that small groupings of code can be combined to form classes.
I think that traits can be a good idea; however, I think that using interfaces to implement that feature is a mistake. Another construct in C# would be less confusing to developers (even if it compiles down to the same IL).
To show the confusion that's on its way, let's take a look at some of the other changes to interfaces that facilitate these features.
Previously, access modifiers were not allowed on interface members. Here's an example from a .NET Framework 4.7.2 project:
Previously, all interface members were automatically visible. We weren't even allowed to mark something as "public". If we think of interfaces as a contract, then it makes sense that we are not allowed to use access modifiers. The terms of the contract need to be visible to everyone -- both the clients and the implementing classes.
But things are different with C# 8. We can have access modifiers. Here's an example of an interface that has a "protected" member (this is in the IReader.cs file in the GitHub project):
This causes a bit of confusion when implementing the interface. Mainly because the tooling isn't there yet. (I'm hoping it will be there soon; .NET Core 3.0 releases in 3 weeks.)
If we create a new class and mark it with the interface, we get the standard errors that there are members that are not implemented:
Note: since the Save method has a default implementation, it does not show up with the missing members.
I use the Ctrl+. shortcut to implement interfaces (or use the light bulb helper):
This, however, only implements the non-protected member. So if we use this "fix" and try to build, we still get errors:
The protected member is still not implemented.
To implement the protected member, we need to do an explicit implementation. This associates the method explicitly with the interface. This is another option on the Ctrl+. menu:
But if we do this, it implements *both* of the methods explicitly:
Note: Regardless of whether we use "Implement interface" or "Implement interface explicitly". The Save method (which has a default implementation) is not included. If we want to override that behavior, we need to type that in ourselves.
Now we have 2 implementations for "GetItem": one associated with the class, and one associated with the interface. This is probably not what we want.
You can take a look at the IntReader.cs file in the GitHub project to see the implementation that is probably more inline with what we want in this case:
This is a tooling problem. It can be fixed in Visual Studio, but it opens the door to some of the other things that come up when we have access modifiers on interface members.
Since we can add access modifiers to interface members, we can have "private" members.
In this code, the "Save" method is marked as "private". The compiler will only let you mark a member as "private" if it has an implementation. So that's good.
But what would we do with a private interface member? The answer is not much. It is not accessible outside of the interface, so it is really only good if we want to create a method that will be used in other default implementations in the interface.
At this point, we're really creating a class, and an abstract class would be a much better solution.
The thing that make me go O_o is the fact that we can have "abstract" interface members. The following code is valid and builds (this is the final IReader.cs file from GitHub):
In this code, the "GetItem" method is marked as "abstract".
What does this even mean? When we're dealing with abstract classes, abstract members have a declaration but not implementation. That describes *everything* in an interface (okay, it described everything in an interface until we had default implementation).
I'm a bit confused by this. The code behaves the same way whether the "abstract" is here or not.
I'll need to do a bit more research to find out why this is necessary.
Interfaces can also have static members. This is something that is brand new. Statics were not allowed at all in interfaces previously.
If you're curious about using static members in interfaces, take a look at the tutorial: Update interfaces with default interface members in C# 8.0. This example shows how static members can be used to parameterize default implementations.
Static members break the spirit of interfaces. Interfaces have to do with describing a contract -- something that a client can rely upon being there and an implementer has to include. The tutorial shows using static fields to hold data. Those are now implementation details.
In my view, interfaces in C# 8 are a bit of a mess. We can no longer think of them as a contract. They are no longer the "shape" of a set of members in a class or struct. Instead, they can contain implementation. The members are no longer automatically visible, they can be private, protected, or internal.
I may be completely missing the point. But I can no longer answer the question: "What is an interface?"
An interface is no longer one thing. It has multiple aspects depending on how we want to use it. It will take a while to make sense of the new world. If you want to share your views, please leave a comment. I'm looking for clarification and to gain a better understanding of the changes.
Stay tuned to the GitHub project (jeremybytes/interfaces-in-csharp-8). There will be more samples added and more articles. Together we can figure this out.