Some possibly non-obvious notes about generics #3286
Replies: 3 comments 2 replies
-
The last two lines will probably give you a compiler warning or suggestion, too, because they're actually accessing BaseClassWithStatic.AString, which is why it works in the first place. |
Beta Was this translation helpful? Give feedback.
-
It has a public static string. That isn't type-safe for generics: public static string NoBuilderError = "ERROR: TreeBuilder Not Set"; That exists for every constructed concrete type of There is one exception to this behavior, though, which I didn't cover before because it's pretty specific. If there exists an identity conversion between the type of the type parameter used to construct two different closed constructed types of the same open generic, those constructed types will share statics. WTF does that even mean? OK. Say you have something like that public class SomeConsumingType<T> where T : class
{
TreeView<T> treeA = new();
TreeView<object> treeB = new();
TreeView<List<object> treeC = new();
} How many constructed types are there, and therefore how many incarnations of that static string are there? Expand for answer:It's 2. T is equivalent to object, in this case, because the constraint in So, And! There's What if that user class is instantiated with a T of The point is that, depending on the type parameter supplied for the user's class, there could be anywhere from 1 to 3 constructed types, just at that level, at run-time. It's unbounded, though, since T itself could be a generic type. So, there are from 1 to n instances of that static, where n is the number of possible combinations of type arguments that go into creating an actual closed constructed type. The main thing that class needs to eliminate that problem is simply a non-generic abstract base class to hold the static. Or just eliminate the static. If the reason for that was for globalization, there are plenty of other ways to handle that which wouldn't require changing the type hierarchy. |
Beta Was this translation helpful? Give feedback.
-
You crack me up. In a very good way. Thanks again for your essays like this! |
Beta Was this translation helpful? Give feedback.
-
Something we need to be extremely careful about when writing any generic type (as in the type itself takes a type parameter, not just individual methods in it) is that static members and nested types behave in ways that might not be immediately obvious.
If a class is generic, it represents essentially (but not literally) an abstract type that feels like a concrete type in code because that's the point of the feature. But the concrete types do not exist until runtime, when they are first encountered. Those types are entire types, all by themselves. Thus, static members exist per concrete type, meaning there will be one instance of what you thought was a singleton for every unique runtime concrete type created. The compiler will also warn you about it, if you do this. Take heed of that warning.
Nested classes inside a generic class are, themselves, also generic, even if you don't put a type parameter on them. They inherit the type parameter of their enclosing type. Further, you cannot instantiate a nested type of a generic type until the enclosing type has been initialized using the same type parameter, which is an error you won't see until runtime, and have a non-negligible probability of behaving fine in Debug builds, but breaking at run-time for Release builds. So add all that to the pile of reasons to NOT use nested types.
One other thing to watch out for with generics, but in a different context, is generic delegates, especially if they are public and especially especially if they are used for any public events.
The issue there isn't as dangerous, and can usually be figured out by static analysis and is a compiler error. From one of my other essays, I mentioned how delegates wrap classes that you don't see as the programmer. Well, when you stick generics on top of that, it makes things more complex. To make that not a nightmare to- or perhaps even possible to compile, there are variance restrictions placed on type parameters of components of generic delegates.
The safest and what will always compile, of course, is to define them as invariant. But, if you need or want to support covariance or contravariance, you'll have these restrictions on those delegates:
In general, you can make generic return type parameters of delegates covariant but not contravariant, and you can make generic type parameters for method parameters contravariant but not covariant.
These are caught by the compiler, but can be confusing. For example, a generic event or raw delegate can only be chained with exactly the same concrete implementation at run-time. So if you define a variant generic event handler, there is only one way to compile it. Regardless of whether the type parameter is invariant or contravariant, the event can only exist one way and only one type parameter is ever usable with it, once it is created. For that reason, it's usually a better idea to just use interfaces when you want an event to support extensibility. And that's better for unit testing, anyway.
So, you can define generic delegates if you want, but they really should be fully closed by the time they are exposed on the public API surface.
What's the right thing to do?
With all that said, if there are any generic View types or other types that are generic but not marked sealed and which have static members defined in that type, that's bad news. If you want to expose a generic type and want it to have statics, define a base type that is not generic to hold the statics and then make the generic type inherit from that type.
Here's code showing both the right and wrong way of doing it and what happens when you do each:
Beta Was this translation helpful? Give feedback.
All reactions