7

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
{
   ...
}
Anders Forsgren
  • 10,827
  • 4
  • 40
  • 77
  • As always never optimize without ever hitting bottlenecks. Just code in a way that makes most sense, mostly it wont be a problem. Regarding the question, I dont know but you can see a lot of nested private classes in BCL so I wouldnt mind going that route, from a design point of view. The second one as you say can be confusing – nawfal Mar 12 '16 at 09:59
  • 1
    This code is not semantically the same. In your first example the `SomeOtherHelper` isn't a single type as it depends on the generic parameters. For example, `typeof(MyGenericType.SomeOtherHelper)` is not the same as `typeof(MyGenericType.SomeOtherHelper)`. – Enigmativity Mar 12 '16 at 10:00
  • If your problem is to have the classes in the namespace because you want it cleaner, then add it to a sub-namespace, per example if your namespace is MyNamespace then add then to MyNamespace.AccesoryNamespace. – Gusman Mar 12 '16 at 10:17
  • @Enigmativity I realize they become different types but I don't *need* them to be different (Unless they have static fields or I use e.g. typeof(..) in what way can they be observably different?). I'd want to nest them because they are *only* used inside my type. But I wouldn't want to pay a perf cost. – Anders Forsgren Mar 12 '16 at 10:21
  • @Gusman I guess what I'm thinking is that it is a smell to have a type discoverable anywhere that no one outside my class should be able to use. It's a tiny smell, so I'd be happy to pay for it if there was a tiny benefit in performance. – Anders Forsgren Mar 12 '16 at 10:23
  • @AndersForsgren: It took me a while, but I've come to accept that the distinction between `private` and `internal` is quite minor. Clean design is only really important when it affects the public API of your library. This is the same rationale as why a `protected AND internal` modifier does not exist – just make the member `internal` and *document* that it's only to be used from derived classes. – Douglas Mar 12 '16 at 10:25
  • If it's internal then it can only be used on the declaring assembly, if it's in a bigger project where more people is working and that's your concern you can always create a sepparated DLL containing those types, then nobody else but your assembly would be capable of using them. – Gusman Mar 12 '16 at 10:26

2 Answers2

6

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).

Sriram Sakthivel
  • 72,067
  • 7
  • 111
  • 189
  • I think non-generic base class might be the way to go. Only bad thing is if it isn't actually useful, it still has to be (at least as) public as my generic type, so now I polluted the public api and not just the internal one :( I still think this is the least bad solution though. Thanks – Anders Forsgren Mar 12 '16 at 11:56
1

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.

Community
  • 1
  • 1
Kapol
  • 6,383
  • 3
  • 21
  • 46
  • Yes, but it isn't even shared for value types of the same size, so `MyGenericType.IndexThing` afaik. won't re-use `MyGenericType.IndexThing` etc. With 2+ type parameters I fear a combinatorial explosion even though all reference instances share code! – Anders Forsgren Mar 12 '16 at 10:19
  • @AndersForsgren Yes, I know. That's why I said that it's shared for *reference* types. – Kapol Mar 12 '16 at 10:22