18

我正在清理一些代码并删除了if不再需要的语句。但是,我意识到我忘了删除括号。这当然是有效的,只是创建了一个新的本地范围。现在这让我思考。在我多年的 C# 开发中,我从来没有遇到过使用它们的理由。事实上,我有点忘了我能做到。

定义本地范围有什么实际好处吗?我知道我可以在一个范围内定义变量,然后在不相关的范围内(for、foreach 等)再次定义相同的变量,如下所示:

void SomeMethod()
{    
    {
        int i = 20;
    }

    int i = 50; //Invalid due to i already being used above.
}

void SomeMethod2()
{    
    {
        int i = 20;
    }
    {
        int i = 50; //Valid due to scopes being unrelated.
    }
    {
        string i = "ABCDEF";
    }
}

定义本地范围的真正意义是什么?实际上可以有任何形式的性能提升(或潜在的损失)吗?我知道您可以在 C++ 中执行此操作,并且是帮助您管理内存的一部分,但是因为这是 .NET,真的会有好处吗?这只是让我们定义随机范围的语言的副产品,即使没有真正的好处?

4

5 回答 5

8

在 C# 中,将一组语句转换为单个语句是纯粹的语法。对于任何需要单个语句的关键字都是必需的,例如 if、for、using 等。一些极端情况:

  • switch 中的case关键字很特殊,因为它不需要它是单个语句。breakgoto关键字结束它。这就解释了为什么你可以使用大括号来阻塞变量声明。
  • trycatch关键字很特殊,即使后面只有一条语句,它们也需要大括号。非常不寻常,但可能是由于迫使程序员考虑块内声明的范围而受到启发,由于异常处理的工作方式,catch 块不能引用 try 块内的变量。

用它限制局部变量的范围是一个失败的原因。这在 C++ 中很重要,因为结束大括号是编译器将在作用域块内注入对变量的析构函数调用的地方。这是 RAII 模式一直使用的 ab/used,在程序中使用标点符号并没有什么非常漂亮的东西会产生如此严重的副作用。

C# 团队对此没有太多选择,局部变量的生命周期受到抖动的严格控制。它忽略了方法内的任何分组结构,它只知道 IL。除了 try/except/finally,它没有任何分组结构。任何局部变量的范围,无论它写在哪里,都是方法的主体。当您在编译的 C# 代码上运行 ildasm.exe 时可以看到一些东西,您会看到局部变量被提升到方法体的顶部。这也部分解释了为什么 C# 编译器不允许您在另一个范围块中声明另一个同名的局部变量。

抖动有关于局部变量生存期的有趣规则,它们完全取决于垃圾收集器的工作方式。当它 jit 一个方法时,它不仅会生成该方法的机器代码,还会创建一个表,描述每个局部变量的实际范围、初始化它的代码地址和不再使用它的代码地址. 垃圾收集器根据活动执行地址使用该表来确定对对象的引用是否有效。

这使得它在收集对象方面非常有效。与本机代码互操作时有时效率太高且麻烦,您可能需要神奇的 GC.KeepAlive() 方法来延长生命周期。一个非常了不起的方法,它根本不生成任何代码。它的唯一用途是让抖动改变表并为变量生命周期插入更大的地址。

于 2013-06-01T12:17:37.590 回答
4

就像函数一样,这些“块”几乎只是为了隔离(大部分)不相关代码的区域及其在函数中的局部变量。

如果您需要一些临时变量只是为了在两个函数调用之间传递,您可以使用它,例如

int Foo(int a) {

    // ...

    {
        int temp;
        SomeFuncWithOutParam(a, out temp);

        NowUseThatTempJustOnce(temp);            
    }

    MistakenlyTryToUse(temp);    // Doesn't compile!

    // ...

}

然而,有人可能会争辩说,如果您需要这种词法作用域,那么内部块无论如何都应该是自己的函数。

至于性能等。我非常怀疑它是否重要。编译器将函数视为一个整体,并在确定堆栈帧大小时收集所有局部变量(甚至是内联声明的变量)。所以基本上所有的局部变量无论如何都集中在一起。对变量的使用给予更多的限制是纯粹的词汇方面的事情。

于 2013-06-01T10:03:23.093 回答
3

局部范围可能有用的地方:case语句的内部switch语句。switch默认情况下,所有案例都与语句共享相同的范围。case不允许在多个语句中声明具有相同名称的局部临时变量,并且您最终可能只在第一个 case 语句中或什至在switch-statement 之外声明该变量。您可以通过给每个 case 语句一个局部范围并在该范围内声明临时变量来解决这个问题。但是不要让你的案例太复杂,这个问题可能表明最好调用一个单独的方法来处理case-statement。

于 2013-06-01T10:37:24.910 回答
2

优点主要是它使语言的定义更简单。

定义现在可以简单地声明 if、while、for 等应该后跟一条语句。括号内的一组语句只是另一种可能的语句。

禁止您的示例中使用的语句块并没有真正的好处。有时它们对于避免名称冲突很有用,但您可以在没有名称的情况下解决您的问题。与 C、C++ 和 Java 等语言相比,它还会不必要地引入语法规则的差异。

如果您想知道它也不会改变引用对象的对象生命周期。

于 2013-06-01T10:57:07.903 回答
0

至少在发布模式下不会有性能提升:如果 GC 知道 - 或至少认为 - 无论范围如何,它都将不再使用它,它可以收集一个对象。这可能会使您受到非托管互操作的困扰:http: //blogs.msdn.com/b/oldnewthing/archive/2010/08/13/10049634.aspx

也就是说,我不时使用这个“特性”来确保方法后面的代码不能使用一些临时对象。在大多数情况下,它可能可以通过拆分为其他方法来完成,但有时会变得不必要地笨拙,或者由于某种原因是不可能的(例如,在您设置只读成员的构造函数中)。有时我会使用它来重用变量名,但这通常与“临时对象”的原因相结合。

于 2013-06-01T13:26:34.813 回答