C# is too restrictive about local variable scopes #2574
-
I understand that C# has always been this way. I'm asking for it to be changed. Version Used: C# 7.3 Steps to Reproduce:
These are two different variables, and in reality, they have different scopes. Deleting the 2nd declaration of Other languages (e.g. TypeScript) get this right. There's no reason why C# can't figure it out. |
Beta Was this translation helpful? Give feedback.
Replies: 20 comments
-
This is intentional (and has been since 1.0) as it's a common source of bugs where someone adds a variable and doesn't realize it's now shadowing something else. If you want to explicitly show that you want this, you can, by writing: void Foo()
{
{
int x = 5;
Console.WriteLine(x);
}
{
int x = 6;
Console.WriteLine(x);
}
} C# asks you to make your intent clear to prevent accidental bugs. This is similar to how case-fallthrough is not a thing for case-blocks with statements in them. Instead, you have to do an explicit goto to convey that you want hte fallthrough to happen intentionally. |
Beta Was this translation helpful? Give feedback.
-
The problem isn't a compiler figuring it out. That's easy. The problem is the developer figuring it out. Local shadowing is a known vector for bugs in those languages that allow it. It makes the code more difficult to read and reason about. |
Beta Was this translation helpful? Give feedback.
-
I strongly disagree on this one. Almost every time I see that error, I think "Huh? But that's what I meant to do." I then end up renaming one of the variables something like "x2", which probably confuses the next person who has to read the code just as much as the compiler error confused me.
That's the thing, though. In this scenario, neither variable is actually shadowing the other. If I had written something like this:
...then I would agree with you. Thanks for the reply, regardless. |
Beta Was this translation helpful? Give feedback.
-
I agree that allowing shadowing (especially without warnings) can lead to problems, and I'm on board with that. I'd just like the language to allow same-named variables in situations where there is no chance of shadowing. |
Beta Was this translation helpful? Give feedback.
-
i think the case distinctions here are far too subtle to matter. It's just not a good idea to do this given the confusion it can cause. The workarounds to express intent are also super simple to adopt. So i think the language is doing what it wanted in way that is appropriately painful, with the capability to escape the pain if desired. -- Note1: void Foo() {
{
int x = 5;
Console.WriteLine(x);
}
int x = 6;
Console.WriteLine(x);
} This honestly just feels bad to me. It feels like a straight up footgun that's going to cause a problem down the line. In any language, i would ask for the names of these variables to be different to make it clear, and to remove any chance of confusion about what each means and what is being referred to in any particular location. -- Note2: |
Beta Was this translation helpful? Give feedback.
-
This is very true. It's difficult to express in a GitHub issue the intricacies of all of the various cases that one might run into that would exemplify situations where the language is hurting you vs. where it is helping you. I think I'm most boggled when I have some large function (feel free to tell me that large functions are an anti-pattern) and some nested scope early on uses a variable name -- usually, a common one like "width" or "size" -- and later on, long after that scope is dead and gone, I want to write code that's doing a similar thing (dealing with rectangles, to continue my example) and the name I chose above happens to be the perfect name for the thing I'm doing down below. Also:
..this is perfectly legal code in C#, but I don't necessarily think that the code I'm wanting to write is any more "dangerous" or "foot-gun-ish" than this snippet. |
Beta Was this translation helpful? Give feedback.
-
The language did. And here we are :) Your perspective is perfectly valid. It comes down to an personal feeling on what is safe/reasonable/footgunny/etc. When designing the language, the prevalent belief (which i don't think has changed) is that thsi was more likely bad than good. And i don't think that opinion has changed over the years. So here we are. |
Beta Was this translation helpful? Give feedback.
-
void Foo() {
{
int x = 5;
Console.WriteLine(x);
}
x = 6;
Console.WriteLine(x);
} why such a need to redeclare it? EDIT: Appears my brain stopped working while writing this. Ignore it |
Beta Was this translation helpful? Give feedback.
-
@johnkellyoxford You should test before you write. 😉 |
Beta Was this translation helpful? Give feedback.
-
I get why retroactive shadowing is not legal (even if I don't like it) but I really don't get why C# doesn't let this compile: IFoo qaz = new Bar();
IFoo zomp = new Bar();
if (qaz is Bar b){
}
if (zomp is Bar b){ // error CS0128: A local variable or function named 'b' is already defined in this scope
} when it's perfectly okay with this: IFoo qaz = new Bar();
IFoo zomp = new Bar();
if (qaz is Bar){
Bar b = qaz as Bar;
}
if (zomp is Bar){
Bar b = zomp as Bar;
} Like, I understand why from an implementation perspective, but it feels really weird from a usage perspective. |
Beta Was this translation helpful? Give feedback.
-
So, let's think about this code a second: IFoo qaz = new Bar();
IFoo zomp = new Bar();
if (qaz is Bar b){
} Tihs is equivalent to: IFoo qaz = new Bar();
IFoo zomp = new Bar();
if (qaz is Bar b){
// b is assigned here
} else {
// b is not assigned here
} Why is that important? Because people could certainly want to write: IFoo qaz = new Bar();
IFoo zomp = new Bar();
if (qaz is Bar b){
// b is assigned here
} else {
b = GetBBySomeOtherMeans();
}
// b is now assigned and usable here. The main problem is that there are two reasonable things people want to do. The first is that they only really care in the truthy branch and don't want the vairable to live beyond that. The second is that they do want it live beyond that, but they need to handle all the ways it would need to be assigned. A pattern you'd often see is actually more like: if (!(qaz is Bar b)) {
b = ...
}
// use 'b' here If 'b' had 'narrow' scope, this wouldn't be possible. We felt it was important to support this case, so we gave these variables a 'wider' scope. For the case when you want a narrow scope, you can simulate doing that by using unique variable names. We felt this was an acceptable tradeoff that enabled enough scenarios we wanted, while not being too onerous. |
Beta Was this translation helpful? Give feedback.
-
That subject was a bit of a controversy here. The team decided that by having pattern variables and It is a little wonky, especially since the scope leakage changes based on what language construct you use. The scope is narrow in |
Beta Was this translation helpful? Give feedback.
-
It's true that it is a tradeoff, but what is the recourse for those of us who disagree with which side of the tradeoff the team ultimately landed on? We don't have any real way to influence how the language behaves, other than voicing our opinion in these issues, which doesn't really seem to do much. |
Beta Was this translation helpful? Give feedback.
-
There are a few options:
It definitely has an impact. These opinions are evaluated as part of the LDM decision making process. That said, C# doesn't have a goal of satisfying all users. Decisions always end up usually upsetting some subset of users. That is known, understood, and accepted as part and parcel of having a language with a multi-million userbase. |
Beta Was this translation helpful? Give feedback.
-
It was a member of the community that got the LDM to reverse the decision that the leaking scope would also apply to But when it comes down strictly to style vs. function, function generally wins. With a wider scope you can't reuse identifiers, but identifiers are cheap and easy to define, so the impact to the developer is minimal. But with a narrow scope, attempting to get that value into the wider scope becomes more difficult and you end up having to declare multiple variables and reassign them from within the narrow scope. The team thought that this was fairly burdensome for what they considered to be a common use case. |
Beta Was this translation helpful? Give feedback.
-
@fr0 I'd also recommend reading: https://devblogs.microsoft.com/dotnet/the-net-language-strategy/ |
Beta Was this translation helpful? Give feedback.
-
Another, real-world example of having to rename variables:
Although in this instance it's |
Beta Was this translation helpful? Give feedback.
-
I agree. Variables being in scope in the entire block they're declared in, instead of just from the declaration point is my single biggest issue with C#. I agree with @fr0, that while preventing shadowing makes sense, this is just a silly restriction: internal class Program
{
private static void Main()
{
// int i declared on line 14 is in scope here >.>
if (true)
{
for (int i = 0; i < 10; i++) // CS0136 A local or parameter named 'i' cannot be declared in this scope because that name is used in an enclosing local scope to define a local or parameter
{
}
}
int i;
for (i = 0; i < 10; i++)
{
}
}
}
C# Spec 8.5.1
|
Beta Was this translation helpful? Give feedback.
-
New to C# and I am running out of temporary variable names due to this strange restriction. Rules don't seem to make sense or seem to contradict themselves e.g. for (int g = 0; g < 5; ++g)
{
//something
}
for (int g = 0; g < 5; ++g) // this by itself is fine???
{
//something
}
int g = 5; //but declare another g and suddenly compiler errors For functions with high cyclomatic complexity, I am beginning to run out of names for temporary variables and it's starting to hamper my code quality. When variables are declared within the scope of a loop or conditional, I cannot touch them once I've left the scope. Intuitively one would assume the variable is 'gone'. While they may not have been GC'd yet, or whatever internal mechanism is used for disposing of stack variables, I don't see why the compiler wouldn't simply internally rename the proceeding instances of the same variable name, as the redundancy is purely for my dumb human brain's sake. |
Beta Was this translation helpful? Give feedback.
-
@nauticaldev, since you're new to C#, it's worth pointing out that the issue with your examples isn't that the variables from the for statements prevent the later declaration of The language definition specifies that all variables are consider to have been declared at the top of their scope, so your example is effectively this:
Thus, the for loops now conflict with the You also wrote
If you dig back into the reasons this decision was made in the original C# - way back around the year 1999 or 2000, this is exactly why the compiler works this way. Variable shadowing is an extremely fertile source of bugs exactly because we all have dumb human brains. You and me both. Compiler complexity wasn't the issue - programmer productivity and the "pit of success" was the issue. Many C# design decisions were driven by a desire to prevent bugs from happening in the first place - this is one of those. |
Beta Was this translation helpful? Give feedback.
This is intentional (and has been since 1.0) as it's a common source of bugs where someone adds a variable and doesn't realize it's now shadowing something else. If you want to explicitly show that you want this, you can, by writing:
C# asks you to make your intent clear to prevent accidental bugs. This is similar to how case-fallthrough is not a thing for case-blocks with statements in them. Instead, you have to do an explicit goto to convey that you want hte fallthrough to happen intentionally.