Multiple Inheritance

.NET does not support multiple inheritance (MI). It supports interface multiple inheritance only, which cannot replace real multiple inheritance class hierarchies.

There are basically two ways of simulating MI: one is, in short, to create a parallel interface hierarchy that mirrors exactly the class structure of the application and to wrap all methods into static methods; the second technique is to use delegate instances of the second base classes and generate instance wrappers.

The first method is good for compilers and generation of IL code because the kind of source code that needs to be generated is very convoluted and hard to understand and maintain. The second method is the most readable and it is the same approach that a developer would take when writing code and there is a shared class that is not part of the single-inheritance class hierarchy.

To explain simply: all first base-classes are generated normally since .NET fully support single-inheritance, all second-base classes are generated as an internal instance of the class, then all fields and methods are generated in the derived class as wrappers that redirect the call to the second-base instance. The end result is that the derived class contains all the "inherited" fields and functions but the implementation is in one single place: the second-base class.

For example, the class hierarchy shown below:

Is generated as follows:

public class CBase1 : SalFunctionalClass
{
}
 
public class CBase2 : SalFunctionalClass
{
  public CBase2(object derived)
  {
    this._derived = derived;
  }
}
 
public class CDerived : CBase1
{
  private CBase2 _CBase2 = null;
  public CDerived()
  {
    this._CBase2 = new CBase2(this);
  }
}

As you can see, the derived class contains an instance of the second-base class CBase2, and the instance of CBase2 also references the derived class by saving the reference to the special internal member _derived.

Wrappers

All functions and variables inherited from second-base classes are generated as wrappers in the derived class. Variables are generated as properties. The end result is that a derived class inheriting second-base classes looks exactly as if .NET supported multiple inheritance.

The implementation stays in one place and there is no code duplication. The wrappers are "plumbing" code that glues together the class hierarchy that is not supported in .NET.

See the example below:

Functional Class: CBase1
 Instance Variables:
         String: sName
 Functions:
         Function: Copy
 
Functional Class: CBase2
 Instance Variables:
         Sql Handle: hSql
 Functions:
         Function: Save
 
Functional Class: CDerived
 Derived From:
         CBase1
         CBase2

Is translated to:

public class CBase1 : SalFunctionalClass
{
  public SalString sName = "";
  public SalNumber Copy() {...}
}
 
public class CBase2 : SalFunctionalClass
{
  public SalSqlHandle hSql = new SalSqlHandle();
  public SalNumber Save() {...}
}
 
public class CDerived : CBase1
{
  internal CBase2 _CBase2 = new CBase2();
 
  public SalSqlHandle hSql
  {
     get { return _CBase2.hSql; }
     set { _CBase2.hSql = value; }
  }
 
 public SalNumber Save()
 {
     return _CBase2.Save();
 }
}

CDerived is directly derived from CBase1.

Everything that CDerived would inherit from CBase2 is generated as wrappers that delegate the implementation to the second-base class instance.

In addition to the wrappers, we also generate optimized casting operators that allow MI (simulated) class structures to cast to/from second-base classes.

Late Bind Calls

Late bind calls in a Multiple Inheritance structure may become a bit complicated when the caller is part of a second-base class branch.

As described in the Late Bind calls section, when a function is called in a late bind expression we generate a virtual method with the same name and a predefined prefix. This technique takes advantage of standard virtual methods in a normal single inheritance hierarchy.

However, when the caller is in a second-base class, it is impossible for a derived class that is not part of the same inheritance tree to override the virtual method. See class inheritance structure below:

CDerived.TestImpl() cannot override CSecondBase.TestImpl() because CSecondBase is not really a base class.

Therefore, the standard late-bind code doesn't work in this case. For this reason, when generating late bind calls we also generate late-bind interfaces containing the declarations of all late-bind methods.

For the structure illustrated above, Ice Porter generates also an interface (as an inner member) named CSecondBaseLateBind. All the classes that implement any late-bind method from any base class (including first base and second base classes) also declare that they implement the LateBind interface exposed by the base class containing the original late-bind methods.

Since we can also take advantage of real multiple inheritance (interfaces support multiple inheritance in .NET), the late-bind methods are generated to be able to work in any configuration: single or multiple inheritance.

A typical late-bind method looks like this:

public virtual SalNumber __TestImpl()
{
  LateBind lateBind = this._derived as lateBind;
  if (lateBind == null)
    return this.TestImpl();
  else
    return lateBind.__TestImpl();
}

When the method is in a second-base class, the _derived field references the in instance of the derived class.

If the derived class implements the late-bind interface exposed by the second-base class, then the cast returns a valid implementation and the call is correctly routed down the inheritance tree to the late-bind wrapper (notice that the call is to the __TestImpl() method).

When the method is in a single-inheritance base class, the call is simply redirected to the local method implementation, unless the method has been implemented down the inheritance tree.

The class named CDerived in this sample, is declared like this:

public class CDerived : CBase, CSecondBase.LateBind
{
}

And the second-base class CSecondBase is declared as:

public class CSecondBase
{
  public interface LateBind
  {
    SalNumber __TestImpl();
  }
}

Basically, the class that first declares a late-bind method also declares an interface that define all the late-bind methods.

Any class that implements any of the late-bind methods of another class, must implement that class' late bind interface.

While multiple inheritance is complex, this is the cleanest, simplest, most efficient and standard way to route virtual calls down a second-base inheritance tree.

Visual Multiple Inheritance

Visual Multiple Inheritance has some additional problems that need to be considered.

The problems are related to two areas: Visual Studio Form Designer and Control's Properties and Functions. Because of the way Ice Porter generates multiple inheritance visual classes, the designer cannot design (and doesn't visualize) controls that are inherited from a second-base class.

Since the Control class is not aware of multiple inheritance constructs, members that affect the visual appearance of the control in a second-base class do not affect the control's instance.

InitializeComponentEx

Visual Studio's Form Designer basically works by instantiating the base class of the form being designed and then interpreting the InitializeComponent() method in the form being designed. The designer expects all the controls to be declared in the class being designed, or inherited from a base class. It doesn't allow for controls to be "imported" from another container instance.

Team Developer, on the contrary, supports multiple inheritance also for form containers. A form class can inherit controls from multiple base classes. With the obvious advantage that a container class can be reused in different class inheritance trees and share it's controls, events and functionality.

To support this kind of constructs, Ice Porter generates an additional method named InitializeComponentEx(), where all the controls that are inherited from a second-base class are in fact "imported" into the form.

This approach works very well (and there is really nothing better), but cannot be supported by Visual Studio Form Designer. The good news is that if the visual class with multiple bases is further derived, the derived class looks like single inheritance to the designer and can also design controls inherited from a second-base class.

The figure below shows a simple visual multiple-inheritance tree:

In this case, CFormBase1, CFormBase2 and CForm can all be designed in the Form Designer without any problems and all the controls are visible and can be configured. CDerived can also be designed, but df2 (imported) is not visible and is not recognized by the designer.

Using properties and methods from a second-base

A second-base class references the derived class using the _derived member. This means that when the class is a control, the actual control seen on screen is the last derived class.

All PPJ Framework methods are aware of this construct, therefore when using any visual function, or converting SalWindowHandle to Control, the code operates on the last _derived instance and everything works well also for visual MI.

However, the native Control class is obviously not aware of this type of structure, therefore if you use Control's members in new code in a multiple inheritance structure, you may not get the desired behavior.

Last updated