Static Field Init Order
ID |
csharp.static_field_init_order |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Code Smell |
Language |
CSharp |
Tags |
code_smell, field, initialization, surprising-behaviour |
Description
Reports a static field whose initializer reads another static field of the same type that is
declared below it. A reference to a field declared above is correct and is not reported.
Rationale
Static field initializers run in textual declaration order, once, before the static constructor. A
field that reads one declared further down does not get the value that initializer will eventually
produce — it reads the storage as it stands at that moment, which is the zero value of the type:
0, false, null.
The result is stable and silent. There is no exception and no warning; the field simply holds a plausible default forever, and every consumer of it is quietly wrong. It is also fragile in the opposite direction: a file where the declarations happen to be in a working order changes behaviour the day someone sorts the members alphabetically or moves a block for readability, with a diff that looks like pure formatting.
Constants are not affected, because the compiler substitutes their value at each use rather than initializing storage in order.
A reference inside a function body written in the initializer — a lambda, an anonymous method — is
not reported either. The body is stored, not run, so it does not read anything during static
initialization, and by the time it is invoked every field of the type has its final value. A
Func<T> or a Lazy<T> that names a field declared below it is correct code.
using System;
public class Limits
{
public static int Ceiling = Step * 10; // FLAW, Step is still 0 here
public static int Step = 5;
public static string Label = Prefix + "-x"; // FLAW, Prefix is still null here
public static string Prefix = "limit";
public static int Floor = 1;
public static int Start = Floor; // OK, Floor is already initialized
public const int Scale = Factor; // OK, constants have no initialization order
public const int Factor = 4;
public static readonly Func<int> Now = () => Cap; // OK, the body runs after initialization
public static int Cap = 99;
}
Remediation
Move the declaration that is read above the declaration that reads it, which is enough whenever the
dependency is acyclic. When the order is hard to keep correct by reading — several derived values, or
a value that needs a loop or a try — assign the fields in a static constructor instead, where the
statements are executed in the order they are written and the dependency is explicit.