Variables and Parameters

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

The stack and the heap are the places where variables and constants reside. Each has very different lifetime semantics.

Stack

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.

Heap

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.

Note

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.

Definite Assignment

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.

Default Values

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.

Parameters

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 a parameter
{
  ...
}
static void Main() { Foo (8); }   // 8 is an argument

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

ref

Reference

Going in

out

Reference

Going out

Passing arguments by value

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.)

The ref modifier

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 by reference
  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.

Note

A parameter can be passed by reference or by value, regardless of whether the parameter type is a reference type or a value type.

The out modifier

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

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 } );

Optional parameters

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.

Warning

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.

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);

This is particularly useful when calling COM APIs.

var—Implicitly Typed Local Variables

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset