Use Const For Unchanged Variable

ID

csharp.use_const_for_unchanged_variable

Severity

high

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Code Smell

Language

CSharp

Tags

code_smell, immutability, local-variable, readability

Description

Reports a local variable that is initialised with a compile-time constant and never assigned to again. Marking it const puts that fact in the declaration instead of leaving it to be discovered.

Rationale

A local holding a fixed value is a constant whether or not it says so. The difference is what a reader has to do to find out. With const, the declaration answers the question. Without it, the only way to know the value never changes is to read the rest of the method looking for an assignment — and to repeat that reading every time the method is edited.

const also turns the intent into a rule the compiler enforces. Someone adding an assignment later gets a compile error rather than quietly introducing a second meaning for the name, which is the way these values usually drift: a constant becomes an accumulator, and every earlier use of the name has to be re-read to see whether it still holds.

Both benefits matter most in exactly the methods where this shape appears — long ones, where the declaration and the possible reassignment are pages apart.

The rule only fires where const is legal and the edit is small:

  • The type is one a constant may have: a numeric primitive, bool, char, string or an enum. Everything else — classes, structs, nullable value types, arrays — can be readonly but never const. An enum is recognised by resolving the type against the enums declared in the same file; a type from elsewhere is left alone rather than assumed to be one.

  • The initialiser is a constant expression: a literal, an expression built from literals, casts and other constants, or a member of an enum declared in the same file. A method call or an object creation produces a run-time value however fixed it looks.

  • Nothing writes to the local: no assignment, no ++ or --, and it is never passed as a ref or out argument.

A local declared with var is reported when its initialiser is a literal — var limit = 32; is the usual modern spelling of exactly this defect. The fix has to write the type out, so the message names the type the literal gives the local, and only where that is certain: an integer literal too large for the type its suffix names, and a u8 string literal (a span of bytes, not a string), are left alone rather than described wrongly. A var local initialised with anything other than a literal is not reported either, because naming its type would mean inferring it. Neither are locals read from inside a nested lambda or local function.

public enum Currency { Euro, Dollar }

public class Pricing
{
    public decimal Total(decimal amount)
    {
        int scale = 100;                     // FLAW — constant, never reassigned
        string currency = "EUR";             // FLAW — same
        var factor = 3;                      // FLAW — declare it const int
        Currency billed = Currency.Euro;     // FLAW — an enum can be const too
        decimal rate = 0.21m;                // OK, reassigned below

        if (amount > 1000) rate = 0.15m;
        return amount * (1 + rate) * scale * factor / 100 + currency.Length + (int) billed;
    }

    public int Retries()
    {
        const int limit = 3;                 // OK, already const
        int attempts = 0;                    // OK, incremented
        var deadline = DateTime.UtcNow;      // OK, var from a call has no literal type
        while (attempts < limit && DateTime.UtcNow < deadline) attempts++;
        return attempts;
    }
}

Remediation

Add const to the declaration:

const int scale = 100;
const string currency = "EUR";
const Currency billed = Currency.Euro;

Where the local was declared var, the type has to be written out as part of the change — the message names it, so var factor = 3; becomes const int factor = 3;.

If the value is used by more than one method, promote it to a private const field of the type so the two copies cannot drift apart. If it belongs to the caller rather than to the method, make it a parameter with a default instead.

Where the type prevents const — anything other than a numeric primitive, bool, char, string or an enum — readonly on a field is the equivalent guarantee for the object reference.