A plea for full multiple inheritance support in .NET
Disclaimer
Although I find it absurd to put in a disclaimer, I know for a fact that talking about Multiple Inheritance (MI) is risky,
because it is one of those subjects which can cause irrational reactions, resulting in flame-fests. Discussions about
MI should be theoretical and thus based on theory, not about one of the many different implementations. I'm discussing the theoretical benefits of MI and am not discussing a real implementation like C++'s MI. I've done some research on this subject prior to writing this article and have read many discussions about MI and .NET and also discussions about MI and languages like Eiffel, so I'm aware of the disadvantages of MI, I also am aware of the reasons (of the ones that are publicly stated) why .NET doesn't have MI at the moment. Still I think .NET contains enough functionality and implementations of classes and interfaces which require MI to be fully utilized. MI is a complex concept, however so is the concept of generics. MI can result in unmaintainable code, but so can Single Inheritance (SI) (any language can be used to write unmaintainable, bad code). You can work around MI in most situations, but you can also work around the lack of polymorphism (in C for example, by using function pointers) or even OO. Still it's seen as an advantage to have OO, polymorphism and (soon) generics.
Multiple Inheritance types
There are two types of MI: multiple type/interface inheritance and multiple implementation inheritance. .NET supports only multiple type/interface inheritance (you can derive an interface from multiple interfaces, you can implement in your class multiple interfaces (which makes your class derive from those types). There is no support for multiple implementation inheritance, so when your class Foo inherits from a base class Bar and you want to implement an interface IBlah that is already implemented on another class, say Blah, you are obliged to re-implement that interface IBlah in your class in some form, you can't inherit that implementation from Blah as well, because you already inherit Foo from Bar. This is called redundancy of implementation. It should be noted that every MI class hierarchy can be transformed, in theory, into an SI hierarchy, by using 'redundancy of implementation' and multiple interface inheritance (The Eiffel compiler for .NET does this for example).
It's however not always the case in practise though.
Working around MI in a Single Inheritance (SI) environment
Because .NET doesn't support multiple implementation inheritance, it has to use tricks every SI environment has to use, to mimic multiple implementation inheritance. There are several ways to overcome the lack of MI, and each of them requires extra work. This extra work wouldn't have been necessary if .NET would have supported full MI. Metaphor: .NET will soon support generics, which will save you a lot of work when dealing with data-structures which should supply behaviour on a variety of types without casting, and thus will require less work on your part.
To illustrate the ways around MI in .NET, we use three classes, A, B and C. A implements IA, B implements IB and C should have the functionality of IC, which is the interface derived from IA and IB (IA, IB and IC also can be seen as 'the class as interface': every class has an interface, made up by it's publicly available methods, fields and properties, so they shouldn't necessarily be seen as physically defined interfaces). To achieve this, we can do the following things:
- Inherit IC from IA and IB, inherit C from A or B and re-implement the interface of the class C doesn't inherit from (IA or IB) in full in C.
- Inherit IC from IA and IB, inherit C from A or B and aggregate an instance of the class C doesn't inherit from inside C, where the interface of that class is implemented in C as a wrapper around the aggregated object.
The second option is the least amount of extra work, but still requires that extra work, thus extra time and effort. Because it's a fact that .NET is SI only, we as .NET developers can only opt for one of these two options and move on. There are however situations where neither of the two will work, or will result in a lot of duplicate code, which results in extra maintenance issues. Most of these situations involve inheritance from an existing .NET framework class. Let's look at such a situation: adding optional serviced component functionality to a class.
When SI doesn't cut it.
To make a class be able to participate in COM+ services, it has to inherit from ServicedComponent, which is a class of the .NET framework. It can't aggregate a ServicedComponent object and mimic its interfaces nor can it re-implement its interfaces, it has to derive from that class. Say, we have a class C and that's a class derived from another class we wrote, A. A derives from System.Object. To make sure C is able to participate in COM+
transactions, we have to make C derive from ServicedComponent, directly or indirectly. In the case of .NET, we don't have another choice but make A inherit from ServicedComponent. Because A derives from System.Object, this is not a problem per se. Another class, B, also derives from A. Because A is a serviced component now, B is also a serviced component all of a sudden. But perhaps we want to marshall B by value using a remoting network, or we want to keep B very slim, without the overhead a serviced component brings on the table. In short: we have to rework our class hierarchy: we have to duplicate A's code into two classes: A and Adupe. Adupe is now becoming the base class for B, so B's clean from serviced component's inheritance. However, this results in two implementations of A's code in our class hierarchy. We can solve this somewhat by creating a class Aaggregate, which contains A's code and which is aggregated in A and Adupe, and using option 2, we can make A look like it is implementing Aaggregate's interface in full. This solves us from the duplicate implementation of A's code, however in return it gives us the maintenance nightmare of supporting the same interface in three classes while there is just one actual implementation.
A total different story is the situation where A doesn't derive from System.Object, but from another framework class, say, CollectionBase. Because this is a theoretical discussion, the class mentioned is just an illustration. This creates a problem: ServicedComponent can't be aggregated nor re-implemented, so CollectionBase should be aggregated or re-implemented. Because CollectionBase is an abstract class, we first have to derive a new class from CollectionBase which we then can use to be the aggregated class inside A. Another option is the drop CollectionBase and re-implement its functionality in A. Although this maybe seem like a reasonable option, it's silly.
Why would you re-implement an existing abstract base class which is solely there for the purpose to make life easier for you?
This problem occurs in almost all situations where a class C inherits from a base class A and wants to add behaviour defined by an interface which is already implemented in another class: you then have to choose which base class requires the least amount of duplicate code, and aggregate/re-implement the class you didn't pick to inherit C from. You can't design the extra code away, other than merging classes, simplifying a hierarchy, or opt for full MI. In the case of behaviour implementation and inheritance from .NET classes, you can't merge classes or simplify your hierarchy, because the .NET classes are not changeable. Because .NET also doesn't support
MI in full, you have just one choice: duplicate code.
Caveats
Full MI support has caveats. Most of them are however not the problem of the .NET framework user: they are on the plate of the .NET framework developers: implementing MI is hard, it's complex and there are other problems as well: because of the multi-language nature, you can open up MI to languages which don't support it. This is the case when you inherit C from A and B and C, together with A and B are part of a class library. This class library is then referenced in a project using a language which doesn't support MI. C is then available as a type, however C inherits from both A and B. In theory, the non-MI supporting language now supports MI because it can inherit
from an MI type. Another problem is the way how MI should be implemented. Eiffel has a very elegant MI implementation, C++ has another, more raw implementation of MI. Is an implementation of MI ala Eiffel in the CLR enough to support MI in C++ or vice versa? One of the classical problems of MI is the 'diamond' inheritance tree: base class A is inherited by B and C. D inherits from both B and C. What if A contains an abstract method which is implemented differently by B and C, which implementation is then exposed by D? There are solutions for this problem, which is by the way also a problem already possible in .NET, because it is interface related, however these solutions ask for different implementations and which one to pick? A good reason to drop MI? Not at all, since you can run into this (naming) problem today too when aggregating objects for example.
Eiffel is available for .NET today, with generics, full MI and a set of advanced concepts like design by contract. It might seem like a good alternative, but Eiffel is a language with a relative small userbase and there are problems mixing MI constructs written in Eiffel with regular C# code by referencing a library written in Eiffel for .NET.
It shows however that the choices how to implement MI is not solely a responsibility for the CLR team, but also for the language designers. After all, generics is also a technique which is implemented in various ways in a wide range of languages. Still, the CLR implements one way of generics, so it is possible to choose one implementation for the CLR, why can't it be done for MI as well?
Conclusion
Developers run into various problems when they want to build software on top of the framework classes provided. Problems which are the result of the absence of MI in the framework or at least the absence of MI support in one of the major .NET languages (C#, J#, VB.NET and C++). In a lot of cases, the developer can work around the problems by implementing redundant code, code which is already available to the developer by the presence of an (abstract) base class in a class library, for example the .NET framework. Avoiding the re-implementation of functionality that is already available is one of the reasons why Object Oriented (OO) development is preferred in a lot of situations over procedural development (due to its polymorphic nature. Re-use of code is of course available in procedural languages as well, though not in the way OO offers it). Imagine that you would have MI in the framework. All interfaces with 'able' at the end of their name could have a basic, abstract implementation in the framework which could be used as a base class for your own classes and using the strategy pattern, you could add just the functionality required to make the abstract code work for your particular situation. That way, you don't write extra code at all, better yet, you
write less code than when you would have implemented the interface from scratch, because a lot of interfaces require plumbing code which is always the same and which could be perfectly implemented in the abstract base class.
To make .NET the platform of choice, I can't see any other conclusion than that Microsoft implements some form of multiple implementation inheritance. Make it a choice. Now developers don't have a choice, and in some situations are forced to do a lot of extra work, have to redesign their class hierarchies to work around SI limitations. When it is a choice, people can still opt for a pure SI language, for example C#, or for an MI language like MC++.
Links
Below are a list of links to material worth reading. Most of them are usenet discussions, but very recommended if you are interested in the material.
- http://www.eiffel.com
- Google groups thread (sept 2000, comp.lang.eiffel)
- Google groups thread (sept 2000, microsoft.public.dotnet.languages.csharp)
- Google groups thread (April 1999, comp.object. Highly recommended)
- Google groups thread (June 2002, microsoft.public.dotnet.languages.csharp. Eric Gunnerson explains why .NET doesn't have MI)
A personal note: in one of the threads, I stumbled upon one of my own postings, arguing against MI because it wouldn't be necessary and it would cause a problem with languages which don't support MI but target .NET. It was funny to see how a person can change his way of thinking once you know more about the subject discussed. :)