I found myself staring at code that looks similar to this:
private double? _foo;
public double? Foo
{
get { return _foo; }
set { Baz(-value); }
}
public void Baz(double? value)
{
// Perform stuff with value, including null checks
}
And in a test, I had this:
[Test]
public void Foo_test()
{
...
Foo = null;
...
}
I anticipated that the test would fail (throw), since I thought that you really can't apply unary minus to null
, which I believed would happen in Foo
s setter. However, the unexpected happened and the test did not fail.
I could probably make an informed guess as to why this happens, but I figured that it'd be better if someone who actually knows what's going on could enlighten me.
(And before someone points out the obvious: Yes, the negation should definitely be pushed down into the innards of Baz
.)
Edit and follow up
So, I'm grateful for the inputs from Jon Skeet and JaredPar, who both answered the How part of my question. After some googleing I found this blog post by Eric Lippert explaining the mathematical definition of 'lifted', which makes perfect sense. I also read the relevant parts of the language specification, which are fairly straightforward explaining the mechanics of lifting in c#.
Specifically I now know that early in the process of lifting, a null check is made on the operand, and if the operand is null, null will be the result of applying the operator (unary minus, in my case). So -null
is null
.
Now, to the "Why" part of the question. If I define my own (unary) operator -
on a class
(in which I negate a wrapped value), and invoke this operator on a null reference, I will get a NullReferenceException
. Why is it, that 'lifting' behaves differently? I'm actually interested in the reasoning behind this design decision. I'm definitely not opposing the decision.
Why is it, that 'lifting' behaves differently? I'm actually interested in the reasoning behind this design decision.
Well, first off, you shouldn't always get a null reference exception. An operator call is statically dispatched; it doesn't need to do a null check. This is perfectly legal:
class P
{
public static P operator -(P p)
{ return p; }
static void Main()
{
P p1 = null;
P p2 = -p1;
Console.WriteLine(p2 == null);
}
}
Really I think what your question means is "why do nullable value types get automatic lifting from non-nullable operations to nullable operations, but reference types require you to write specific logic in the operator if you want nullable semantics?"
It's a bit of a mess.
The thing is, reference types were always nullable to begin with, so you could always write that code yourself. Since nullable value types were added later, we added a mechanism so that all the existing non-nullable operators would suddenly work correctly with nullable types, so that you would not have to write all that boring code yourself.
There is a more general problem though. The more general problem is that we've conflated "null" to mean two different things. In the reference type world, null means "I do not refer to any object". In the value type world, null means what it means in databases: this quantity might have a value but we don't know what it is. What are the sales figures for November, if all the sales staff have not reported their results yet? There definitely is a dollar value to that quantity, but we don't know what it is, so we mark it as null.
Lifted arithmetic is designed to work on the "database null" semantics; null plus twelve is null. Something you don't know plus twelve is something else you don't know. Unfortunately, since we did not add nullable arithmetic to C# until version 2, there was already all the gear in place to treat null as the "there's an object reference that refers to no object" semantics.
Had we designed the whole thing from scratch, my suspicion is that (1) there would be nullable reference types, non nullable reference types, nullable value types and non nullable value types, and (2) there would be either a more clearly defined difference between null reference and null value, or their semantics would be more aligned when it comes to arithmetic, and (3) there would be automatic lifting of all non-nullable operations to nullable operations.
Note that VB/VBScript implement the more clear distinction by having two separate values: Null, which is database null, and Nothing, which is reference null.
It can negate it because that's how the "lifted" unary operators are defined... if you perform any operation such as negation, addition, subtraction etc and either operand is null, the result is null too.
For more details, see section 7.3.7 of the language specification.
In this particular case:
For the unary operators (+, ++, -, --, !, ~) a lifted form of an operator exists if the operand and result types are both non-nullable value types. The lifted form is constructed by adding a single ? modifier to the operand and result types. The lifted operator produces a null value if the operand is null. Otherwise, the lifted operator unwraps the operand, applies the underlying operator, and wraps the result.
Note that this is specific to the language you're using - in particular, the & and | operators work differently with bool?
values in C# than they do in VB.
This is just a feature of nullable values in C#. Applying the set of predefined unary and binary operators to a nullable value will produce null
if any of the nullable inputs are null
.
More information here in the Operators section
EDIT
As Jon pointed out this rule does not hold for operators like ||
, true
which have a non-nullable return type.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With