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

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

  • What's New for Python, Java in Visual Studio Code

    Microsoft announced March 2024 updates to its Python and Java extensions for Visual Studio Code, the open source-based, cross-platform code editor that has repeatedly been named the No. 1 tool in major development surveys.

Subscribe on YouTube