C# (and any language with similar type requirements) does not allow to make input of an overridden implementation narrower (to exclude some types that base class supports as valid parameters) and output wider (to return values that no longer compatible with return of the base class method) - this will break inheritance expectations - LSP.
Your code wants to limit types your derived class take as argument (as "in" parameter of action delegate) to just subset of what base class supports - caller of that method can't guarantee that only subset of types will be used there. In particular someClassDerivedFromBase.Do(new ChangeSet());
should work for all derived types - the way you setting up the Node
type would have no way to handle parameter of ChangeSet
or any other type that is not derived from NodeChangeSet
and the missing part of the code would need to be somehow created. While some imaginary compiler could come up with solution to throw something like "NotSupportedException" in this particular case inventing desired behavior is not something compilers usually do.
So your options:
- respect the base class method signature - override the method as-is (
Action<ChangeSet>
) and do necessary type checking at run-time
- change base class to actually take real type of the change and use that in the
abstract
method (note that this will make Base<SomeChangeSet>
and Base<OtherChangeSet>
to no longer be related via inheritance and hence can't be used interchangeably): Base<TChangeSet> where TChangeSet:ChangeSet
The code that could work with variance is either narrowing return type ("out") in the derived type:
protected abstract Func<ChangeSet, BaseResult> Do { get; }
// in derived you can return narrower results as it will be always BaseResult
protected override Func<ChangeSet, DerivedResult> Do { get;}
or widening argument types ("in"):
protected abstract Action<DerivedChangeSet> Do { get; }
// in derived accept wider set of types for the action:
protected override Action<BaseChangeSet> Do { get; }
Node
is necessarily aware of the ChangeSet because it inherits anAction<ChangeSet>
property from its base. There would be nothing inherently wrong with leaving that type as it is, and maybe adding a new property if access to the more specific NodeChangeSet type is required. Alternatively, you might have to do something likeclass Base<CS> where CS: ChangeSet { ... }
but that is likely to make other stuff more difficult. Hmm, but since your property is an Action I think only this second approach would be correct – your code as written is unsound and can never type-check!Action<NodeChangeSet>
is not a subtype ofAction<ChangeSet>
since I wouldn't be able to invoke the action with SomeOtherChangeSet as argument. You should think carefully about whether you want to use interfaces for OOP inheritance (cannot work here as you expect), or interfaces as generic type constraints.Node
is a standalone class with no inheritance, all the parameters of the methods can be explicitly typed. The resulting class is pure in the sense that it can 100% accomplish it's design goal. I'm trying to understand why the language cannot "aid" the class in maintaining the design goal and must introduce some "confusion" like type checking.override
is the critical part of this question. I think theAction
indirection is distracting from the main problem. Your example boils down to this simpler case in which the LSP violation is more apparent:class Base { public void Do(ChangeSet s) {} } class Node: Base { public override void Do(NodeChangeSet s) {} }
. The compiler is correct to reject this, regardless of return type covariance.