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

  • GitHub Expands Copilot Enterprise Search in Visual Studio and VS Code

    GitHub supercharged search for its Copilot Enterprise AI assistant in both Microsoft's Visual Studio IDE and Visual Studio Code so developers can now get results from well beyond local codebases, including the internet.

  • What's New in TypeScript 5.5, Now Generally Available

    Microsoft shipped the latest iteration of its type-infused superset of JavaScript, TypeScript 5.5, introducing inferred type predicates, control flow narrowing, JSDoc @import and other enhancements.

  • GitHub Copilot for Azure Gets Preview Glitches

    This reporter, recently accepted to preview GitHub Copilot for Azure, has thus far found the tool to be, well, glitchy.

  • New .NET 9 Templates for Blazor Hybrid, .NET MAUI

    Microsoft's fifth preview of .NET 9 nods at AI development while also introducing new templates for some of the more popular project types, including Blazor Hybrid and .NET MAUI.

  • What's Next for ASP.NET Core and Blazor

    Since its inception as an intriguing experiment in leveraging WebAssembly to enable dynamic web development with C#, Blazor has evolved into a mature, fully featured framework. Integral to the ASP.NET Core ecosystem, Blazor offers developers a unique combination of server-side rendering and rich client-side interactivity.

Subscribe on YouTube