Inheritance Dispatch
SAL can inherit implementation from multiple base classes. C# has one direct class base. Ice Porter combines real inheritance, contained secondary objects, forwarding members, and generated late-bind interfaces to preserve the original call routes.
- CDerivedFinal application objectActive
- CBase1Real C# base class
- Forwarding memberGenerated property or method
- CBase2 instanceOwned secondary object
- LateBind interface_derived → final object
CDerived inherits directly from CBase1 in C#. Its base state belongs to the same object.
CDerived : CBase1A conceptual sequence, not a live runtime trace. Playback advances every four seconds. The full explanation follows below.
Read the Object Relationships
CDerived ── inherits ──► CBase1
│
├── owns ──────────► _CBase2 instance
│ │
◄──── _derived ────────┘
│
└── implements ───► CBase2.LateBind (when generated)
The first-base portion belongs to the same C# object as CDerived. The secondary-base state lives in the contained _CBase2 instance. Its _derived reference points back to the final application object. These relationships have different purposes; replacing them all with direct inheritance or interfaces would lose part of the behavior.
First-Base Calls
A member inherited through CBase1 follows ordinary C# inheritance. For virtual members, dispatch can reach an override on CDerived. There is no secondary object hop for that first-base state.
Trace: caller → inherited member → selected implementation → caller.
Secondary-Base Calls
A secondary member exposed as CDerived.Save() can be a forwarding method that invokes _CBase2.Save(). Generated properties similarly expose fields owned by the secondary instance.
Trace: caller → CDerived forwarding member → owned CBase2 implementation → forwarding return → caller.
Preserve the identity and lifetime of the owned instance. Creating a fresh secondary object on every call would reset its fields. Copying the implementation into several wrappers risks creating independent copies of state or inconsistent fixes.
Calls Back to a Derived Implementation
A method inside CBase2 cannot use ordinary C# overriding to reach CDerived, because CDerived does not inherit from that secondary class. For generated late-bound calls, the secondary object uses _derived and a generated interface to route to the final implementation.
Trace: secondary method → generated late-bind wrapper → _derived cast to the late-bind interface → final derived implementation. The generated wrapper includes the fallback behavior for a final object that does not supply that interface implementation.
Calling the secondary implementation directly during a refactor can bypass this route. Conversely, changing a qualified base call to a virtual/late-bound call can introduce an override that the original code deliberately avoided. Preserve the distinction between a normal call, a qualified base call, and a late-bound call.
A Focused Regression Test
Create a small hierarchy with a first-base field, a secondary-base field, and a derived implementation of a method invoked through a late-bound expression. Give each implementation a distinct trace label.
- Read and write the first-base field through the derived object; confirm one persistent value.
- Read and write the secondary field through its generated property; confirm the owned object sees the same value.
- Call the secondary method through the derived wrapper and check its return value and state changes.
- Trigger the late-bound expression from within the secondary branch; confirm it selects the derived implementation where the source does.
- Exercise any qualified base calls and casts used by the source; verify that they retain their original meaning.
For visual multiple inheritance, add creation/destruction and control-state checks. The simplified object diagram does not describe every designer-generated visual relationship.
See Multiple Inheritance for generated code examples and Late Bind Calls for the translation details.