Skip to main content

Multiple Inheritance

The inheritance dispatch walkthrough traces first-base, secondary-base, and late-bound calls through the generated object relationships.

Screenshots and diagrams on this page illustrate the historical desktop tools or conversion workflow; installed versions may look different.

C# classes have one direct base class, while a SAL class can inherit implementation from several classes. Implementing several interfaces does not by itself reproduce the state and behavior of those base classes.

Ice Porter uses inheritance for the first base and composition for secondary bases. The derived class holds a secondary-base instance and exposes forwarding methods and properties. This keeps the secondary implementation in one place while preserving the member access expected by converted code.

The secondary object also needs to reach the final derived instance for some virtual and visual operations. The generated _derived reference and late-bind interfaces provide that connection; they are part of the compatibility behavior, not redundant fields.

For example, the class hierarchy shown below:

Simple multiple inheritance

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 require additional dispatch 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:

Late-bind call from a second-base class.

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

The generated interface routes virtual calls through the containing derived instance. Preserve this routing when refactoring; delegating directly to the secondary object can bypass an override.

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.

The designer cannot interpret these imported controls in every generated class. A further derived class may expose them through ordinary visual inheritance. Check the actual designer result and runtime layout before changing the generated initialization sequence.

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

Child controls inheritance

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.

Refactoring Checks​

The examples are structural excerpts, not complete compilable classes. Before simplifying a wrapper, identify which object owns its state and whether a derived class overrides the call. Test calls through the first base, secondary base, and late-bind interface, including disposal and visual-control access. Composition preserves behavior here; it does not make the secondary object the same CLR instance as the containing form.