在C#中,什么时候可以在语句中不使用作用域?

26

最近我发现在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) );
}

替代 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) );

这可能是一个可以用很多其他方式解决的不好的例子。

有没有任何模式,在这种情况下,“无语句作用域”特性是一种良好的实践方法?


5
一开始使用这样的作用域感觉很有用,因为它意味着您可以使用相同的变量名称和其他与作用域相关的活动。然而,在实际应用中,应避免过度嵌套,因为它会影响可读性,如果您能够像这样分离作用域,则可能应该将代码重构为单独的方法。 - Steve Lillis
嗨,感谢您的回答。但是什么情况下会使用这种做法?或者这通常被认为是一种不好的做法吗?为什么他们决定在那种情况下实现这个功能? - Dirk Boer
3
我相信你所写的内容是“裸块”(bare blocks)的一个完全合理的应用场景。当然,如果你有很多这样的块排成一行,应该考虑将每个块的共同部分重构为一个新方法,但有时你确实需要连续进行两次几乎但又不完全相同的操作。 - Ilmari Karonen
2
@rhughes 作用域不会影响GC的回收规则,除非附加了调试器。如果没有附加调试器,则变量在方法中最后一次使用后就可以被回收(并且在方法外没有引用)。 - Scott Chamberlain
3
不,你是错误的。如果你有100行代码,并且对象在第2行被最后读取,GC可以在第3行收集和释放该对象的内存,即使该变量在另外97行中仍未“超出范围”(这仅适用于当你没有连接调试器时)。 - Scott Chamberlain
显示剩余5条评论
5个回答

23

在许多情况下,我认为可以使用的一种方法是将switch语句的每个选项封装在本地范围内。


后来添加:

C#源代码中存在的本地作用域块{ ... }似乎与生成的IL字节码无关。 我尝试了这个简单的例子:

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);
    }
}

这是在启用优化的发布版模式下编译的。根据IL DASM生成的结果如下:

.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

结论:

  • 在 IL 字节码中,局部作用域声明不存在任何形式。
  • C# 编译器将为可能存在冲突的局部变量选择新名称(例如 C 方法中的第二个变量 o)。
  • 认为引入 C# 源代码中的局部作用域可以使垃圾回收器尽早执行其工作的人是错误的(请参见其他答案和问题的评论)。

我还尝试以 调试 模式(无优化)编译此代码。局部作用域的开始和结束似乎仅显示为 nop指令(“无操作”)。在某些情况下,来自不同局部作用域的两个具有相同名称的局部变量被映射到 IL 中的同一局部变量,就像上面命名为 C 的 C# 方法一样。仅当它们的类型兼容时才可能对两个变量进行“统一”。


这个。我在Java中没有做这个,结果被咬了一口。我假设每个情况已经是它自己的作用域。 - Justin
我认为Chris Eelma的回答更直接,但非常感谢你提供如此有趣和详尽的答案!所有的点赞都很有意义 :) - Dirk Boer

12
如果你问他们为什么实现了这个功能?
我开发了自己的语言,我发现“没有语句的范围”语法很容易出现在语法中。这是一个副作用,当编写一个漂亮且易于阅读的语法时。
在我的语言中,我拥有同样的功能,但我从未明确地“设计”过它 - 我免费获得了它。起初,我甚至不知道我的语法允许这样做。
“{}”是一个“复合语句”,因为它简化了所有想要使用它的地方的语法(条件语句,循环体等)...并且因为它让你在控制单个语句时可以省略大括号(例如,“if(a <10)++ a;”等)。它可以在任何语句出现的地方使用,这是直接产生的;它是无害的,并且偶尔有帮助,正如其他答案所说。“如果它没坏,就不要修理它。”- keshlam。
因此,问题不在于“他们为什么要实现它”,而在于“为什么他们没有禁止它/为什么他们允许这样做?”我能否禁止我的语言中的特定功能?当然可以,但我认为没必要-这会增加公司的成本。
现在,上面的故事可能是真实的,也可能不是C#的情况,但我不是设计语言的人(也不是真正的语言设计师),所以很难说为什么,但我还是想提一下。
相同的功能也存在于C++中,它实际上有一些用例-如果对象超出范围,则允许运行对象的终结器,尽管在C#中不是这种情况。

话虽如此,但在我使用C#的4年中,我从未使用过具体术语(嵌套语句 -> 块),也没有看到它被用在任何地方。你的例子需要重构成方法。

看一下C#语法:http://msdn.microsoft.com/en-us/library/aa664812%28v=vs.71%29.aspx

此外,正如Jeppe所说,我使用'{}'语法来确保'switch'结构中的每个case块都有单独的局部作用域:

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

    case 2:
    {
        int b = 10;
        break;
    }
}

当然。'{}'是一个“复合语句”,因为这简化了你想要使用它的所有地方(条件语句、循环体等)的语法...而且因为这样可以在控制单个语句时省略大括号('if (a<10) ++a;'等)。它可以在任何语句出现的地方使用,这一点直接体现了它的优点;正如其他答案所说,它是无害的,有时还会有帮助。“如果它没有坏,就不要修理它。” - keshlam
有趣的是,在这里有一个关于switch语句case作用域的有趣讨论:http://programmers.stackexchange.com/a/195254/9433。 - StuartLC

11
您可以使用{}来重新定义变量名称(即使是不同的类型):
{
   var foo = new SomeClass();
}
{
   Bar foo = new Bar(); // impairs readability
}

然而,这种方式重复使用变量会影响可读性。

因此,在大多数情况下,代码不应该使用“未经请求”的作用域块而没有前置语句,而应相应地将其重构为单独的函数。

编辑

在我看来,每当需要命令式地重置本地可变变量值或重新用于其他或替代问题时,就会出现问题。例如,原始代码可以重构为:

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

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

我认为这样可以传达意图,既不会出现 # 回退无法应用的风险,也无需使用显式的本地可变变量来保存状态。

(或者创建一个带有默认值 #MenuItem 的重载配置文件,或将 MenuItem 的创建移动到工厂方法中等等。)

编辑
关于作用域对生命周期没有影响

作用域可以在方法内部使用,以限制昂贵对象的生命周期

我的最初发布错误地说明了局部作用域可以用于影响对象的生命周期。这是不正确的,对于 DEBUGRELEASE 构建版本都是如此,而且无论变量名称是否被重新分配,正如 Jeppe 的 IL 反汇编所示,还有这些单元测试。感谢 Jeppe 指出这一点。此外,Lasse 指出,即使没有明确地超出作用域,也不再使用的变量在释放构建中将有资格进行垃圾回收。

TL;DR 尽管使用非请求作用域可能有助于向人传达变量作用域的逻辑使用方式,但这样做对于在同一方法中对象是否变得可回收没有任何影响。

换句话说,在下面的代码中,作用域甚至重新定义下面的变量 foo 对对象的寿命完全没有影响。

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"
}

在X点:

  • DEBUG构建中,尽管采用了试图引导垃圾收集器执行的hacky方法,但两个foo变量都不会被收集。
  • RELEASE构建中,无论作用域如何,只要不再需要,两个foo都将被标记为可回收。当然,回收的时机应该由垃圾回收器自行决定。

11
在不影响寿命的情况下,如果一个变量在方法中不再使用,则其引用的对象在发布版本中(未连接调试器时)可以进行回收。 - Lasse V. Karlsen
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - TomTom
Lasse是正确的。垃圾收集器可能会删除任何将不再使用的对象,并且它不会查看C#源代码中的那些花括号。因此,这与生命周期无关。如果一个线程在睡眠几个小时的同时,其他线程为新对象分配了大量空间,则不再使用的对象可能会被收集。 - Jeppe Stig Nielsen
我甚至会重构它,使其循环遍历一个 Tuple<int, string> 列表。 - user1804599
@JeppeStigNielsen 感谢您,Jeppe - 你和Lasse的观点都被证明了。我已经更新了我的错误帖子。 - StuartLC

3

这种模式对C#的运行时几乎没有影响,所以它纯粹是一种美学(与C++相比,我们经常使用此模式和RAII来管理诸如锁定之类的事物)。

如果我有两个完全不相关的代码块,有时我会以这种方式对它们进行作用域限定,以使程序员清楚地知道他们必须记住哪些变量是“可能在前一个块中被修改的”。它填补了大代码块和独立函数之间的空白;我可以共享某些变量而不是其他变量。

我还会在自动生成的代码周围使用它。这样的插件块通常更容易处理,而不必担心相互作用。

当我使用这个时,我喜欢在每个块之前放置一个注释,大致在if语句的位置,解释该块将做什么。我发现这有助于避免其他开发人员认为“这看起来像曾经有控制流,但有人搞乱了它。” 在这种情况下,可能有点过头,但您会明白的:

// 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) );
}

如前所述,这在C#中仅是纯粹的风格问题。在有意义的地方随意使用自己的风格。


0
{
    string f = "hello";
}

看起来很奇怪。

显然,方法需要它们:

private void hmm() {}

以及 switch 语句:

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

    // ...
}

即使是for、foreach、while语句...

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

但如果您的循环或if语句中只有1个语句,则不需要大括号:

if("Samantha" != "Man")
    msg = "Phew!";

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接