In C#, it is purely syntax to turn a group of statements into a single statement. Required for any keyword that expects a single statement to follow, like if, for, using, etc. A few corner cases:
- the case keyword inside a switch is special since it doesn't require it to be a single statement. The break or goto keyword ends it. Which explains why you can use braces to jam in a variable declaration.
- the try and catch keywords are special, they require braces even if only a single statement follows. Pretty unusual but probably inspired by forcing the programmer to think about the scope of declarations inside the blocks, a catch block cannot refer to variables inside the try block because of the way exception handling works.
Limiting the scope of local variables with it is a lost cause. It is a big deal in C++ because the ending brace is the place where the compiler will inject destructor calls for variables inside the scope block. This is ab/used all the time for the RAII pattern, nothing terribly pretty about having punctuation in a program have such drastic side-effects.
The C# team didn't have a lot of choice about it, the life-time of local variables is strictly controlled by the jitter. Which is oblivious to any grouping constructs inside a method, it only knows about IL. Which doesn't have any grouping constructs beyond try/except/finally. The scope of any local variable, no matter where it was written, is the body of the method. Something you can see when you run ildasm.exe on compiled C# code, you'll see the local variables hoisted to the top of the method body. Which partly also explains why the C# compiler won't let you declare another local variable in another scope block with the same name.
The jitter has interesting rules about local variable lifetime, they are entirely dominated by how the garbage collector works. When it jits a method, it doesn't just generate the machine code for the method but also creates a table that describes the actual scope of every local variable, the code address where it is initialized and the code address where it is no longer used. The garbage collector uses that table to decide if a reference to an object is valid, based on the active execution address.
Which makes it very efficient at collecting objects. A little too efficient sometimes and troublesome when you interop with native code, you may need the magic GC.KeepAlive() method to extend the lifetime. A very remarkable method, it doesn't generate any code at all. Its only use is to get the jitter to change the table and insert a larger address for the variable life-time.