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,stringor an enum. Everything else — classes, structs, nullable value types, arrays — can bereadonlybut neverconst. 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 areforoutargument.
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.