Variable Scoping and Definite Assignment

In C#, you can give local variables only those names that allow them to be uniquely identified in a given scope. If a name has more than one meaning in a scope and you have no way to disambiguate the name, the innermost declaration of the name is an error and you must change it. Consider the following:

using System;

class MyObject

{

public MyObject(int x, int y)

{

this.x = x;

this.y = y;

}

int x; int y;

}

In the constructor, x refers to the parameter named x because parameters take precedence over member variables. To access the instance variable named x, it must be prefixed with this., which indicates it must be an instance variable.

The preceding construct is preferred over renaming the constructor parameters or member variables to avoid the name conflict.

In the following situation, you have no way to name both variables, and the inner declaration is therefore an error:

// error using System; class MyObject {

public void Process() {

int x = 12;

for (int y = 1; y < 10; y++)

{

// no way to name outer x here. int x = 14;

Console.WriteLine(“x = {0}”, x);

}

}

}

Because the inner declaration of x would hide the outer declaration of x, it isn’t allowed. C# has this restriction to improve code readability and maintainability. If this restriction wasn’t in place, it might be difficult to determine which version of the variable was being used— or even that there are multiple versions—inside a nested scope.

1. Definite Assignment

Definite assignment rules prevent the value of an unassigned variable from being observed. Consider the following:

// error using System; class Test {

public static void Main()

{

int n;

Console.WriteLine(“Value of n is {0}”, n);

}

}

When this is compiled, the compiler will report an error because the value of n is used before it has been initialized.

Similarly, you can’t perform operations with a class variable before it’s initialized:

// error

using System;

class MyClass {

public MyClass(int value)

{

this.value = value;

}

public int Calculate()

{

return(value * 10);

}

public int value;

}

class Test {

public static void Main() {

MyClass mine;

Console.WriteLine(“{0}”, mine.value);          // error

Console.WriteLine(“{0}”, mine.Calculate());    // error

mine = new MyClass(12);

Console.WriteLine(“{0}”, mine.value);          // okay now…

}

Structs work slightly differently when you consider definite assignment. The runtime will always make sure they’re zeroed out, but the compiler will still check to make sure they’re initialized to a value before they’re used.

You initialize a struct either by calling a constructor or by setting all the members of an instance before it’s used:

using System; struct Complex {

public Complex(float real, float imaginary)

{

this.real = real;

this.imaginary = imaginary;

}

public override string ToString()

{

return(String.Format(“({0}, {1})”, real, imaginary));

}

public float              real;

public float              imaginary;

}

class Test {

public static void Main()

{

Complex myNumberl;

Complex myNumber2;

Complex myNumber3;

myNumber1 = new Complex();

Console.WriteLine(“Number 1: {0}”, myNumber1);

myNumber2 = new Complex(5.0F, 4.0F);

Console.WriteLine(“Number 2: {0}”, myNumber2);

myNumber3.real = 1.5F;

myNumber3.imaginary = 15F;

Console.WriteLine(“Number 3: {0}”, myNumber3);

}

}

In the first section of this code, myNumber1 is initialized by the call to new. Remember, structs don’t have default constructors, so this call doesn’t do anything; it merely has the side effect of marking the instance as initialized.

In the second section, myNumber2 is initialized by a normal call to a constructor.

In the third section, myNumber3 is initialized by assigning values to all members of the instance. Obviously, you can do this only if the members are public.

2. Definite Assignment and Arrays

Arrays work a bit differently for definite assignment. For arrays of both reference and value types (classes and structs), an element of an array can be accessed, even if it hasn’t been initialized to a nonzero value.

For example, suppose you have an array of Complex:

using System;

struct Complex {

public Complex(float real, float imaginary)

{

this.real = real;

this.imaginary = imaginary;

}

public override string ToString()

{

return(String.Format(“({0}, {0})”, real, imaginary));

}

public float             real;

public float             imaginary;

}

class Test {

public static void Main()

{

Complex[] arr = new Complex[10];

Console.WriteLine(“Element 5: {0}”,arr[5]);        // legal

}

}

Because of the operations that might be performed on an array—such as Reverse()— the compiler can’t track definite assignment in all situations, and it could lead to spurious errors. It therefore doesn’t try.

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *