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.