可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I was cleaning up some code and removed an if
statement that was no longer necessary. However, I realized I forgot to remove the brackets. This of course is valid and just created a new local scope. Now this got me thinking. In all my years of C# development, I have never come across a reason to use them. In fact, I kind of forgot I could do it.
Is there any actual benefit to defining a local scope? I understand I can define variables in one scope and then define the same ones again in an unrelated scope (for, foreach, etc.) like below:
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";
}
}
What's the true point of defining a local scope? Can there actually be any sort of performance gain (or potentially a loss)? I know you could do this in C++ and was part of helping you manage memory, but because this is .NET, would there really be a benefit? Is this just a bi-product of the language that let's us define random scopes even though there is no true benefit?
回答1:
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.
回答2:
Just like functions, these "blocks" are pretty much just to isolate regions of (mostly) unrelated code and their local variables within a function.
You might use it if you needed some temporary variable just to pass between two function calls, for example
int Foo(int a) {
// ...
{
int temp;
SomeFuncWithOutParam(a, out temp);
NowUseThatTempJustOnce(temp);
}
MistakenlyTryToUse(temp); // Doesn't compile!
// ...
}
One might aruge, however, that if you need this kind of lexical scoping, the inner blocks should be functions on their own anyway.
As for performance, etc. I highly doubt it matters at all. The compiler looks at the function as a whole, and collects all local variables (even ones declared in-line), when determining the stack frame size. So basically all local variables are lumped together anyway. It's a purely lexical thing to give you a little more constraint on your variables' usage.
回答3:
There is place where local scopes could be useful: inside case
statements of a switch
statement. All cases share by default the same scope as the switch
statement.
Declaring a local temporary variable with the same name inside multiple case
statements is not allowed, and you could end up with declaring the variable only in the first case-statement or even outside the switch
-statement.
You can solve this problem by giving each case statement a local scope and declare the temporary variable inside the scope.
However don't make your cases too complex, this problem might be an indication that it's better to call a separate method to handle the case
-statement.
回答4:
The advantage is mostly that it makes the definition of the language simpler.
The definiton can now simply state that if, while, for, etc.. should be followed by a single statement. A set of statements within parentheses is simply one more possible kind of statement.
There is no real gain by forbidding statement blocks as used in your example. Sometimes they are useful to avoid name clashes but you could solve your problem without. It would also unneccesarily introduce a difference in syntax rules as compared to languages like C, C++ and Java.
In case you wonder it also does not change the object lifetimes of the referenced objects.
回答5:
There won't be a performance gain, at least in release mode: the GC can collect an object if it knows - or at least thinks - it will no longer be used, regardless of scope. This can screw you with unmanaged interop: http://blogs.msdn.com/b/oldnewthing/archive/2010/08/13/10049634.aspx.
That said, I use this "feature" from time to time to ensure that code later in the method can't use some temporary object. In most cases it could probably be accomplished by splitting into additional methods, but occasionally that gets unnecessarily unwieldy, or is for some reason impossible (e.g. in a constructor where you are setting readonly members). And once in a while I use it to be able to reuse variable names but that's usually coupled with the "temporary object" reason.