A variable is a storage location with a type (C# specification clause 9). The compiler makes sure that every variable has a value before it is read, by the rules of definite assignment.
Variable categories
| Category | Example | Initial state | Clause |
|---|---|---|---|
| Static variable | static int count; | Assigned, default value | 9.2.2 |
| Instance variable | int count; in a class | Assigned, default value | 9.2.3 |
| Array element | a[0] | Assigned, default value | 9.2.4 |
| Value parameter | void F(int x) | Assigned by the caller | 9.2.5 |
| Reference parameter | void F(ref int x) | Assigned by the caller | 9.2.6 |
| Output parameter | void F(out int x) | Unassigned | 9.2.7 |
| Input parameter | void F(in int x) | Assigned by the caller | 9.2.8 |
| Local variable | int x; in a method | Unassigned | 9.2.9 |
A field of a struct variable belongs to that variable’s category. In a struct variable,
the instance variables are assigned when the struct variable is. A local variable may
be in a block, a for, foreach, using,
fixed or catch header, or be created by a pattern or an
out argument.
Local variables
int a; // declared, unassigned
int b = 1, c = 2; // several in one declaration
var d = 1.5; // inferred as double
const int Max = 10; // a local constant
using var file = File.OpenRead(path); // disposed at the end of the scope
A local variable’s scope is the block it is declared in, including any part of the block before the declaration. It cannot be used before its declaration, and a nested block cannot declare the same name while the variable is in scope. Since C# 8, the parameters of a lambda, anonymous method or local function may shadow outer names.
Implicitly typed locals
var declares a local whose type is taken from its initialiser (C# 3,
clause 13.6.2). The initialiser is required and must have a type: var x =
null; and var a = { 1, 2 }; are errors. The result is still
statically typed. var also works in foreach,
using and ref var. It is a contextual keyword, not a
reserved word, so a type could be called var, though then that type is
used instead.
Discards
A discard _ is a write-only variable with no name (C# 7.0,
clause 9.2.9.2). Values assigned to it are dropped.
_ = Compute(); // ignores the result
int.TryParse(text, out _); // ignores an out value
var (first, _) = GetPair(); // ignores a deconstructed value
If a variable named _ is in scope, _ means that variable.
In a switch expression, _ is the discard pattern.
Definite assignment
A local variable, or an out parameter, must be definitely
assigned before it is read (clause 9.4). The compiler works this out by flow
analysis at every statement and expression. It does not evaluate conditions, except
constants.
int a;
if (cond) a = 1; else a = 2;
Console.WriteLine(a); // fine: assigned on both paths
int b;
if (cond) b = 1;
Console.WriteLine(b); // error: unassigned on the other path
int c;
if (x > 0 && (c = x) > 0)
Console.WriteLine(c); // fine: && assigned c on the true path
int d;
while (true) { d = 1; break; }
Console.WriteLine(d); // fine: while (true) ends only through the break
- Static variables, instance variables and array elements are always assigned.
- An
outparameter must be assigned before the method returns normally. A method that always throws has no such obligation. - For a struct variable, each field is tracked separately until all are assigned.
- Inside a struct constructor,
thismust be fully assigned before use. Since C# 11, fields not assigned by a constructor are set to their default values, so a struct constructor need not assign every field. - Variables captured by a lambda, or declared in a
try, follow the rules of clauses 9.4.4.14 to 9.4.4.16 and 9.4.4.31: assignment in atryblock is not definite in the matchingcatch.
Parameters
A method’s parameters can be passed in five ways (clause 15.6.2).
| Modifier | Passes | Caller writes | Must be assigned | Since |
|---|---|---|---|---|
| none | A copy of the value | F(x) | Before the call | 1.0 |
ref | A reference to the variable | F(ref x) | Before the call | 1.0 |
out | A reference; the method assigns it | F(out x) or F(out int x) | By the method | 1.0 |
in | A read-only reference | F(x) or F(in x) | Before the call | 7.2 |
ref readonly | A read-only reference | F(ref x) or F(in x) | Before the call | 12 |
out var xdeclares the variable in the argument list (C# 7.0). Its scope is the enclosing block, so it can be read after the call.- An
inargument may be a value, in which case a temporary is created. Aref readonlyparameter expects a variable, and the compiler warns if given a value. - A by-reference parameter and a by-value parameter are different for overloading,
but two overloads cannot differ only in which of
ref,in,ref readonlyandoutthey use. inandref readonlystop the method writing through the parameter. If a method called on such a struct parameter might change it, the compiler makes a defensive copy.
params
params on the last parameter lets a caller pass any number of arguments.
Originally an array, from C# 13 it can be any collection type that a
collection
expression can target, including Span<T>,
ReadOnlySpan<T> and interfaces such as
IEnumerable<T>.
static int Sum(params ReadOnlySpan<int> values)
{
int total = 0;
foreach (int v in values) total += v;
return total;
}
Sum(); // empty
Sum(1, 2, 3); // three arguments
Optional and named arguments
A parameter with a default value is optional (C# 4). The default must be a constant,
default, or new S() for a value type. Arguments can be named,
and since C# 7.2 a positional argument may follow a named one if the names are in the
right position. Optional parameters must follow required ones.
void Draw(int x, int y = 0, string color = "red") { }
Draw(1);
Draw(1, color: "blue"); // skips y
Draw(y: 2, x: 1);
Reference variables and returns
A reference variable is an alias for another variable. Reference parameters are one kind; ref locals and ref returns are the others (C# 7.0, clause 9.7).
int[] data = { 1, 2, 3 };
ref int first = ref data[0]; // ref local
first = 9; // data[0] is now 9
first = ref data[1]; // ref reassignment, C# 7.3
ref readonly int r = ref data[2]; // cannot be used to write
ref int Max(ref int a, ref int b) // ref return
{
if (a > b) return ref a;
return ref b;
}
Max(ref data[0], ref data[1]) = 0; // assigns to the larger
ref int e = ref (cond ? ref data[0] : ref data[1]); // ref conditional, C# 7.2
refappears both at the declaration and on the assignment of a ref local.- A
foreachcan usereforref readonlyvariables when the enumerator’sCurrentreturns by reference. - Since C# 13, ref locals may be used in iterators and async methods, within
restrictions: not across a
yield returnor anawait. - A
reffield exists only in aref struct.
Ref safety
The compiler checks that a reference cannot outlive the variable it refers to
(clause 9.7.2). Each reference has a ref safe context: the scope in which it
may be used. Roughly, a reference to a local variable may not be returned, and a
reference that outlives its target is rejected at compile time. The rules also apply
to values of ref struct types such as Span<T>.
scoped
The scoped modifier (C# 11, clause 9.7.3) tells the compiler that a
reference, or a ref struct value, does not escape the current method:
Span<int> Keep(scoped Span<int> values)
{
values[0] = 1; // fine: values is used only inside the method
return values; // error: values cannot escape this method
}
UnscopedRefAttribute relaxes the same rule where a member needs to
return a reference (clause 23.5.8).
Atomicity
Reads and writes of bool, char, byte,
sbyte, short, ushort, int,
uint, float, reference types and enums based on those are
atomic. Other types, including long, ulong,
double, decimal and user structs, have no such guarantee
(clause 9.6). A combined read and write such as i++ is never atomic.
Interlocked in System.Threading provides atomic
read-modify-write operations.