23

I have two functions, one can be compiled and the other cannot. What is the difference?

Does function number 1 assume that case 1 always will be hit, or it just a compiler problem?

public void Test(int x)
{
    switch (x)
    {
        case 1:
            uint cId = (uint)3;
            break;

        case 2:
            cId = (uint)5; //NO ERROR HERE. WHY?
            break;
    }
}

public void DeclaringInsideSwitch(int x)
{
    uint tst = 0;
    switch (x)
    {
        case 1:
            int y = 3;
            uint variable = tst;
            break;

        case 2:
            variable++; //ERROR HERE. WHY?
            break;
    }
}

I tried of course searching for "Declaring variables inside switch case in C#", but to me it just seems like some sort of bug in C# now, preserved for backward compatibility.

// After getting a warning that it was already answered, my question can now be reduced to what it is really about.

Why:

int x;
x++;

doesn't this work?

Boann
  • 48,794
  • 16
  • 117
  • 146
justromagod
  • 933
  • 9
  • 20
  • So the difference is that you are only assigning a value to the variable in the first code but in the 2nd you actually try to use it (which fails since it has not been initialized) – Magnus Jul 23 '18 at 09:10
  • `variable++` first _reads_ the variable, then writes a new value that is one greater than what was read to the variable. But for local variables (that is variable declared inside methods/accessors/etc.), you are not allowed to read if the compiler cannot prove that the variable has been set. In your last example, this will be allowed: `case 2: variable = 100; variable++; break;` – Jeppe Stig Nielsen Jul 23 '18 at 09:35
  • My question is very close to "Variable declaration in C# switch statement", but you should see that variable++, even failed to compile! This made me really confused – justromagod Jul 23 '18 at 11:01
  • Did you read the error message? That should give you a *massive* clue what is going wrong. – Buh Buh Jul 23 '18 at 12:43
  • As a general answer on figuring out things like this: keep making the difference between two pieces of code smaller, until you get to the point where you can no longer change anything without also destroying the different behavior. In this case, you'd find assignment to a local is different than reading from a local. Your two examples are way too different to come to any conclusions unless you already know how the compiler translates the two whole blocks of code. There's a reason we ask for the shortest piece of code that reproduces a problem :) – Luaan Jul 23 '18 at 13:16
  • `uint cId = (uint)3` — is there a particular reason for using explicit type conversion here? – enkryptor Jul 23 '18 at 18:22

2 Answers2

24

Basically, the variable declartion is effectively wider than you think; the second example suffers from "definite assignment" since it is declared (wider), but not actually assigned, so ++ makes no sense on an unassigned value.

If you want scopes per case, you can do it... just add braces:

        switch (x)
        {
            case 1:
            {
                uint cId = (uint)3;
                break;
            }
            case 2:
            {
                uint cId = (uint)5;
                break;
            }
        }

Is it a little vexing? Yes. Is it anti-intuitive? Yes. Will it ever be changed? Unlikely, as it would be a significant breaking change that would stop a lot of existing C# from compiling.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • “that would stop a lot of existing C# from compiling” — do you think this is actually used in a significant portion of C# code? I mean, it’s not even commonly used in C code, and in C it’s a much more established idiom. – Konrad Rudolph Jul 23 '18 at 09:15
  • 8
    @KonradRudolph yes, yes I do – Marc Gravell Jul 23 '18 at 09:16
  • @MarcGravell Would you care to regale us with a story of an absurdly tiny breaking change causing absurd and impossible-to-predict issues? – DanielOfTaebl Jul 23 '18 at 12:40
  • 1
    @DanielOfTaebl: Not Marc, but consider the (non-C#) case of [how one developer just broke Node, Babel and thousands of projects in 11 lines of JavaScript](https://www.theregister.co.uk/2016/03/23/npm_left_pad_chaos/). If you change a dependency, **all** of its consumers will have to deal with the changes behavior. If you change how code compiles, **all** consumers (i.e. everyone who compiles C#) can be affected. Having an application that worked, was not touched, and now somehow doesn't work is incredibly unintuitive and leads to a lot of debugging work (what failed? why? what changed? ...) – Flater Jul 23 '18 at 13:19
  • @Fnclater That’s a completely different case. There’s precedent for making backward incompatible changes in widespread languages *without* affecting any code. Consider the case of C++11, which changed the meaning of the pre-existing keyword `auto`. There was no fallout whatsoever from that change. – Konrad Rudolph Jul 23 '18 at 13:33
  • 1
    @KonradRudolph - given the general quality of programmers, I think the number of programs containing a `switch` where the programmer originally tried to declare the same variable in multiple `case`s, got a "duplicate declaration" error and *did the minimum to clear this up by removing all but one declaration* is going to be vastly higher than the same situation where they really understood the issue and moved the declaration entirely outside of the `switch` (or introduced separate scopes for each `case`). – Damien_The_Unbeliever Jul 23 '18 at 13:50
  • @Damien_The_Unbeliever I feel that, if anything, this reinforces my point: people are, at an approximation almost exclusively, using the feature *wrongly*. — Fair enough, this will mean that a breaking change would *not* be entirely painless but it would almost certainly be beneficial in the long run, and eminently fixable in the short run. People are *way* too afraid of minor breaking changes. Yes, it will cause some work. *So what*? – Konrad Rudolph Jul 23 '18 at 14:01
  • 3
    @KonradRudolph the C# language design / compiler team care *massively* about back-compat; they have a very high bar *just to add a new warning* if that warning can trigger on existing code (since warn-as-error can be enabled, etc) - and the default response will be "add it via an analyzer instead"; I think the chances of selling this change are asymptotically close to zero. – Marc Gravell Jul 23 '18 at 15:30
  • @MarcGravell I’m actually aware of that — and I’m not even necessarily advocating to change this particular behaviour (though, for the record, I do fundamentally think that the C# team is wrong in their absolutism, even if I understand the reasons). I was merely contemplating how much code was actually using this (even in C) obscure feature, and how much code uses it intentionally. My suspicion, based on my knowledge of C and C# code bases, is: next to none. *And* I’m aware of its “legitimate” use in lexers (and Duff’s device). – Konrad Rudolph Jul 23 '18 at 15:49
  • 1
    @Daniel "Would you care to regale us with a story of an absurdly tiny breaking change..." that's the wrong question to ask. Nobody implements features because "hey there's almost no negative consequences". As Raymond Chen is wont to say: Every feature starts with -100 points and you have to show how why the feature is worth the large effort. So can you show a single professional program (i.e. not student homework) where the current behavior caused a serious bug? I find it hard to imagine. – Voo Jul 23 '18 at 21:33
16

Well, uint cId is defined within {...} scope which is in your case switch scope

switch (x)
{
    case 1:
        uint cId = (uint)3; // <- definition 
        break;

    case 2:
        // cId has been defined and thus can be assigned (initialization)
        cId = (uint)5; //NO ERROR HERE WHY?
        break;
}  // <- end of cId scope

In the second case variable is defined, but being a local variable must be initialized before use (increment):

switch (x)
{
    case 1:
        int y = 3;
        uint variable = tst; // <- definition 
        break;
    case 2:
        // variable defined, but has not been initialized ("case 1:" hasn't been run),
        // variable contains trash and so you can't increment it 
        variable++; //ERROR HERE WHY?
        break;
} // <- end of variable scope
Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
  • 1
    Now, I get it. I have compile error for ++, becuase variable not initialised. When for assigment it is not compile error. Thx – justromagod Jul 23 '18 at 11:05
  • Just to add a bit for future people, it may help to think of `variable++` as `variable = variable + 1` and since `variable` is undefined you get an exception – Marie Jul 23 '18 at 13:31