Generally speaking my understanding was that static objects in classes are constructed when the class is referenced for the first time. However I am experiencing a behavior that I did not expected.
Consider the following class
public abstract class SmartColor<TColor> where TColor : SmartColor<TColor> {
private static readonly ConcurrentDictionary<string, TColor> _items = new();
protected SmartColor(string code, string name) {
Code = code;
Name = name;
//register all colors
foreach (var field in typeof(TColor).GetFields(BindingFlags.Public | BindingFlags.Static)) {
TColor? item = (TColor?)field.GetValue(null);
if (item is not null) {
Register(item);
}
}
}
public string Code { get; }
public string Name { get; }
public static TColor FromCode(string code) {
if (_items.TryGetValue(code, out var result)) {
return result;
}
return null;
}
private void Register(TColor item) {
_items.GetOrAdd(item.Code, item);
}
}
which is in turn inherited from this one
public class WebColor : SmartColor<WebColor> {
public static readonly WebColor White = new WebColor("#ffffff", nameof(White));
public static readonly WebColor Black = new WebColor("#000000", nameof(Black));
protected WebColor(string code, string name) : base(code, name) {
}
}
It produce a NullReferenceException if used like this
public static void Main()
{
WebColor color = WebColor.FromCode("#ffffff");
Console.WriteLine(color.Name);
}
Can I understand why this is happening? Fiddle here
From the documentation for Static Constructors:
A static constructor is used to initialize any static data, or to perform a particular action that needs to be performed only once. It is called automatically before the first instance is created or any static members are referenced.
So let's look at this in detail. For your code to work, the static field constructors for class WebColor
must be called before WebColor.FromCode("#ffffff")
is called.
Has an instance of WebColor
been created? No.
Has any of its static members been referenced? No. The FromCode()
method is not a member of WebColor
- it's a static member of SmartColor<TColor>
, and although static methods of base classes can be called via a derived class, they are not actually members of that derived class.
Therefore the criteria for calling the static initialisers of WebColor
have not been fulfilled, so the White
initialisation has not occured and therefore it has not been added to _items
in SmartColor<T>
.
But do note that _items
itself has been initialised because of the call to WebColor.FromCode()
.
You can fix this in a slightly hacky way by adding a static constructor to SmartColor<TColor>
as follows:
static SmartColor()
{
Type type = typeof(WebColor);
System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(type.TypeHandle);
}
The expresses the fact that the static members of SmartColor<TColor>
have a temporal dependency on the WebColor
type, and it will force the static WebColor
initialisers to run when the SmartColor<TColor>
static initialisers run.
However, as others have pointed out this is a somewhat awkward design, so it might be worth employing a different approach!
Addendum:
You can avoid using a static constructor by using a class such as this:
public sealed class StaticClassInitialiser<T>
{
public StaticClassInitialiser()
{
System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(T).TypeHandle);
}
}
And then create a static instance of that class in the SmartColor<TColor>
class:
static readonly StaticClassInitialiser<WebColor> _ = new();
That might fool SonarQube, but it's hardly an improvement - I include it here only for curiosity.