Static Members in Generic C# Classes
In C#, static members inside generic classes are not shared across all instances of the open generic type; instead, they are unique to each closed generic type. When a generic class is supplied with different type arguments, the Common Language Runtime (CLR) treats each unique combination as a distinct type at runtime. Consequently, each specialized type maintains its own independent set of static fields, properties, methods, and constructors.
How the Runtime Treats Generic Static Members
When you define a generic class like
Repository<T>, the CLR does not allocate static
memory for the unconstructed type Repository<>.
Memory for static members is only allocated once the generic class is
instantiated or referenced with a specific type argument, such as
Repository<int> or
Repository<string>.
Because Repository<int> and
Repository<string> are distinct runtime types, their
static fields do not share state. Modifying a static variable in
Repository<int> has zero effect on the static
variable in Repository<string>.
Code Example
Consider the following implementation of a generic counter:
public class ItemTracker<T>
{
public static int Count = 0;
public static void Increment()
{
Count++;
}
}If you modify and access the static fields across different type arguments:
ItemTracker<int>.Increment();
ItemTracker<int>.Increment();
ItemTracker<string>.Increment();
Console.WriteLine(ItemTracker<int>.Count); // Outputs: 2
Console.WriteLine(ItemTracker<string>.Count); // Outputs: 1
Console.WriteLine(ItemTracker<double>.Count); // Outputs: 0Each constructed type has its own isolated memory location for
Count.
Static Constructors in Generic Classes
Static constructors also run independently for each closed type. A static constructor inside a generic class executes once per closed constructed type, precisely before the first instance of that specific type is created or any of its static members are accessed.
public class Logger<T>
{
static Logger()
{
Console.WriteLine($"Initialized for: {typeof(T).Name}");
}
public static void Log() { }
}
// Execution:
Logger<int>.Log(); // Prints: Initialized for: Int32
Logger<int>.Log(); // Does not print (already initialized)
Logger<string>.Log(); // Prints: Initialized for: StringSharing Static State Across All Type Arguments
If your architecture requires a static member to be genuinely shared across all type arguments of a generic class, you must move the static member into a non-generic base class.
// Non-generic base class
public abstract class EntityBase
{
public static int TotalEntitiesCreated { get; protected set; }
}
// Generic derived class
public class Entity<T> : EntityBase
{
public Entity()
{
TotalEntitiesCreated++;
}
}In this pattern, creating an instance of
Entity<int> and an instance of
Entity<string> will both increment the single, shared
TotalEntitiesCreated static field defined on
EntityBase.