Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Variable breaking scope

Tags:

c#

How come this gives a 's' does not exist in the current context error (as expected):

public static void Main()
{
    foreach(var i in new[]{1, 2, 3}) {
        int s = i;
    }

    Console.WriteLine(s);
}

(ideone)

But this gives a 's' cannot be redeclared error?

public static void Main()
{
    foreach(var i in new[]{1, 2, 3}) {
        int s = i;
    }

    int s = 4;
}

(ideone)

The first error tells me that s doesn't exist outside of the foreach, which makes sense, but the second error says otherwise. Why (and how!?) would I ever need to access a variable from a child scope?

like image 534
Scott Avatar asked Dec 12 '22 00:12

Scott


2 Answers

The first error tells me that s doesn't exist outside of the foreach, which makes sense

Indeed - it's right.

but the second error says otherwise.

No, it doesn't. It's telling you that you can't declare the first (nested) variable s, because the second one is already in scope. You can't access it before the declaration, but it's in scope for the whole block.

From the C# 5 specification, section 3.7:

• The scope of a local variable declared in a local-variable-declaration (§8.5.1) is the block in which the declaration occurs.

So yes, it extends upwards to the enclosing {., basically.

And then from section 8.5.1:

The scope of a local variable declared in a local-variable-declaration is the block in which the declaration occurs. It is an error to refer to a local variable in a textual position that precedes the local-variable-declarator of the local variable. Within the scope of a local variable, it is a compile-time error to declare another local variable or constant with the same name.

That final part (emphasis mine) is why you're getting an error.

Why (and how!?) would I ever need to access a variable from a child scope?

Not sure what you mean here, but the rule is basically there to make it harder for you to write hard-to-read or fragile code. It means there are fewer places where moving the declaration of the variable up or down (but still in the same block, at the same level of nesting) produces code which is valid but with a different meaning.

like image 177
Jon Skeet Avatar answered Dec 28 '22 22:12

Jon Skeet


Ok, first consider the following, simple class implementation.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestNamespace
{
    class ILOrderTest
    {
        public int DeclarationTests()
        {
            int intDeclaredAtTop = 0;

            for (int intDeclaredInForLoopDef = 0; intDeclaredInForLoopDef < 10; intDeclaredInForLoopDef++)
            {
                int intDeclaredInForLoopBody = intDeclaredInForLoopDef;
                intDeclaredAtTop = intDeclaredInForLoopBody;
            }
            int intDeclaredAfterForLoop;
            intDeclaredAfterForLoop = intDeclaredAtTop;
            return intDeclaredAfterForLoop;
        }
    }
}

As we can see, many variables are declared at different locations of our method and it could be assumed that when the C# interpreter reads our file, it will organize the IL in such a way that declared objects will be defined where we wrote our variable definition code.

However, after compiling and inspecting our IL we see a much different story.

Class ILOrderTest IL

.method public hidebysig 
    instance int32 DeclarationTests () cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 38 (0x26)
    .maxstack 2
    .locals init (
        [0] int32 intDeclaredAtTop,
        [1] int32 intDeclaredInForLoopDef,
        [2] int32 intDeclaredInForLoopBody,
        [3] int32 intDeclaredAfterForLoop,
        [4] int32 CS$1$0000,
        [5] bool CS$4$0001
    )

    IL_0000: nop
    IL_0001: ldc.i4.0
    IL_0002: stloc.0
    IL_0003: ldc.i4.0
    IL_0004: stloc.1
    IL_0005: br.s IL_0011
    // loop start (head: IL_0011)
        IL_0007: nop
        IL_0008: ldloc.1
        IL_0009: stloc.2
        IL_000a: ldloc.2
        IL_000b: stloc.0
        IL_000c: nop
        IL_000d: ldloc.1
        IL_000e: ldc.i4.1
        IL_000f: add
        IL_0010: stloc.1

        IL_0011: ldloc.1
        IL_0012: ldc.i4.s 10
        IL_0014: clt
        IL_0016: stloc.s CS$4$0001
        IL_0018: ldloc.s CS$4$0001
        IL_001a: brtrue.s IL_0007
    // end loop

    IL_001c: ldloc.0
    IL_001d: stloc.3
    IL_001e: ldloc.3
    IL_001f: stloc.s CS$1$0000
    IL_0021: br.s IL_0023

    IL_0023: ldloc.s CS$1$0000
    IL_0025: ret
} // end of method ILOrderTest::DeclarationTests

Notice that all of our objects for our method have been gathered together and initialized in the .locals init... call at the top of our method. If I find the time, I may do a little further digging on why the .Net IL is organized this way, but if I had to make an educated guess it would be that variable declaration has a certain level of overhead involved and that bundling together all variables within a particular scope might save a few CPU cycles, instead of making many calls to .locals init... , every time that a new object is declared.

I hope that provides a little further clarity. Jons answer is correct by spec definition, but this could shed a little light on why the spec was written that way.

like image 42
RLH Avatar answered Dec 28 '22 22:12

RLH