Type Inheritance Recursive
ID |
csharp.type_inheritance_recursive |
Severity |
critical |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Code Smell |
Language |
CSharp |
Tags |
code_smell, generics, inheritance, type-loading |
Description
Reports a generic type whose base type is instantiated with a growing instantiation of the
declaring type, as in class C2<T> : C1<C2<C2<T>>>. Such a type compiles, but the runtime cannot
lay it out and throws a TypeLoadException the first time anything touches it.
Rationale
Laying out C2<T> requires laying out its base C1<C2<C2<T>>>, which requires laying out
C2<C2<T>>, whose base is C1<C2<C2<C2<T>>>>, and so on. The chain never closes, so the type
loader gives up. Every individual type argument is well formed, which is why the compiler accepts
the declaration and the failure lands at run time, at whatever point the type is first
instantiated or reflected over — usually far from where it was declared.
What the rule looks for is an occurrence of the declaring type anywhere in the base type’s type-argument tree that is closed over something other than the declaring type’s own bare type parameters. Passing the parameters straight through keeps the instantiation fixed, which is the ordinary self-referential-generic idiom and never diverges. Substituting a constructed type — a wrapper, another instantiation of the declaring type — is what makes every level larger than the last, and that is the reported case, however deeply the occurrence is nested.
public class Registry<T>
{
}
public class Wrapper<T>
{
}
public class ExpandingRegistry<T>
: Registry<ExpandingRegistry<ExpandingRegistry<T>>> // FLAW — layout never terminates
{
}
public class GrowingNode<T> : Registry<GrowingNode<Wrapper<T>>> // FLAW — T is wrapped again at each level
{
}
public class DeepNode<T>
: Registry<Wrapper<DeepNode<Wrapper<T>>>> // FLAW — the growing occurrence can be nested
{
}
public class Node<T> : Registry<Node<T>> // OK — T is passed through unchanged
{
}
public class Money : IComparable<Money> // OK — the standard self-referential idiom
{
public int CompareTo(Money other) => 0;
}
public class Pair<T> : Registry<Wrapper<Pair<T>>> // OK — nested, but still closed over T
{
}
public class Handler<T> : Registry<T> // OK — no self-reference at all
{
}
Remediation
Decide what the base type was meant to be closed over. In nearly every case the intent was the
non-growing self-reference Registry<ExpandingRegistry<T>>, so drop the extra level of nesting. If
the extra level was deliberate, the shape cannot be expressed through inheritance: hold the nested
instantiation in a field or a property instead of inheriting from it, or introduce a non-generic
base type or interface that both levels share.