When do you use scope without a statement in C#?

2019-02-21 09:24发布

Just recently I found out you can do this in C#:

{
    // google
    string url = "#";

    if ( value > 5 )
        url = "http://google.com";

    menu.Add( new MenuItem(url) );
}
{
    // cheese
    string url = "#"; // url has to be redefined again, 
                      // so it can't accidently leak into the new menu item

    if ( value > 45 )
        url = "http://cheese.com";

    menu.Add( new MenuItem(url) );
}

instead of i.e.:

    string url = "#";

    // google
    if ( value > 5 )
        url = "http://google.com";

    menu.Add( new MenuItem(url) );


    // cheese
    url = "#"; // now I need to remember to reset the url

    if ( value > 45 )
        url = "http://cheese.com";

    menu.Add( new MenuItem(url) );

This might be a bad example that can be solved in a lot of other manners.

Are there any patterns where the 'scope without statement' feature is a good practice?

标签: c# syntax block
5条回答
Rolldiameter
2楼-- · 2019-02-21 10:10

If you ask why did they implement the feature?

I've developed my own language, and what I found was that the 'scope without a statement' syntax creeps up into grammar, very easily. It's a side-effect if you will, when writing a grammar that just works nicely & is easy to read.

In my language, I have the same functionality, and I never "designed" for it explicitly - I got it for free. I never sat down on my desk, and thought "oh, wouldn't it be cool to have such 'feature'?". Heck - at first, I didn't even know that my grammar allowed that.

'{}' is a "compound statement" because that simplifies the syntax for all the places you'd want to use it (conditionals, loop bodies, etc)... and because that lets you leave out the braces when a single statement is being controlled ('if (a<10) ++a;' and the like).

The fact that it can be used anywhere a statement can appear falls directly out of that; it's harmless, and occasionally helpful as other answers have said. "If it ain't broke, don't fix it. - keshlam.

So, the question is not so much about "why did they implement it", but rather, "why didn't they ban it / why did they allow this?"

Could I ban this specific functionality in my language? Sure, but I see no reason to - it'll be extra cost for the company.

Now, the above story might, or might not be true for C#, but I didn't design language (nor am I really a language designer), so it's hard to say why exactly, but thought I'd mention it anyway.

The same functionality is in C++, which actually has some use cases - it allows a finalizer of an object to be run deterministically, if the object goes out of the scope, though this is not the case for C#.


That said, I have not used that specific syntax in my 4 years of C# (embedded-statement -> block, when talking about concrete termins), neither I've seen it being used anywhere. Your example is begging to be refactored to methods.

Take a look at C# grammar: http://msdn.microsoft.com/en-us/library/aa664812%28v=vs.71%29.aspx

Also, as Jeppe has said, I've used the '{}' syntax in order to make sure that each case-block in 'switch' construction has separate local scope:

int a = 10;
switch(a)
{
    case 1:
    {
        int b = 10;
        break;
    }

    case 2:
    {
        int b = 10;
        break;
    }
}
查看更多
来,给爷笑一个
3楼-- · 2019-02-21 10:18

One use that I find acceptable in many cases, is to enclose each switch section of a switch statement in a local scope.


Late addition:

The local scope blocks { ... } present in the C# source do not seem to be relevant for the resulting IL bytecode. I tried this simple example:

static void A()
{
    {
        var o = new object();
        Console.WriteLine(o);
    }

    var p = new object();
    Console.WriteLine(p);
}

static void B()
{
    var o = new object();
    Console.WriteLine(o);

    var p = new object();
    Console.WriteLine(p);
}


static void C()
{
    {
        var o = new object();
        Console.WriteLine(o);
    }

    {
        var o = new object();
        Console.WriteLine(o);
    }
}

This was compiled in Release mode (optimizations enabled). The resulting IL according to IL DASM is:

.method private hidebysig static void  A() cil managed
{
  // Code size       25 (0x19)
  .maxstack  1
  .locals init ([0] object o,
           [1] object p)
  IL_0000:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_000c:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0011:  stloc.1
  IL_0012:  ldloc.1
  IL_0013:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_0018:  ret
} // end of method LocalScopeExamples::A

 

.method private hidebysig static void  B() cil managed
{
  // Code size       25 (0x19)
  .maxstack  1
  .locals init ([0] object o,
           [1] object p)
  IL_0000:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_000c:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0011:  stloc.1
  IL_0012:  ldloc.1
  IL_0013:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_0018:  ret
} // end of method LocalScopeExamples::B

 

.method private hidebysig static void  C() cil managed
{
  // Code size       25 (0x19)
  .maxstack  1
  .locals init ([0] object o,
           [1] object V_1)
  IL_0000:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_000c:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0011:  stloc.1
  IL_0012:  ldloc.1
  IL_0013:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_0018:  ret
} // end of method LocalScopeExamples::C

Conclusions:

  • Local scope declarations do not exist in any form in the IL bytecode.
  • The C# compiler will choose new names for local variables that would otherwise be in conflict (the second of the variables o in the C method).
  • People who feel (see other answers and comments to question) that the garbage collector might be able to do its work earlier when they introduce local scope in the C# source, are wrong.

I also tried compiling this in Debug mode (no optimizations). The start and end of a local scope seem to show up as a nop instruction only ("No operation"). In some cases two identically named local variables from distinct local scopes were mapped to the same local variable in the IL, like with C# method named C above. Such a "unification" of two variables is only possible if their types are compatible.

查看更多
相关推荐>>
4楼-- · 2019-02-21 10:18

That pattern has little to no effect on the runtime of C#, so it is purely an aesthetic thing (compare to C++, where we regularly use this pattern with RAII to scope things like locks).

If I have two completely unrelated blocks of code, I will sometimes scope them this way, to make it 100% clear what variables a programmer has to keep in their head as "potentially modified in the previous block. It fills a gap between on-big-code-block and isolated functions; I can share some variables not others.

I will also use this around auto-generated code. It can often be MUCH easier to work with such plugable-blocks without worrying about interactions.

When I do use this, I stylistically like to put a comment before each block, roughly where an if statement would go, explaining what the block will do. I find it is helpful for avoiding other developers thinking "this looks like there used to be control flow, but somebody buggered it up." In this case, it might be a little overkill, but you'll get the idea:

// Add a menu item for Google, if value is high enough.
{
    string url = "#";

    if ( value > 5 )
        url = "http://google.com";

    menu.Add( new MenuItem(url) );
}

// Add a menu item for Cheese, if the value is high enough
{
    // cheese
    string url = "#";

    if ( value > 45 )
        url = "http://cheese.com";

    menu.Add( new MenuItem(url) );
}

As stated before, this is purely stylistic in C#. Feel free to use your own style where it makes sense.

查看更多
家丑人穷心不美
5楼-- · 2019-02-21 10:23

You could use {} to to repurpose variable names (even for different types):

{
   var foo = new SomeClass();
}
{
   Bar foo = new Bar(); // impairs readability
}

However, repurposing variables in this way will confuse readability.

So instead of using "unsolicited" scope block without a preceding statement, in most cases the code should instead be refactored into separate functions accordingly.

Edit

IMO, any time there is the need to imperatively reset local mutable variable values or repurpose them for additional or alternative concerns is a sign of a smell. e.g. the original code can be refactored as:

menu.Add( value > 5 
            ? new MenuItem("http://google.com")
            : new MenuItem("#"));

menu.Add( value > 45 
            ? new MenuItem("http://cheese.com")
            : new MenuItem("#"));

Which I believe conveys the intention, without the risk of the # fallback not being applied, and without the need for explicit local mutable variables to hold state.

(or new MenuItem(value > 45 ? "http://cheese.com" : "#"), or create an overload of MenuItem with a default of #, or move the creation of MenuItem into a factory method, etc.)

Edit
Re : Scope has no effect on lifespan

Scope can be used within a method to limit the lifespan of expensive objects

My initial post incorrectly stated that local scope could be used to influence the lifespan of objects. This is incorrect, for both DEBUG and RELEASE builds, and irrespective of whether the variable name is reallocated or not, as demonstrated by Jeppe's IL disassembly, and by these Unit tests here. Thanks to Jeppe for pointing this out. In addition, Lasse makes the point that even without explicitly going out of scope, variables no longer in use will be eligible for garbage collection in release builds.

TL;DR Although use of unsolicited scope may help convey the logical use of a variable scope to a human, doing so has no influence on whether the object becomes eligible for collection, within the same method.

i.e in the code below, scoping, and even re-purposing the variable foo below has absolutely no effect on lifespan.

void MyMethod()
{
  // Gratuituous braces:
  {
      var foo = new ExpensiveClass();
  }
  {
      Bar foo = new Bar(); // Don't repurpose, it impairs readability!
  }
  Thread.Sleep(TimeSpan.FromSeconds(10));
  GC.Collect();
  GC.WaitForPendingFinalizers();
  <-- Point "X"
}

At Point X:

  • In a DEBUG build, neither of the foo variables will have been collected despite the hacky attempts to entice the GC to do so.
  • In a RELEASE build, both foos would have been eligible for collection as soon as they aren't needed, irrespective of the scope. The timing of collection of course should be left to its own devices.
查看更多
戒情不戒烟
6楼-- · 2019-02-21 10:25
{
    string f = "hello";
}

Just looks weird.

Obviously, methods need them:

private void hmm() {}

And switch statements:

switch(msg)
{
    case "hi":
    // do something
    break;

    // ...
}

And even if, for, foreach, while statements...

if(a == 1)
{
    bool varIsEqual = true;
    isBusy = true;
    // do more work
}

But if you've only got 1 statement in a loop or if statement, you don't need the braces:

if("Samantha" != "Man")
    msg = "Phew!";
查看更多
登录 后发表回答