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.