A variable represents a storage location that has a modifiable value. A variable can be a local variable, parameter (value, ref, or out), field (instance or static), or array element.
The stack and the heap are the places where variables and constants reside. Each has very different lifetime semantics.
The stack is a block of memory for storing local variables and parameters. The stack logically grows and shrinks as a function is entered and exited. Consider the following method (to avoid distraction, input argument checking is ignored):
static int Factorial (int x) { if (x == 0) return 1; return x * Factorial (x-1); }
This method is recursive, meaning that it calls itself. Each
time the method is entered, a new int
is allocated on the stack, and each time
the method exits, the int
is
deallocated.
The heap is a block of memory in which objects (i.e., reference-type instances) reside. Whenever a new object is created, it is allocated on the heap, and a reference to that object is returned. During a program’s execution, the heap starts filling up as new objects are created. The runtime has a garbage collector that periodically deallocates objects from the heap, so your computer does not run out of memory. An object is eligible for deallocation as soon as it’s not referenced by anything that’s itself alive.
Value-type instances (and object references) live wherever the variable was declared. If the instance was declared as a field within an object, or as an array element, that instance lives on the heap.
You can’t explicitly delete objects in C#, as you can in C++. An unreferenced object is eventually collected by the garbage collector.
The heap also stores static fields and constants. Unlike objects allocated on the heap (which can get garbage-collected), these live until the application domain is torn down.
C# enforces a definite assignment policy. In practice, this
means that outside of an unsafe
context, it’s impossible to access
uninitialized memory. Definite assignment has three implications:
Local variables must be assigned a value before they can be read.
Function arguments must be supplied when a method is called (unless marked optional—for more information, see Optional parameters).
All other variables (such as fields and array elements) are automatically initialized by the runtime.
For example, the following code results in a compile-time error:
static void Main() { int x; Console.WriteLine (x); // Compile-time error }
However, if x
were instead a
field of the containing class, this would be legal
and would print 0
.
All type instances have a default value. The default value
for the predefined types is the result of a bitwise zeroing of memory,
and is null
for reference types,
0
for numeric and enum types, ' '
for
the char
type, and
false
for the bool
type.
You can obtain the default value for any type using the default
keyword (in practice, this is useful
with generics, as we’ll see later). The default value in a custom value
type (i.e., struct
) is the same as
the default value for each field defined by the custom type.
A method has a sequence of parameters. Parameters define the
set of arguments that must be provided for that method. In this example,
the method Foo
has a single parameter
named p
, of type int
:
static void Foo (int p) // p is aparameter
{ ... } static void Main() { Foo (8); } // 8 is anargument
You can control how parameters are passed with the ref
and out
modifiers:
Parameter modifier | Passed by | Variable must be definitely assigned |
---|---|---|
None | Value | Going in |
| Reference | Going in |
| Reference | Going out |
By default, arguments in C# are passed by value, which is by far the most common case. This means a copy of the value is created when passed to the method:
static void Foo (int p) { p = p + 1; // Increment p by 1 Console.WriteLine (p); // Write p to screen } static void Main() { int x = 8; Foo (x); // Make a copy of x Console.WriteLine (x); // x will still be 8 }
Assigning p
a new value does
not change the contents of x
, since
p
and x
reside in different memory
locations.
Passing a reference-type argument by value copies the
reference, but not the object. In the following
example, Foo
sees the same StringBuilder
object that Main
instantiated, but has an independent
reference to it. In other words, sb
and fooSB
are separate variables that reference
the same StringBuilder
object:
static void Foo (StringBuilder fooSB) { fooSB.Append ("test"); fooSB = null; } static void Main() { StringBuilder sb = new StringBuilder(); Foo (sb); Console.WriteLine (sb.ToString()); // test }
Because fooSB
is a
copy of a reference, setting it to null
doesn’t make sb
null. (If, however, fooSB
was declared and called with the
ref
modifier, sb
would become
null.)
To pass by reference, C# provides the
ref
parameter modifier. In the
following example, p
and x
refer to the same memory locations:
static void Foo (ref
int p) { p = p + 1; Console.WriteLine (p); } static void Main() { int x = 8; Foo (ref
x); // Pass x byreference
Console.WriteLine (x); // x is now 9 }
Now assigning p
a new value
changes the contents of x
. Notice
how the ref
modifier is required
both when writing and when calling the method. This makes it very
clear what’s going on.
An out
argument is like
a ref
argument, except it:
Need not be assigned before going into the function
Must be assigned before it comes out of the function
The out
modifier is most
commonly used to get multiple return values back from a method.
The params
modifier may
be specified on the last parameter of a method so that the method
accepts any number of parameters of a particular type. The parameter
type must be declared as an array. For example:
static int Sum (params int[] ints) { int sum = 0; for (int i = 0; i < ints.Length; i++) sum += ints[i]; return sum; }
We can call this as follows:
Console.WriteLine (Sum (1, 2, 3, 4)); // 10
You can also supply a params
argument as an ordinary array. The preceding call is semantically
equivalent to:
Console.WriteLine (new int[] { 1, 2, 3, 4 } );
Starting with C# 4.0, methods, constructors, and indexers can declare optional parameters. A parameter is optional if it specifies a default value in its declaration:
void Foo (int x = 23
) { Console.WriteLine (x); }
Optional parameters may be omitted when calling the method:
Foo(); // 23
The default argument of 23
is actually passed
to the optional parameter x
—the
compiler bakes the value 23
into
the compiled code at the calling side. The
preceding call to Foo
is semantically identical to:
Foo (23);
because the compiler simply substitutes the default value of an optional parameter wherever it is used.
Adding an optional parameter to a public method that’s called from another assembly requires recompilation of both assemblies—just as though the parameter were mandatory.
The default value of an optional parameter must be specified by
a constant expression, or a parameterless constructor of a value type.
Optional parameters cannot be marked with ref
or out
.
Mandatory parameters must occur before
optional parameters in both the method declaration and the method call
(the exception is with params
arguments, which still always come
last). In the following example, the explicit value of 1
is passed to x
, and the default value of 0
is passed to y
:
void Foo (int x = 0, int y = 0) { Console.WriteLine (x + ", " + y); } void Test() { Foo(1); // 1, 0 }
To do the converse (pass a default value to x
and an explicit value to y
), you must combine optional parameters
with named arguments.
Rather than identifying an argument by position, you can identify an argument by name. For example:
void Foo (int x, int y)
{
Console.WriteLine (x + ", " + y);
}
void Test()
{
Foo (x:1, y:2
); // 1, 2
}
Named arguments can occur in any order. The following calls to
Foo
are semantically
identical:
Foo (x:1, y:2); Foo (y:2, x:1);
You can mix named and positional parameters, as long as the named arguments appear last:
Foo (1, y:2);
Named arguments are particularly useful in conjunction with optional parameters. For instance, consider the following method:
void Bar (int a=0, int b=0, int c=0, int d=0) { ... }
We can call this supplying only a value for d
as follows:
Bar (d:3);
It is often the case that you declare and initialize a
variable in one step. If the compiler is able to infer the type from the
initialization expression, you can use the word var
in place of the type declaration. For
example:
var x = "hello"; var y = new System.Text.StringBuilder(); var z = (float)Math.PI;
This is precisely equivalent to:
string x = "hello"; System.Text.StringBuilder y = new System.Text.StringBuilder(); float z = (float)Math.PI;
Because of this direct equivalence, implicitly typed variables are statically typed. For example, the following generates a compile-time error:
var x = 5; x = "hello"; // Compile-time error; x is of type int
In the section Anonymous Types, we describe a
scenario where the use of var
is
mandatory.