95

After I have migrated my project from VS2013 to VS2015 the project no longer builds. A compilation error occurs in the following LINQ statement:

static void Main(string[] args)
{
    decimal a, b;
    IEnumerable<dynamic> array = new string[] { "10", "20", "30" };
    var result = (from v in array
                  where decimal.TryParse(v, out a) && decimal.TryParse("15", out b) && a <= b // Error here
                  orderby decimal.Parse(v)
                  select v).ToArray();
}

The compiler returns an error:

Error CS0165 Use of unassigned local variable 'b'

What causes this issue? Is it possible to fix it through a compiler setting?

Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
ramil89
  • 851
  • 13
  • 22
  • 11
    @BinaryWorrier: Why? It only uses `b` after assigning it via an `out` parameter. – Jon Skeet Aug 12 '15 at 09:39
  • 1
    [The VS 2015 documentation says](https://msdn.microsoft.com/en-us/library/kx37x362.aspx) "Although variables passed as out arguments do not have to be initialized before being passed, the called method is required to assign a value before the method returns." so this does look like a bug yes, it is guaranteed to be initialised by that tryParse. – Rup Aug 12 '15 at 09:40
  • Yes, I'm surprised too. As I know `out` requires assigning inside method. So this error little bit strange. – ramil89 Aug 12 '15 at 09:43
  • @Jon: It hurts my eyes! Hang on, I've unrolled the linq syntax into lambdas and I can see what it's doing. My initial wibble was for locals being set as a side effect of the where clause, I can see it's perfectly legal (still hurts my eyes a little though). – Binary Worrier Aug 12 '15 at 09:46
  • 3
    Regardless of the error, this code exemplified everything that’s bad about `out` arguments. Would that `TryParse` returned a a nullable value (or equivalent). – Konrad Rudolph Aug 12 '15 at 09:48
  • 1
    @KonradRudolph `where (a = decimal.TryParse(v)).HasValue && (b = decimal.TryParse(v)).HasValue && a <= b` looks *much* better – Rawling Aug 12 '15 at 09:51
  • 1
    @Rawling No of course not, that code is silly; but that’s now how you’d actually write this. You’d write `from v in array let a = decimal.Parse(v) let b = decimal.Parse("15") where a <= b select v` (Or, if nullables were proper monads, you’d use `SelectMany` and write `from v in array from a in Parse(v) from b in Parse("15") where a <= b select v` — that’s actually valid C#, for suitable definitions of `Parse`). In *real* code (OP’s code makes absolutely no sense) this becomes even more readable. – Konrad Rudolph Aug 12 '15 at 10:48
  • Beyond the compiler error. Why are you doing a TryParse on each iteration? – Fernando Pelliccioni Aug 12 '15 at 15:49
  • 2
    Just to note, you can simplify this to `decimal a, b; var q = decimal.TryParse((dynamic)"10", out a) && decimal.TryParse("15", out b) && a <= b;`. I've [opened a Roslyn bug](https://github.com/dotnet/roslyn/issues/4507) raising this. – Rawling Aug 12 '15 at 17:04
  • Also a note: this doesn't build for me in VS2013 on a system with VS2015 installed. Does 2015 overwrite 2013's compiler or something? – Rawling Aug 12 '15 at 17:06
  • @Rawling Firstly I had uninstalled VS 2013. Only then I installed VS 2015. – ramil89 Aug 12 '15 at 17:10
  • @ramil89 Yeah, I believe it used to work for you :p This is the second time I've seen something that installing VS2015 seems to have broken retroactively in VS2013 too. – Rawling Aug 12 '15 at 17:12

4 Answers4

112

What does cause this issue?

Looks like a compiler bug to me. At least, it did. Although the decimal.TryParse(v, out a) and decimal.TryParse(v, out b) expressions are evaluated dynamically, I expected the compiler to still understand that by the time it reaches a <= b, both a and b are definitely assigned. Even with the weirdnesses you can come up with in dynamic typing, I'd expect to only ever evaluate a <= b after evaluating both of the TryParse calls.

However, it turns out that through operator and conversion tricky, it's entirely feasible to have an expression A && B && C which evaluates A and C but not B - if you're cunning enough. See the Roslyn bug report for Neal Gafter's ingenious example.

Making that work with dynamic is even harder - the semantics involved when the operands are dynamic are harder to describe, because in order to perform overload resolution, you need to evaluate operands to find out what types are involved, which can be counter-intuitive. However, again Neal has come up with an example which shows that the compiler error is required... this isn't a bug, it's a bug fix. Huge amounts of kudos to Neal for proving it.

Is it possible to fix it through compiler settings?

No, but there are alternatives which avoid the error.

Firstly, you could stop it from being dynamic - if you know that you'll only ever use strings, then you could use IEnumerable<string> or give the range variable v a type of string (i.e. from string v in array). That would be my preferred option.

If you really need to keep it dynamic, just give b a value to start with:

decimal a, b = 0m;

This won't do any harm - we know that actually your dynamic evaluation won't do anything crazy, so you'll still end up assigning a value to b before you use it, making the initial value irrelevant.

Additionally, it seems that adding parentheses works too:

where decimal.TryParse(v, out a) && (decimal.TryParse("15", out b) && a <= b)

That changes the point at which various pieces of overload resolution are triggered, and happens to make the compiler happy.

There is one issue still remaining - the spec's rules on definite assignment with the && operator need to be clarified to state that they only apply when the && operator is being used in its "regular" implementation with two bool operands. I'll try to make sure this is fixed for the next ECMA standard.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Yeah! Applying `IEnumerable` or adding brackets worked for me. Now compiler builds without error. – ramil89 Aug 12 '15 at 10:20
  • 1
    using `decimal a, b = 0m;` might remove the error, but then `a <= b` would always use `0m`, since the out value hasn't been calculated yet. – Paw Baltzersen Aug 12 '15 at 10:26
  • 12
    @PawBaltzersen: What makes you think that? It always *will* be assigned before the comparison - it's just that the compiler can't prove it, for some reason (a bug, basically). – Jon Skeet Aug 12 '15 at 10:27
  • 1
    Having a parsing method without a side effect ie. `decimal? TryParseDecimal(string txt)` may be a solution too – zahir Aug 12 '15 at 10:42
  • 1
    I wonder if it's lazy initialization; it thinks "if the first one is true then I don't need to evaluate the second one which means `b` might not be assigned"; I know that's invalid reasoning but it explains why the parentheses fixes it... – durron597 Aug 12 '15 at 13:47
  • @durron597: I suspect it's more like "this is a dynamically evaluated expression - pretty much anything could happen". But hey, we'll see... – Jon Skeet Aug 12 '15 at 13:50
  • @JonSkeet Unless the bug is that it tries to compare before the output param is initialized. – Paw Baltzersen Aug 12 '15 at 14:35
  • @PawBaltzersen: Well it doesn't happen when the expressions involved *aren't* dynamic... and the comparison itself definitely *shouldn't* happen until after the method calls. – Jon Skeet Aug 12 '15 at 14:40
  • Why not just initialize `b` outside the LINQ expression - it's invariant throughout the enumeration of the values of `array`? – tvanfosson Aug 12 '15 at 17:25
  • @tvanfosson: I suspect the real code is more complicated - I very much doubt that the OP is *really* trying to parse a constant... – Jon Skeet Aug 12 '15 at 17:33
  • I don't disagree but it's probable that it does come from outside the collection being iterated over in which case it could be parsed prior to the conditional. Hard to tell from an abstracted example but I wouldn't want to be performing that parse for every element in the collection if I could avoid it. – tvanfosson Aug 12 '15 at 17:52
  • @tvanfosson: I don't think I'd say it's "probable" just from the example which is only meant to demonstrate the compiler error... – Jon Skeet Aug 12 '15 at 18:00
  • It reminds me of a [popular question](http://stackoverflow.com/questions/16305287/why-does-this-null-tryparse-conditional-result-in-use-of-unassigned-local) that also had something to do with an uninitialized variable, bool and dynamic. Maybe this has something to do with it. – IS4 Aug 12 '15 at 22:30
  • Sorry about the typo. I tried Google (https://www.google.pt/search?q=parenthesys&ie=utf-8&oe=utf-8&gws_rd=cr&ei=_VrMVeLCH4qeaNjmg9gC) to fix my spelling. I guess it failed. – Ismael Miguel Aug 13 '15 at 08:54
21

This does appear to be a bug, or at the least a regression, in the Roslyn compiler. The following bug has been filed to track it:

https://github.com/dotnet/roslyn/issues/4509

In the meantime, Jon's excellent answer has a couple of work arounds.

Community
  • 1
  • 1
JaredPar
  • 733,204
  • 149
  • 1,241
  • 1,454
  • Also [this bug](https://github.com/dotnet/roslyn/issues/4507) that notes that it's nowt to do with LINQ... – Rawling Aug 12 '15 at 18:03
16

Since I got schooled so hard in the bug report, I'm going to try to explain this myself.


Imagine T is some user-defined type with an implicit cast to bool that alternates between false and true, starting with false. As far as the compiler knows, the dynamic first argument to the first && might evaluate to that type, so it has to be pessimistic.

If, then, it let the code compile, this could happen:

  • When the dynamic binder evaluates the first &&, it does the following:
    • Evaluate the first argument
    • It's a T - implicitly cast it to bool.
    • Oh, it's false, so we don't need to evaluate the second argument.
    • Make the result of the && evaluate as the first argument. (No, not false, for some reason.)
  • When the dynamic binder evaluates the second &&, it does the following:
    • Evaluate the first argument.
    • It's a T - implicitly cast it to bool.
    • Oh, it's true, so evaluate the second argument.
    • ... Oh crap, b isn't assigned.

In spec terms, in short, there are special "definite assignment" rules that let us say not only whether a variable is "definitely assigned" or "not definitely assigned", but also if it is "definitely assigned after false statement" or "definitely assigned after true statement".

These exist so that when dealing with && and || (and ! and ?? and ?:) the compiler can examine whether variables may be assigned in particular branches of a complex boolean expression.

However, these only work while the expressions' types remain boolean. When part of the expression is dynamic (or a non-boolean static type) we can no longer reliably say that the expression is true or false - the next time we cast it to bool to decide which branch to take, it may have changed its mind.


Update: this has now been resolved and documented:

The definite assignment rules implemented by previous compilers for dynamic expressions allowed some cases of code that could result in variables being read that are not definitely assigned. See https://github.com/dotnet/roslyn/issues/4509 for one report of this.

...

Because of this possibility the compiler must not allow this program to be compiled if val has no initial value. Previous versions of the compiler (prior to VS2015) allowed this program to compile even if val has no initial value. Roslyn now diagnoses this attempt to read a possibly uninitialized variable.

Community
  • 1
  • 1
Rawling
  • 49,248
  • 7
  • 89
  • 127
  • 1
    Using VS2013 on my other machine, I have actually managed to read unassigned memory using this. It's not very exciting :( – Rawling Aug 12 '15 at 22:14
  • You can read uninitialized variables with simple delegate. Create a delegate that gets `out` to a method that has `ref`. It will happily do it, and it will make variables assigned, without changing the value. – IS4 Aug 12 '15 at 22:33
  • Out of curiosity, I tested that snippet with C# v4. Out of curiosity, though - how does the compiler decide to use the operator `false`/`true` as opposed to the implicit cast operator? Locally, it will call `implicit operator bool` on the first argument, then invoke the second operand, call `operator false` on the first operand, followed by `implicit operator bool` on the first operand *again*. This doesn't make sense to me, the first operand should essentially boil down to a boolean once, no? – Rob Aug 13 '15 at 04:52
  • @Rob Is this the `dynamic`, chained-`&&` case? I've seen it basically go (1) evaluate the first argument (2) use implicit cast to see if I can short circuit (3) I can't, so evauate the second argument (4) now I know both types, I can see the best `&&` is a user-defined `&` (5) call operator `false` on the first argument to see if I can short circuit (6) I can (because `false` and `implicit bool` disagree), so the result is the first argument... and then the next `&&`, (7) use implicit cast to see if I can short circuit (again). – Rawling Aug 13 '15 at 06:45
  • @IllidanS4 Sounds interesting but I haven't found out how to do it. Can you give me a snippet? – Rawling Aug 13 '15 at 07:00
  • @Rawling Yeah, check [here](https://github.com/IllidanS4/SharpUtils/blob/master/Reference.cs#L38). – IS4 Aug 13 '15 at 20:15
  • @IllidanS4 Ah... some kind of reflection? That does bypass the type safety of the compiler, and the CLR doesn't know the difference between `out` and `ref` parameters, so I can believe that. – Rawling Aug 13 '15 at 21:41
15

This is not a bug. See https://github.com/dotnet/roslyn/issues/4509#issuecomment-130872713 for an example of how a dynamic expression of this form can leave such an out variable unassigned.

Neal Gafter
  • 3,293
  • 20
  • 16
  • 1
    As my answer is accepted and highly upvoted, I've edited it to indicate the resolution. Thanks for all your work on this - including explaining my mistake to me :) – Jon Skeet Aug 14 '15 at 05:51