C# Corner

Understand Variable Scoping and Definite Assignment

Learn how to scope your variables correctly, as well as the rules governing definite assignment in C#.

Technology Toolbox: C#

It's absolutely critical that you understand how to scope your variables correctly. Making mistakes in this basic process can cause your application to fail or otherwise behave unexpectedly. If you're familiar with other Microsoft- or Java-related languages, C# should present few hurdles to getting up to speed on this process, but you should still be aware of how C# specifically handles variable scoping and definite assignment. This short article will cover what you need to handle this process smoothly and properly.

C# lets you assign only local variable names that allow them to be identified uniquely 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. For example, consider this short snippet:

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, you must prefix it with "this," which indicates it must be an instance variable.

This construct is preferable to avoiding the naming conflict by renaming the constructor parameters or member variables. For example, this code snippet gives you no way to name both variables, so the inner declaration results in 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);
     }
   }
}

The inner declaration of x, hides the outer declaration of x, so it isn't allowed.

C# incorporates 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 you were using—or even that there are multiple versions—inside a nested scope.

Another key area where you should understand how C#'s compiler works: 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);
   }
}

Compiling this code generates an error because it uses the value of "n" before the variable is 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 lets you make sure they're zeroed out, but the compiler still checks whether structs have been initialized before you use them.

You initialize a struct either by calling a constructor or by setting all the members of an instance before you use it:

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

You initialize myNumber1 with 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.

You initialize myNumber2 in this code with a normal call to a constructor, while you initialize myNumber3 by assigning values to all members of the instance. Note that you can do this only if the members are public.

Arrays work a bit differently for definite assignment. You can access an element of an array for arrays of classes and structs, even if you haven't initialized the arrays to a nonzero value yet:

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

Note that the compiler can't track definite assignment in all situations because some operations you might perform as an array, such as Reverse(), can lead to spurious errors. For this reason, the compiler doesn't attempt to track definite assignment in these cases.

This article is excerpted from Chapter 13 of the book by Eric Gunnerson and Nick Wienholt, A Programmer's Introduction to C# 2.0. [ISBN: 1-59059-501-7].

About the Author

Eric Gunnerson is a software design engineer in Microsoft''s Visual C++ QA group and a member of the C# design team. In his professional career, he has worked primarily on database products and tools, and is proud that nearly half the companies he has worked for remain in business. Nick Wienholt is an independent Windows and .NET consultant based in Sydney, Australia. He is the author of Maximizing .NET Performance, one of the top-selling .NET performance books. Since the book''s release, it has ranked among Amazon''s top 25 .NET titles.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube