Anders Forsgren
Anders Forsgren

Reputation: 11101

Should I avoid nested types in generic types?

If I implement a generic type that risks being instantiated with lots of type params, should I avoid (for JIT performance/code size etc. reasons) having many nested non-generic types?

Example:

public class MyGenericType<TKey, TValue>
{
    private struct IndexThing
    {
        int row; int col;
    }

    private struct SomeOtherHelper
    {
        ..
    }

    private struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>> { }

}

The alternative which works equally well is to have the non-generic types outside, but then they pollute the namespace. Is there a best practice?

public class MyGenericType<TKey, TValue>
{
    private struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>> { }
}

internal struct IndexThingForMyGenericType
{
   int row; int col;
}

internal struct SomeOtherHelper
{
   ...
}

Upvotes: 7

Views: 737

Answers (2)

Sriram Sakthivel
Sriram Sakthivel

Reputation: 73442

In C# every nested type of a generic type is inherently generic. Compiler will make the nested type as generic too(without our knowledge). Refer this article for more info.

Although generics shares JIT code for reference types as explained in this interview, it has some overhead compared to non generic classes. Each value type gets its own JIT code.

  • If the type is used only in the generic class --It makes more sense to be a private nested type.

  • If the type is used elsewhere, then it should ideally be a non nested type (as internal).

That said, if your nested types isn't using the Type Parameter T in this case, it doesn't needs to be a nested type of generic type and thus it becoming a generic type as well.

Most of the time it shouldn't matter but if you're concerned of many types created at runtime, you can refactor your generic type to have a non generic base class which acts as a container type for your nested types and expose the nested types as protected.

public class NonGenericBase
{
    protected struct IndexThing
    {
        int row; int col;
    }

    protected struct SomeOtherHelper
    {
        ..
    }
}

public class MyGenericType<TKey, TValue> : NonGenericBase
{
    private struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>> { }

}

This way you're sharing the same nested types. No runtime overhead. No separate types for each type parameter. Now typeof(MyGenericType<int, string>.SomeOtherHelper) will be equal to typeof(MyGenericType<long, bool>.SomeOtherHelper).

Upvotes: 6

Kapol
Kapol

Reputation: 6463

Although this doesn't fully answer the question, note that for reference types the code is shared, because internally it's all about pointers. So you don't have to worry about bloating the code base. Quote from this answer (Anders Hejlsberg is the quotee).

Now, what we then do is for all type instantiations that are value types—such as List<int>, List<long>, List<double>, List<float> — we create a unique copy of the executable native code. So List<int> gets its own code. List<long> gets its own code. List<float> gets its own code. For all reference types we share the code, because they are representationally identical. It's just pointers.

Upvotes: 1

Related Questions