5

i have following case

beleg.PreisDB = (double?)orders.Where(x => x.orderId == beleg.auftrnr).Sum(x => x.itemPrice + x.shippingPrice + x.giftWrapPrice) ?? 0;
beleg.PreisCouponDB = (double?)orders.Where(x => x.orderId == beleg.auftrnr).Sum(x => x.itemPromotionDiscount + x.shipPromotionDiscount) ?? 0;
var gesamtPreis = Math.Round(beleg.PreisDB??0 + beleg.PreisCouponDB??0, 2);

I have added a quickwatch in debug to some fields in my case:

beleg.PreisDB == 8.39
beleg.PreisDB??0 == 8.39
beleg.PreisCouponDB == -0.49
beleg.PreisCouponDB??0 == -0.49

And now the strange behaviour also from quickwatch and of course the result

beleg.PreisDB??0 + beleg.PreisCouponDB??0 == 8.39
Math.Round(beleg.PreisDB??0 + beleg.PreisCouponDB??0, 2) == 8.39
gesamtPreis == 8.39

So the addition of 8.39 + -0.49 doesn't give me 7.9 but 8.39 This code was running for 600k cases on at least two i had this behaviour the others behaved well. I'm to blind to see my error at the moment. The question is why is .net behaving like this? I'm Using Visual Studio 2015 with .net 4.5.2.

Lars
  • 90
  • 4
  • 2
    Are you seeing this *outside* the debugger? I've seen numerous issues with the debugger not working quite as expected, but if you can show this in [mcve] without using the debugger, that would be more unexpected and a lot easier to help you with. – Jon Skeet Feb 28 '19 at 07:25
  • Having said which, I think I may be able to see what's wrong - I need to check something... – Jon Skeet Feb 28 '19 at 07:25
  • I've closed the question as a duplicate; for additional ways in which the precedence of `??` can be surprising, see https://stackoverflow.com/questions/3218140/. – Eric Lippert Feb 28 '19 at 07:40
  • Your logic here is the same as "`1+2` is `3` and `4+1` is `5`, therefore `1 + 2 * 4 + 1` is `15`. Since I get the answer `10`, multiplication must be broken. No, **you have to put parentheses around operations when combining them**. `(1+2)*(4+1)` is 15. – Eric Lippert Feb 28 '19 at 07:43

2 Answers2

12

The problem is precedence - + has higher precedence than ??, so it "binds tighter".

Here's a complete example to demonstrate:

using System;

class Test
{
    static void Main()
    {
        double? x = 8.39;
        double? y = -0.49;

        // Your expression
        Console.WriteLine(x ?? 0 + y ?? 0);

        // The equivalent you're expecting
        Console.WriteLine((x ?? 0) + (y ?? 0));

        // The actual bracketing
        Console.WriteLine(x ?? ((0 + y) ?? 0));
    }
}

Another alternative would be to use Nullable<T>.GetValueOrDefault() instead of ?? 0:

Console.WriteLine(x.GetValueOrDefault() + y.GetValueOrDefault());

But I think I'd probably just use the version with brackets - so in your case:

var gesamtPreis = Math.Round((beleg.PreisDB ?? 0) + (beleg.PreisCouponDB ?? 0), 2);

I would definitely put the spaces in round ?? just like most other operators, as otherwise it's giving you the impression of binding very tightly (like the . operator does).

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 2
    Apparently Coverity has deleted the article I wrote about this issue in 2013. Vexing! I'll see if I can get it out of the wayback machine and re-post it. – Eric Lippert Feb 28 '19 at 07:38
  • Shouldn't the actual bracketingg be `x ?? (0 + y) ?? 0)` so since `+` has higher precedence he will first try to do `0 + y` ? – Rand Random Feb 28 '19 at 07:46
  • 1
    Can you explain why the actual bracketing is `x??(0+(y??0))` and not `x??((0+y)??0)` ? – Eric Lippert Feb 28 '19 at 07:50
  • @RandRandom: I'm not sure what you mean by "so since + has higher precedence he will first try to to 0 + y" but I've adjusted the bracketing. It never evaluates `0 + y`, because `x` is non-null. – Jon Skeet Feb 28 '19 at 07:50
  • 2
    @EricLippert: Well I can explain why I *wrote* that it was `x??(0+(y??0))` - I wasn't careful enough :) Fixed now. I think. But maybe not - I need to check the associativity as well. – Jon Skeet Feb 28 '19 at 07:51
  • OK, glad I'm not the crazy one tonight. :-) FYI it's right-associative. – Eric Lippert Feb 28 '19 at 07:53
  • 1
    @EricLippert: Yup. The answer now shows the same as your earlier comment. I think I need another coffee. – Jon Skeet Feb 28 '19 at 07:54
  • @EricLippert: Fun fact: in 2015 I raised an issue in our ECMA github repo asking when it would possibly matter whether it's left or right associative. I did work out an example, involving various implicit conversions. – Jon Skeet Feb 28 '19 at 07:56
  • That's quite amusing; if I recall correctly I wrote a Roslyn test case using a bunch of user-defined lifted conversions that demonstrates that `(x??y)??z` and `x??(y??z)` may have different results. – Eric Lippert Feb 28 '19 at 07:58
  • And a comment on your answer here notes that there is a way to test the associativity by seeing which types unify: https://stackoverflow.com/questions/6238074/how-the-right-associative-of-null-coalescing-operator-behaves/6269773#6269773 – Eric Lippert Feb 28 '19 at 08:01
  • As I understood your explanation, it wouldn't matter if `x` is null or not and the runtime would try to evaluate `0 + y` first. Same as math operations `multiply`, `division` have a higher precedence over `addition` and `substraction` - So I thought you meant it would be the math equivalent of `1 + 2 * 3 + 4` where the actual bracketing is `1 + (2 * 3) + 4` - Hope it makes sense what I am talking about. – Rand Random Feb 28 '19 at 08:33
  • Had another look at your last edit and you are more or less actually doing what I meant now, so I was almost correct just couldn't explaing it well enough :) – Rand Random Feb 28 '19 at 08:38
  • @RandRandom: No, because the RHS of the `??` is not evaluated if the LHS is non-null. So `1 ?? (2 * 3)` wouldn't end up evaluating 2 * 3 at all. – Jon Skeet Feb 28 '19 at 08:38
  • 2
    And of course the `??` operator requires that its left side be nullable, so `1??whatever` is not legal code to begin with. But the larger point is important; the `??` operator is defined as evaluating its left operand unconditionally and evaluating its right operand conditionally. – Eric Lippert Feb 28 '19 at 08:45
2
var gesamtPreis = Math.Round(beleg.PreisDB??0 + beleg.PreisCouponDB??0, 2);
// Executed in this order
var gesamtPreis = Math.Round(beleg.PreisDB ?? ((0 + beleg.PreisCouponDB) ?? 0))

Your code gets executed in a different order. Since beleg.PreisDB is not null, beleg.PreisCouponDB is never added. Try adding some brackets:

// Executed as you want it to be
var gesamtPreis = Math.Round((beleg.PreisDB ?? 0) + (beleg.PreisCouponDB ?? 0), 2);
Felix B.
  • 166
  • 8