BT

.NET Futures: Multiple Inheritance

| by Jonathan Allen on Apr 10, 2017. Estimated reading time: 8 minutes |

A controversial new proposal for .NET suggests the introduction of a limited form of multiple inheritance via abstract interfaces. This feature was inspired by Java’s default methods.

The purpose of a default method is to allow a developer to modify an abstract interface after it has been published. Normally this isn’t allowed in Java or .NET, because it would be a breaking change. But with default methods, the interface author can provide an overridable implementation which could alleviate the backwards compatibility issue.

The C# version of this proposal would include syntax for:

  • a method body for a method (i.e. a "default" implementation)
  • a body for a property accessor
  • static methods and properties
  • private methods and properties (the default access is public)
  • override methods and properties

This proposal would not give interfaces the ability to have fields, therefore it is a limited form of multiple inheritance that avoids some of the problems found in C++ (although even fields can be simulated using a ConditionalWeakTable and the extension property pattern).

Use Case: IEnumerble.Count

The most cited use case for this feature is the ability to add a Count property to IEnumerable<T>. The idea is that instead of using the Enumerable.Count extension method, developers can get Count for free and optionally override it if they can provide a more efficient alternative.

interface IEnumerable<T>
{
    int Count()
    {
        int count = 0;
        foreach (var x in this)
            count++;
        return count;
    }
}
interface IList<T> ...
{
    int Count { get; }
    override int IEnumerable<T>.Count() => this.Count;
}


As you can see here, developers who implement IList<T> don’t need to worry about overriding the IEnumerable<T>.Count() method because it will automatically pick up the IList<T>.Count property.

One concern with this proposal is that it bloats the interface. If we add Count to IEnumerable, why not add all of the other IEnumerable extension methods?

Eirenarch writes:

I am kind of surprised you are seriously considering adding Count() to IEnumerable. Isn't this the same mistake as Reset? Not all IEnumerables can be reset and not all of them can be safely counted as some are one time only. Now that I think of it I can't recall ever using Count() on IEnumerable. I only use it in database LINQ calls because I don't want to risk Count() consuming the enumerable or being inefficient. Why encourage more Count()?

DavidArno adds:

Ha ha, that's a really good argument against this proposal. The BCL team have already made a fine old mess of the various collection types. Knowing what they did, I doubt any of them could ever look Barbara Liskov in the eye due to having broken her substitution principle so very thoroughly. The idea of a them being given a feature like this, which would let them wreak yet more havoc, is truly scary!

In a BCL meeting:

"OK guys, we want to add cons support to IEnumerable<T> . Any suggestions?"

"That's easy, Default Interface Methods solve that for us. Just add (T head, IEnumerable<T> tail) Cons() => throw new NotImplementedException(); and job done. Implementers of IEnumerable can then add support at their leisure."

"Awesome. Job done. Thanks all, that concludes this week's meeting".

Do note that LINQ is owned by a separate team and there is no plan to actually migrate LINQ’s functionality into IEnumerable<T>.

This change would also break the layering that extension methods currently offer. Enumerable.Count is currently in System.Core, which is two levels higher than mscorlib. Some may feel that bringing some or all of LINQ into mscorlib may unnecessarily bloat that assembly.

Another criticism is that it isn’t necessary- that we already have a design pattern that allows extension methods to be optionally overridden.

The Overridable Extension Method Pattern

Overridable extension methods rely on interface checking. Ideally would be only one interface that it needs to check for, but for legacy reasons the Enumerable.Count example checks for two interfaces:

public static int Count<TSource>(this IEnumerable<TSource> source) {
    var collectionoft = source as ICollection<TSource>;
    if (collectionoft != null) return collectionoft.Count;
    var collection = source as ICollection;
    if (collection != null) return collection.Count;
    int count = 0;
    using (var e = source.GetEnumerator()) {
        while (e.MoveNext()) count++;
    }
    return count;
}

(Error handling removed for clarity)

The downside of this pattern is that the optional interface may be too broad. For example, a class that wants to override Enumerable.Count needs to implement the entire ICollection<T> interface. If it is a read-only class, that is a lot of NotSupported exceptions to write (again, for historical reasons this checks ICollection<T> instead of the much smaller IReadOnlyCollection<T>).

Default Methods and the Class’s Public API

In order to avoid backwards compatibility issues when adding new methods, default methods are not accessible via the class’s public interface. Consider for example the IEnumerable.Count example about and this class:

class Countable : IEnumerable<int>
{
    public IEnumerator<int> GetEnumerator() {…}
}

Since IEnumerable.Count isn’t overridden, you cannot write this code:

var x = new Countable();
var y = x.Count();

Instead you need to type cast it:

var y = ((IEnumerable<int>)x).Count();

This limits its usefulness for providing default implementations to classes, as they would need to add boilerplate code to expose the interface method on the class’s public API.

Overriding Default Methods with Default Methods

Default methods in an interface can override the default method in another interface. You can see this in our IEnumerable.Count use case.

As with normal methods, you need to explicitly use the override keyword, otherwise the new method will be treated as being unrelated to the other method.

You can also mark an interface method as “override abstract”. Normally the abstract keyword isn’t necessary, as all abstract interface methods are abstract by default.

Resolution Order with Extension Methods vs Default Properties

Zippec raises an import question about what happens when a newly added interface method has the same name as an extension method for that interface:

What is the story of current API upgrade to default methods? I assume they should have higher priority in overload resolution than extension methods? So let's take Count() for example. Will we get it on IEnumerable? If so, will it hide Linq IEnumerable.Count() meaning that recompile against C# with this feature will change the called code? Is it ok? I assume it could be problem for IQueryable.

If this was a problem and we got Count as property in BCL to mitigate this, wouldn't it mean that we can never have any default method (only properties) on already existing BCL interfaces, because it could change meaning of existing custom extension methods?

While rare, some developers do create their own extension method libraries that mirror the ones found in LINQ but behave differently. The ability to swap out one extension library for another would be lost if they were moved into the interface as default methods.

Use Case: INotifyPropertyChanged

Here is another possible use for the new feature that people were considering:

interface INotifyPropertyChanged
{
    event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(args);
    protected void OnPropertyChanged([CallerMemberName] string propertyName) => OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    protected void SetProperty<T>(ref T storage, T value, PropertyChangedEventArgs args)
    {
        if (!EqualityComparer<T>.Default.Equals(storage, value))
        {
            storage = value;
            OnPropertyChanged(args);
        }
    }
    protected void SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName) => SetProperty(ref storage, value, new PropertyChangedEventArgs(propertyName));
}

However, this won’t actually work because the interface doesn’t have a way to raise the event. Interfaces only declare an event’s Add and Remove functionality, not the underlying delegate used to store the list of event handlers.

While not part of the proposal, this could change. The CLR does have a slot for storing an event’s “raise accessor method”, though currently VB is the only major language to use it.

Further Support for the Proposal

HaloFour writes:

This seems like a very ideological argument. There are known problems that the team hasn't been able to address since .NET 1.0 has been released. The standard solutions are and have been long well-known. They often make an absolute mess of an API. IFoo, IFoo2, IFoo3, IFooEx, IFooSpecial, IFooWithBar, etc., etc., etc. Extension methods go pretty far in addressing those concerns but they lack specialization outside of what can be discerned/dispatched explicitly in the extension method.

Default implementations solve these issues nicely. They allowed the Java team to round out several of their long-existing interfaces with additional helper methods, several of which do get specialized by various implementations, e.g. Map#computeIfPresent.

Other Criticisms of the Proposal

HerpDerpImARedditor writes:

Oh, this is going to lead to some chronic spaghetti code. Forgive me if I'm missing something, but what does this pattern solve that couldn't have been solved at the implementation layer? It almost seems as though this murks the gorgeous distinction between Interface and Concrete Implementation. Is the IDE going to be deadly specific about where the run-time implementation will come from? Can't see that working with IoC.

I love .NET, came from a Classic ASP/VB background straight to .NET 1. This is one of the first language spec additions I don't agree with (I did wince a little when 'dynamic' came on the scene but saw its use case). I see others saying they'll ignore that this feature exists; my concern is others' code I might later come across won't ignore [this feature].

Ah well, I guess we'll have to see it in action before any genuine judgment is passed.

Canthros writes:

I think this makes me sad.

The github discussion indicates that this was initiated over dissatisfaction with the mess that is the implementation of LINQ's various extension methods, particularly the type-sniffing that has to be done to provide for optimized implementations. Generalizing that to a language feature may save the implementers of .NET Core some significant effort. At the expense of saddling the language with a muddled distinction between interfaces and abstract classes, and a feature that whiffs pretty strongly of problems downstream.

It feels like this is substantially mitigated by the Shapes proposal, but I don't have time to really think through all this stuff, right now.

Rate this Article

Relevance
Style

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Tell us what you think

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Not sure about this. by Luis Espinal

Err, not sure me like it. As a Java (and a bit of a C#) developer, I've always felt that Java "default" methods are a post-facto work-around to limitations to the original idea behind interfaces (as implemented by Java.)

I don't think this should have been the approach to C#. Seriously, this would have been better served with Mixins or full-blown multiple inheritance as in C++ or Python.

Re: Not sure about this. by Jonathan Allen

Check out the Shapes proposal. I think you'll be happier with it.

www.infoq.com/news/2017/04/DotNet-Type-Classes

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

2 Discuss
BT