19

Clarification of question: I am not looking for answers on how to solve this issue (several are listed below), but as to why it is happening.

I expect the following code to compile:

struct Alice
{
    public string Alpha;
    public string Beta;
}

struct Bob
{
    public long Gamma;
}

static object Foo(dynamic alice)
{
    decimal alpha;
    long beta;

    if (!decimal.TryParse(alice.Alpha, out alpha) // *
        || !long.TryParse(alice.Beta, out beta)) // **
    {
        return alice;
    }

    var bob = new Bob { Gamma = beta }; // ***

    // do some stuff with alice and bob

    return alice;
}

However the following compile time error is thrown at // ***:

Use of unassigned local variable 'beta'

I can make the program compile under the following situations:

  1. If I change the signature to be

    static object Foo(Alice alice)

  2. Explicitly casting on the lines // * and // **, e.g.:

    !long.TryParse((string)alice.Beta, out beta).

  3. Removing the decimal.TryParse on line // *.

  4. Replacing the short circuit or || with |. Thanks to HansPassant

  5. Swapping the TryParses around

  6. Pulling the results of the TryParses into bools Thanks to Chris

  7. Assigning a default value to beta

Am I missing something obvious, or is there something subtle going on, or is this a bug?

dav_i
  • 27,509
  • 17
  • 104
  • 136
  • 2
    What if you simply assign `beta` as `long beta = 0;`. It is always a good idea to assigned a default value of some kind. – rhughes Jul 23 '14 at 09:33
  • 1
    @rhughes It's not necessary to set a default value with an `out` argument as the method is required to provide a value to it. – dav_i Jul 23 '14 at 09:34
  • 3
    Looks similar to [link](http://stackoverflow.com/questions/6051711/why-is-the-c-sharp-compiler-claiming-use-of-an-unassigned-variable-prior-to-y). Unfortunately accepted answer is "I appears to be a compiler bug (or limitation, if you prefer)." – Galina Jul 23 '14 at 10:02
  • Interestingly, playing with the code sample seems to indicate it is more to do with type conversion as `int.TryParse()` fails too but `decimal.TryParse()` works fine. – rhughes Jul 23 '14 at 10:22
  • @rhughes Orly? That __is__ interesting. Lemme play... – dav_i Jul 23 '14 at 10:23
  • 1
    Another fix seems to be to do the two TryParses into a couple of temporary booleans before the if statement. – Chris Jul 23 '14 at 10:23
  • @rhughes I can't reproduce that quirk I'm afraid. Can you [gist](https://gist.github.com/) your code? – dav_i Jul 23 '14 at 10:26
  • @rhughes: If I change the types and the tryParse to decimal it still fails for me. What changes did you make exactly (and are you sure you haven't got one of the fixes in there too)? – Chris Jul 23 '14 at 10:26
  • @Chris I only changed the `TryParse`, not the type in `Bob`. Changing the type in `Bob` as well gives the original error... – rhughes Jul 23 '14 at 10:28
  • @svick I rolled back your change as the [tag:visual-studio-2012] and [tag:c#-5.0] tags are needed because this deals with certain versions of the Microsoft compiler and may not be an issue with Mono. – dav_i Jul 23 '14 at 14:40

10 Answers10

12

I don't know answer for sure, but for me it looks like compiler bug or "by design" issue.

I played with your sample a bit, reducing it bit by bit, and here is what left from it:

    private static bool Default<T>(out T result)
    {
        result = default(T);
        return true;
    }

    private static void Foo()
    {
        int result;

        if (true || Default(out result))
        {
            return;
        }

        Console.WriteLine(result);
    }

Which also fails with

error CS0165: Use of unassigned local variable 'result'

You can play with int result in Foo to check any type you want.

Please notice there's no dynamic usage, and also please notice true branch which should immediately return.

So for me it looks like VS.Net compiler is "not intelligent enough" here.

What is good with this piece of code - it can be compiled with compilers prior to .Net 4 (using csc.exe from appropriate frameworks), so here are the results:

  • .Net 2.0

Build ok, warnings:

warning CS0429: Unreachable expression code detected

warning CS0162: Unreachable code detected

  • .Net 3.5

Build failed:

error CS0165: Use of unassigned local variable 'result'

So if it is a bug, it appears somewhere in between .NET 2 and .NET 3.5

Lanorkin
  • 7,310
  • 2
  • 42
  • 60
  • 7
    Interesting analysis. We need to somehow summon Eric Lippert in to give us definitive commentary on this! :) – Chris Jul 23 '14 at 11:40
  • Nice answer. It does appear to be a bug in later versions of the compiler then as the .Net 2.0 result is what I'd expect from the code sample you gave. – dav_i Jul 23 '14 at 11:41
  • 2
    Its also worth noting that the generics don't seem necessary here either (ie just `Default(out int result)` seems to exhibit this behaviour). – Chris Jul 23 '14 at 11:42
  • @Chris Yes, I used generics just for easier checking for value / ref issues – Lanorkin Jul 23 '14 at 11:44
4

It happens because the dynamic keyword causes a lot of changes in the generated code structure (generated by the C# compiler).

You can observe this using a tool such as .NET reflector (I suggest to select 'None' for C# inference so you can really see all the generated stuff). Basically, each time you access a dynamic object, the generated code adds at least an if case. These ifs can cause important code paths changes.

For example, this simple C# code

    static void MyFoo(dynamic dyn)
    {
        if (dyn == null)
            return;

        var x = dyn;
    }

is generated as:

private static void MyFoo([Dynamic] object dyn)
{
    object obj2;
    CSharpArgumentInfo[] infoArray;
    bool flag;
    if (<MyFoo>o__SiteContainer0.<>p__Site1 != null)
    {
        goto Label_0038;
    }
    <MyFoo>o__SiteContainer0.<>p__Site1 = CallSite<Func<CallSite, object, bool>>.Create(Binder.UnaryOperation(0, 0x53, typeof(Program), new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(0, null) }));
Label_0038:
    if (<MyFoo>o__SiteContainer0.<>p__Site2 != null)
    {
        goto Label_0088;
    }
    <MyFoo>o__SiteContainer0.<>p__Site2 = CallSite<Func<CallSite, object, object, object>>.Create(Binder.BinaryOperation(0, 13, typeof(Program), new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(0, null), CSharpArgumentInfo.Create(2, null) }));
Label_0088:
    if ((<MyFoo>o__SiteContainer0.<>p__Site1.Target(<MyFoo>o__SiteContainer0.<>p__Site1, <MyFoo>o__SiteContainer0.<>p__Site2.Target(<MyFoo>o__SiteContainer0.<>p__Site2, dyn, null)) == 0) == null)
    {
        goto Label_00AE;
    }
    obj2 = dyn;
Label_00AE:
    return;
}

Let's take a another simple example. This code:

    static void MyFoo1(dynamic dyn)
    {
        long value;

        if (long.TryParse(dyn, out value))
            return;

        var x = value;
    }

compiles fine. This one

    static void MyFoo2(dynamic dyn)
    {
        long value;

        if (true || long.TryParse(dyn, out value))
            return;

        var x = value;
    }

doesn't. If you look at the generated code (set value to 0 to make sure it compiles), it will simply contain some extra ifs and goto that change the whole code path quite drastically. I don't think it's a bug, maybe more a limitation of the dynamic keyword (that has quite a few, for example: Limitations of the dynamic type in C#)

Community
  • 1
  • 1
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
3

It is just for || operator usage (Logical OR),

compiler checks the criteria from left to right, so if the first operand evaluate as TRUE the second one does not needs to be evaluated, so Beta may not get a value and compiler throw bellow warning:

"Use of unassigned local variable 'beta'"

Your code:

    if (
        !decimal.TryParse(alice.Alpha, out alpha)  // If evaluated as TRUE...
        ||                                         // 
        !long.TryParse(alice.Beta, out beta))      // ....so Will not evaluated this.
{

Note that if you change the operands order you will get bellow message:

"Use of unassigned local variable 'alpha'"
Mohammad Golahi
  • 367
  • 3
  • 8
  • but if "the second one does not needs to be evaluated" like you said the compiler should be able to compile this code because its like `.TryParse((string)null, out beta)` which is valid IMHO – WiiMaxx Jul 31 '14 at 08:18
  • +1 This is the correct answer, why others keep struggling with `dynamic`.The reason is unrelated with DLR. – Cheng Chen Aug 01 '14 at 09:24
  • @WiiMaxx: If `TryParse` is not evaluated, the variable is unassigned. If `.TryParse((string)null, out beta)` is evaluated, the variable `beta` is assigned, even it's null. The two scenarios are different. – Cheng Chen Aug 01 '14 at 09:26
2

Assign default values to beta and alpha when you declare

decimal alpha=default(decimal);
long beta =default(long); 
Ajay Kelkar
  • 4,591
  • 4
  • 30
  • 29
  • Hi, thanks for your response but I'm not looking for ways of solving the issue as there are several posted in the question itself. I'm looking for the reason *why* this is happening. – dav_i Jul 25 '14 at 14:23
2

Well, here is my attempt:

When alice is of type 'Alice', the compiler knows at compile-time which TryParse method is going to be called, and it knows the method has an 'out' parameter, meaning it will be initialised by the method. So, in case alice is of type Alice, either decimal.TryParse will return false, in which case Bob will not be constructed (because the method returns alice), or decimal.TryParse will return true, and in that case long.TryParse will be called, and the compiler knows that this will assign a value to beta (because of the out keyword).

Now, if alice is of type dynamic, the compiler doesn't care about the signature of the TryParse method and postpones the resolution of the method (based on the actual type of alice) at run-time. That means he cannot make any assumption about the fact that beta will be assigned a value or not by the call to TryParse() and so cannot be sure that beta will be assigned a value when used for Bob's constructor.

My 2 cents...

Thierry
  • 342
  • 2
  • 6
1

it looks like i found the answer. its seems to be a compilation expression tree issue. at the compile time it checks if the variables are assigned a value. when your value assignment to alpha and beta variables take place inside an if condition it assumes that only first expression is evaluated.

this is evaluated all the time.. but since it is an 'or' condition..when the first expression is true it breaks off from the if without evaluating the second.

!decimal.TryParse(alice.Alpha, out alpha)

if you want to see this yourself put the TryParse statements sequentially like below. then that error would not come up..

!decimal.TryParse(alice.Alpha, out alpha) !long.TryParse(alice.Alpha, out beta);

here it is guaranteed that some value will be assigned to beta. I hope i made the point.

Eranjene S
  • 51
  • 3
0

It seems even prominent people have problems to decide whether this is a compiler bug or by design as can be seen here by looking at the two answers by Anthony D. Green, Program Manager, Visual Basic & C# Languages Team. This bug report basically has the same issue as described here, namely using a dynamic value that can lead to CS0165 while using another type solves it. Also splitting the && operation to two if statements instead solved the issue in that bug report.

The answer by Mr. Green has a link to this question which seems to be a duplicate of this one. The mystery behind all this is good explained in this answer and also by Mr. Green in the bug report linked above. The problem is that the value of a dynamic type could have an overloaded true operator which is invoked by || and the if statement and an own implementation could do simply anything so the compiler is cautious with that stuff. The linked information gives a better explanation though :)

Community
  • 1
  • 1
T_D
  • 1,688
  • 1
  • 17
  • 28
0

I assume this is happening because in your current code, with the dynamic included, the algorithm that the compiler uses to determine if TryParse is actually called at run-time is non-deterministic. I think the solutions your'e seeing are avoiding this issue by causing the evaluation of those TryParse calls to become deterministic (i.e. they will be evaluated). When those TryParse calls are evaluated with the out parameter, they will at worst receive the value of default(decimal) and default(long). So that when you assign it later, the value is not unassigned. This is an assumption based on the goto jumping that appears in the generated code

This is slightly supported by the Int32.TryParse documentation

When this method returns, contains the 32-bit signed integer value equivalent of the number contained in s, if the conversion succeeded, or zero if the conversion failed. The conversion fails if the s parameter is null or String.Empty, is not of the correct format, or represents a number less than MinValue or greater than MaxValue. This parameter is passed uninitialized.

This is definitely based on Simon Mourier's findings in his answer.

gudatcomputers
  • 2,822
  • 2
  • 20
  • 27
0

So I understand you want to know why this is happening. Like the error says, the compiler discovered some cases where the variable beta can be used without being initialized. As the documentation says:

Note that this error is generated when the compiler encounters a construct that might result in the use of an unassigned variable, even if your particular code does not.

The compiler is probably not smart enough to realize that the variable 'beta' will always be initialized if the code does not return inside the 'if' statement. This case might seem simple for the compiler to catch but I doubt that this could be a general feature to implement in the compiler.

-1
long beta;

What is actually happening in this statement is a declaration of this variable, but not an instantiation of the same. When you execute this statement the runtime reserves memory space for this variable as a long, but it does not instantiate the value to anything. So through the out parameter you are trying to assign an object to an uninstantiated object, In addition to the default value you could fix this by simply instantiating the object rather than simply reserving the memory required

long beta = new long();
theDarse
  • 749
  • 5
  • 13
  • This is just wrong. out - parameters don't have to be initialized before the call. See [MSDN - out parameter modifier (C# Reference)](http://msdn.microsoft.com/de-de/library/ee332485.aspx) – Felix Keil Jul 31 '14 at 11:15