Compartilhar via


Local variable scoping in C#

In my previous post, Compiler-generated scopes for local variable declarations, I briefly touched on the issue of multiple meanings applied to the same name. In this post, I'll aim to flush out the compiler's rules with regards to binding names in their local scopes.

Simple name resolution

First, lets recall the spec's definition of simple name resolution, from section 7.5.2:

If [...] the simple-name appears within a block and if the block’s (or an enclosing block’s) local variable declaration space (§3.3) contains a local variable, parameter or constant with name I, then the simple-name refers to that local variable, parameter or constant and is classified as a variable or value.

Simply put, the simple name gets resolved to whatever is declared inside its current block, regardless of whether or not there already exists a declaration of the name outside of the block. So consider the following:

 class C
{
    public int y;

    void Foo()
    {
        int x;
        x = 0; // (1) This binds to the local variable defined above.
        y = 0; // (2) This binds to the field y.

        {
            x = "s"; // (3) This binds to the local defined below.
            string x;

            y = "s"; // (4) This binds to the local defined below.
            string y;
        }
    }
}

Notice that both (3) and (4) produce compiler errors, as well as the redeclarations of x and y on the lines following (3) and (4) respectively. This is in accordance with section 5.1.7 of the spec:

Within the scope of a local variable introduced by a local-variable-declaration, it is a compile-time error to refer to that local variable in a textual position that precedes its local-variable-declarator. If the local variable declaration is implicit (§8.5.1), it is also an error to refer to the variable within its local-variable-declarator.
Within the scope of a local variable, it is a compile-time error to refer to the local variable in a textual position that precedes the local-variable-declarator of the local variable.
Locals preceding their declarator

Lets unpack this. First, lets quickly note that it is an error to refer to a local variable in a textual position that precedes its declarator. However, referring to it inside its declarator is permitted if the local variable is not implicitly typed. That means that the following is true:

     int t = (t = 5); // OK
    var s = (s = 10); // Error

In the first statement, by the time we attempt to bind the right hand side of the assignment statement, we've already declared that t is of type int. We can then bind the right hand side successfully with that knowledge, and then bind the assignment to the left hand side variable, which is the variable initializer. However, in the second statement, we do not have a type for s initially, so when we bind the right hand side of the assignment, we cannot determine if 10 is assignable to s. Consider the following:

     var t = (Foo() ? t = "test": t = 15);

What would t's type be? We cannot report convertibility errors on the right hand side for the two branches of the ternary because we don't have a type for t to report convertibility errors on.

We therefore decided simply to disallow this scenario by disallowing the usage of the local variable in its declarator.

Name Hiding?

We should note that name hiding is only allowed on fields that have not been referenced in the current scope. For instance:

 class C
{
    int x;
    int r;
    void Foo(int y)
    {
        int z;
        int s;

        // Legal - x has not been used in this context yet.
        string x = "s";

        // Illegal - cannot hide parameters.
        string y;

        // Illegal - cannot declare two locals of the same 
        // name in the same scope.
        string z;

        r = 10;
        {
            // Illegal - r has already been used in the parent 
            // scope, so cannot redefine it.
            string r = "s";

            // Illegal - cannot hide locals.
            string s = "s";
        }
    }
}

Notice that the only legal hiding action is the first one - you are allowed to redefine x to be a string, because it has not been referenced as a field in any containing scope for the method Foo.

When is it an error?

As you can imagine, this notion of using a name before its declarator will cause the compiler to generate some errors when the situation occurs. Let me first outline the different situations that this can occur, and give quick examples.

 class C
{
    int x;
    int y;
    int z;
    void Foo()
    {
        x = 10; // (1) Binds to C.x
        {
            // (2) Binds to local variable declared below. 
            // Error - usage before declaration.
            x = 10;

            // (3) Error, cannot redefine x because x has been used.
            string x; 
        }

        // (4) Binds to local y. Error, usage before declaration.
        y = 10;
        string y;

        Func<int, int> f = x => x + 1; // (5) Error, cannot redefine x.
        Func<int, int> g = z => z + 1; // (6) OK. z has not been used.
    }
}

The general rule of thumb is that the name will always resolve to the local variable declared in the current scope if there is one, regardless of whether or not the name has been used to mean anything else in any scopes above it. In (2) above, one might think that x would bind to C.x, just like (1) did, but the spec is clear on this point - the name will always resolve to the local variable bound in the closest scope.

2005 C# Compiler vs. 2008 C# Compiler

One thing that is worth mentioning is that in the 2008 C# Compiler, we fixed the error that is reported to be more in line with the specification. Consider the following code:

 class C
{
    void Foo()
    {
        int x;
        {
            // (1)
            // 2005 Compiler compiles this statement without errors.
            // 2008 Compiler yields CS0841: Cannot use variable 'x' 
            // before it is declared
            x = 5;

            // (2) 
            // 2005 Compiler yields CS0029: Cannot implicitly 
            // convert type 'string' to 'int'
            // 2008 Compiler yields CS0841: Cannot use variable 'x' 
            // before it is declared
            x = "s";

            // (3)
            // 2005 and 2008 Compilers yield CS0136: A local variable 
            // named 'x' cannot 
            // be declared in this scope because it would give a 
            // different meaning to 'x', which is already used in a 
            // 'parent or current' scope to denote something else
            string x;
        }
    }
}

In the 2005 C# Compiler, we incorrectly bound both usages of x to the outer local variable, and so we would bind (1) perfectly fine, and we would report an error on (2) saying that string is not convertible to int. We would also report an error on (3), saying that you cannot redeclare x to be something else.

In the 2008 C# Compiler, we fix this to correctly reflect the spec. Both (1) and (2) bind to (3), and since they are textually before their declaration, they both yield an error, saying that they are being used before they are declared. (3) also yields an error saying that you cannot redeclare x.

So what can we conclude?

Well I hope that helps clarify things a bit. I think the real thing that we should be concluding from this little exercise is that we should be choosing better names for our variables and fields! Descriptive names would help avoiding name clashing issues - if they don't, it's probably a sign that some refactoring is in order!

kick it on DotNetKicks.com

Comments

  • Anonymous
    November 09, 2007
    You've been kicked (a good thing) - Trackback from DotNetKicks.com

  • Anonymous
    November 18, 2007
    Sam, your blog is a nice addition to a great number of blogs about .NET. Go ahead!

  • Anonymous
    November 18, 2007
    There is one more case name hiding I remember, a parameter can hide the name of class scoped (instance or static, doesn't matter) variables. Additionally, an exception variable will hide a class scoped variable if that variable is not used outside catch block. This program correctly compiles with the expected meaning: public class Example {    int ex;    public void example( )   {       try       {       }       catch( Exception ex )      {          string message = ex.Message;          ex = null;       };   } }

  • Anonymous
    November 19, 2007
    Hi Tanveer, You're absolutely right - a parameter can hide the name of fields, as can exception variables. Though both of these are not declared via a local variable declaration, they are both considered to be local variables. In my previous post about compiler-generated scopes for local variables, I mention the different types of local variables that the compiler allows you to create - catch variables, foreach iteration variables, anonymous method/lambda parameters, and using statements, along with the regular local variable and local method parameters.

  • Anonymous
    November 19, 2007
    Welcome to the thirty-sixth issue of Community Convergence. This is the big day, with Visual Studio 2008

  • Anonymous
    December 09, 2007
    Welcome to the thirty-sixth issue of Community Convergence. This is the big day, with Visual Studio 2008

  • Anonymous
    December 09, 2007
    Welcome to the thirty-sixth issue of Community Convergence. This is the big day, with Visual Studio 2008