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. :)