什么时候在 C# 中使用没有语句的作用域?

Posted

技术标签:

【中文标题】什么时候在 C# 中使用没有语句的作用域?【英文标题】:When do you use scope without a statement in C#? 【发布时间】:2015-02-12 00:27:20 【问题描述】:

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

而不是即:

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

这可能是一个不好的例子,可以通过许多其他方式解决。

“没有声明的范围”功能是否有任何模式是一种好的做法?

【问题讨论】:

起初感觉像这样使用范围很有用,因为这意味着您可以使用相同的变量名和其他范围相关的活动。然而,实际上,应该避免过度嵌套,因为它会损害可读性,如果您能够像这样分离出范围,那么您可能应该将代码重构为单独的方法。 我倾向于喜欢单独的方法,纯粹是因为它可以重复使用。 我相信您所写的“裸块”的一个完全有效的用例。当然,如果你连续有很多这样的块,你应该考虑将每个块的公共部分重构为一个新方法,但有时你真的只需要连续两次做几乎但不完全相同的事情. @rhughes 范围界定不会影响 GC 的回收规则,除非附加了调试器。如果未附加调试器,则变量在方法中最后一次使用后有资格回收(并且在方法之外没有引用)。 @rhughes 不,你不正确。如果您有 100 行代码并且对象是最后一次从第 2 行读取的,则 GC 可以在第 3 行收集并释放该对象的内存,即使该变量在另外 97 行中没有“超出范围”(这只适用于当您没有附加调试器时) 【参考方案1】:

我认为在许多情况下可以接受的一种用法是将switch 语句的每个 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);
    

这是在 Release 模式下编译的(已启用优化)。根据 IL DASM 得到的 IL 是:

.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# 源代码中引入本地作用域时,认为垃圾收集器可能能够更早完成其工作(请参阅其他答案和问题的 cmets)的人是错误的。

我也尝试在 Debug 模式下编译它(没有优化)。本地范围的开始和结束似乎仅显示为nop instruction(“无操作”)。在某些情况下,来自不同局部范围的两个同名局部变量被映射到 IL 中的 same 局部变量,就像上面名为 C 的 C# 方法一样。只有当它们的类型兼容时,才能实现这样的两个变量的“统一”。

【讨论】:

这个。因为不这样做,我对 Java 有所了解。我假设每个案例已经是它自己的范围。 我认为 Chris Eelma 更直接地回答了这个问题,但非常感谢您真正有趣和扩展的回答!所有的赞成票都是有道理的:)【参考方案2】:

如果你问他们为什么要实现这个功能?

我开发了自己的语言,我发现“没有语句的范围”语法很容易爬到语法中。 如果你愿意的话,这是一个副作用,当你编写一个运行良好且易于阅读的语法时

在我的语言中,我拥有相同的功能,而且我从来没有明确地“设计”过它 - 我是免费获得的。我从来没有坐在我的办公桌上,想“哦,拥有这样的‘功能’不是很酷吗?”。哎呀-起初,我什至不知道我的语法允许这样做。

'' 是一个“复合语句”,因为它简化了 您想使用它的所有地方(条件、循环体、 等)......因为这样可以让你在单曲时省略大括号 语句被控制('if (a

它可以在任何可以出现语句的地方使用这一事实下降了 直接出;它是无害的,偶尔也会像其他的一样有用 答案已经说了。 “如果它没有坏,就不要修理它。- keshlam。

因此,问题不在于“他们为什么实施它”,而在于“他们为什么不禁止它/为什么允许这样做?”

我可以用我的语言禁止此特定功能吗?当然可以,但我认为没有理由这样做——这对公司来说是额外的成本。

现在,上面的故事可能适用于 C#,也可能不适用于 C#,但我没有设计语言(我也不是真正的语言设计师),所以很难说到底为什么,但我想我会提到它反正。

相同的功能在 C++ 中,它实际上有一些用例 - 如果对象超出范围,它允许对象的终结器确定性地运行,尽管 C# 不是这种情况。


也就是说,我在 4 年的 C# 中没有使用过那种特定的语法(embedded-statement -> block,当谈到具体的终端时),我也没有看到它在任何地方使用过.您的示例请求重构为方法。

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

另外,正如 Jeppe 所说,我使用了 '' 语法来确保 'switch' 构造中的每个 case-block 都有单独的本地范围:

int a = 10;
switch(a)

    case 1:
    
        int b = 10;
        break;
    

    case 2:
    
        int b = 10;
        break;
    

【讨论】:

当然。 '' 是一个“复合语句”,因为它简化了您想要使用它的所有地方的语法(条件、循环体等)......并且因为这可以让您在单个语句时省略大括号正在被控制('if (a任何地方出现的语句中直接使用它的事实不属于这一点;正如其他答案所说,它是无害的,并且偶尔会有所帮助。 “如果它没有坏,就不要修理它。” 出于兴趣,有一个关于 switch 语句大小写范围的有趣讨论 here。【参考方案3】:

您可以使用 来改变变量名的用途(即使对于不同的类型):


   var foo = new SomeClass();


   Bar foo = new Bar(); // impairs readability

但是,以这种方式重新利用变量会混淆可读性。

因此,在大多数情况下,应将代码相应地重构为单独的函数,而不是使用没有前面声明的“未经请求的”范围块。

编辑

IMO,任何时候需要强制重置局部可变变量值或将它们重新用于其他或替代问题,这都是异味的迹象。例如原代码可以重构为:

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

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

我相信这传达了意图,没有 # 回退不被应用的风险,也不需要显式的局部可变变量来保持状态。

(或new MenuItem(value > 45 ? "http://cheese.com" : "#"),或创建MenuItem的重载,默认为#,或将MenuItem的创建移入工厂方法等)

编辑Re:范围对寿命没有影响

作用域可以在方法中用于限制昂贵对象的生命周期

我最初的帖子错误地指出本地范围可用于影响对象的生命周期。这是不正确的,对于 DEBUGRELEASE 构建,无论变量名是否被重新分配,如 Jeppe 的 IL 反汇编和这些 Unit tests here 所示。感谢 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 构建中,foo 变量都不会被收集,尽管试图诱使 GC 这样做。 在RELEASE 构建中,无论范围如何,只要不需要它们,两个 foo 就可以有资格进行收集。当然,收集的时间应该留给自己的设备。

【讨论】:

寿命不应受到影响。在没有附加调试器的发布版本中,如果变量不再用于方法中,则它所引用的对象有资格被收集。 但它允许为了名称重用而进行隔离;) 并且有时单独的函数可能会更尴尬。像往常一样,它应该按照常识使用。 Lasse 是对的。垃圾收集器可以删除任何不再使用的对象,并且它不会查看 C# 源代码中的那些花括号。所以这与寿命无关。如果一个线程休眠了几个小时,而其他线程为新对象分配了大量空间,则可能会收集不再使用的对象。 我什至会重构它以遍历 Tuple&lt;int, string&gt; 的列表。 @JeppeStigNielsen 谢谢 Jeppe - 你和 Lasse 的观点都得到了证明。我更新了我不正确的帖子。【参考方案4】:

这种模式对 C# 的运行时几乎没有影响,所以它纯粹是一种美学的东西(与 C++ 相比,我们经常使用这种模式和 RAII 来确定锁等事物的范围)。

如果我有两个完全不相关的代码块,我有时会以这种方式确定它们的范围,以使其 100% 清楚程序员必须记住哪些变量“可能在前一个代码块中修改。它填补了空白在大代码块和隔离函数之间;我可以共享一些变量而不是其他变量。

我还将在自动生成的代码中使用它。使用这样的可插入块通常会容易得多,而不必担心交互。

当我使用它时,我喜欢在每个块之前添加注释,大致是 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# 中的风格。在有意义的地方随意使用自己的风格。

【讨论】:

【参考方案5】:

    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!";

【讨论】:

以上是关于什么时候在 C# 中使用没有语句的作用域?的主要内容,如果未能解决你的问题,请参考以下文章

js词法作用域

#抬抬小手学Python# Python 之作用域下的 global 和 nonlocal 关键字

js中作用域链的问题

JS高级. 05 词法作用域变量名提升作用域链闭包

作用域-基础知识总结------彭记(07)

js没有块级作用域但有函数作用域