C# Safe navigation operator - what is actually going on?
Asked Answered
A

4

24

I've been following the safe navigation operator feature added in C#6 with some interest. I've been looking forward to it for a while. But I'm finding some different behavior than I expected. I'm realizing I really don't understand how it actually works.

Given this class

class Foo {
    public int? Measure;
}

Here's some code using the new operator.

Foo f = new Foo { Measure = 3 };
Console.WriteLine(f?.Measure);  // 3

f = new Foo { Measure = null };
Console.WriteLine(f?.Measure);  // null

f = null;
Console.WriteLine(f?.Measure);  // null

Up to here, everything's working as expected. ?. is accessing members when the left hand side is not null, otherwise returning null. But here things go in a direction I wasn't expecting.

var i = f?.Measure; // i is Nullable<int>
Console.WriteLine(i.HasValue); // false
Console.WriteLine(f?.Measure.HasValue); // null

What?

Why can I get HasValue from i, but not from the same expression I assigned to i? How can HasValue ever be null?

Edit: My real question is about program behavior, not a compilation error. I removed the extra stuff about compilation, and focused this question more narrowly on why two different results are returned by what seems like the same logic.

Adley answered 28/8, 2015 at 1:13 Comment(0)
P
25

Let's walk through this logically.

var f = ???;
var i = f?.Measure;
var t = i.HasValue;

We don't know if f is null or not.

  1. If f is null, then the result (i) is null
  2. If f is not null, then the result (i) is an int

Therefore, i is defined as int?, and t is a bool

Now, let's walk through this:

var f = ???;
var i = f?.Measure.HasValue;
  1. If f is null, then the result (i) is null
  2. If f is not null, then the result (i) is Measure.HasValue, which is a bool.

Therefore, i is a bool?.

If f is null, we short-circuit and return null. If it's not, we return the bool result of .HasValue.

Essentially, when using ?. - the return type must be a reference value, or a Nullable<T>, as the expression can short circuit to return null.

Presuppose answered 28/8, 2015 at 2:25 Comment(7)
Hm... That's kind of mind-boggling to me, but I think I can adapt to that behavior. It's certainly different than what I was expecting. If it works as you say it does (and it certainly seems to!) then f?.Measure is not a "logical sub-expression" of f?.Measure.HasValue. Maybe I'll prefer it this way after I think about it for a while, but for now, it seems harder to reason about.Adley
I do agree it's a bit non-intuitive at first. You can read the expression as "If f is null, return null. If f is not null, tell me if Measure has a value". Just takes some getting used to, I suppose. The main thing to realise is that you're reading and understanding this: f?.Measure.HasValue as this: (f?.Measure).HasValue, which it's not - similar to how var i = 1 + 2;var t = i *3; is different from var i = 1 + 2 * 3;Presuppose
That's a helpful comparison, but in that example, at least 2 * 3 has a value! I would have assumed that the right-most member accessor couldn't possibly apply first, since there's nothing to apply it to until the left one is applied. In other words, 2 is a perfectly fine value on it's own, but Measure only makes sense with respect to a particular Foo.Adley
@Adley Maybe the multiplication example was a bit off - as this behavior has nothing to do with the order of execution. It's only to do with the result type (the right-most member - in this case a bool), and the existence of ?. which means that null is possible - so if the return type (bool) is not nullable, it must be wrapped in Nullable<T> - since we need to return null if f is null, but we can't put that in a bool.. If HasValue returned an object - or even an int?, then all would be well and it'd behave as you expectedPresuppose
It's not just static type analysis. Imagine the expression myObject?.SomeProp.ExtensionMethod(). Given these were all reference types, one short day ago, I would have expected the extension method to get invoked if myObject was null, so it's not just the nullable making this violate my expectations. Fortunately, it seems that in practice, the distinction rarely matters, and when it does, that behavior can be forced by applying parentheses to the left-most member accessors. Thanks for your insight.Adley
@recursive: During the initial implementation phase of this feature, I recall seeing some substantial discussion (which I have been unable to locate) about evaluation order for this operator. The designers were well aware of the confusion their decision was going to cause...but the alternative ended up being worse.Dynast
@Brian: I had some initial shock upon encountering this behavior. But after having absorbed it, I'm now quite certain I prefer it how it's actually working.Adley
C
2
var i = f?.Measure; // i is Nullable<int>
Console.WriteLine(i.HasValue); // false
Console.WriteLine(f?.Measure.HasValue); // null

In this case, f is null.

The reason why i.HasValue returned false is because i is of type Nullable<int>. So even when the value of i is null, like in this case, i.HasValue is still accessible.

However, f?.Measure.HasValue immediately returns null after f? is evaluated. Hence the result you see above.

Just to quote Rob's comment:

The main thing to realise is that you're reading and understanding this: f?.Measure.HasValue as this: (f?.Measure).HasValue, which it's not.

Caresa answered 23/2, 2017 at 19:43 Comment(0)
I
0

Nullable<T> is actually a struct and therefore cannot be null, only its Value can, so HasValue will always be accessible.

Indigent answered 28/8, 2015 at 1:41 Comment(1)
Certainly, it's accessible, as I showed in my question. However, its value mysteriously changes from false to null.Adley
T
0

I ran into this today.

What does the following C# snippet print?

public class NullTests
{
    public static void Main(string[] args)
    {
        object obj = DoIt();
        Console.WriteLine(obj?.ToString().NullToNothing());
    }

    private static object DoIt() => null;
}

public static class Extensions
{
    public static string NullToNothing(this string input) => input ?? "nothing";
}

Answer: null.

What does the following Kotlin snippet print?

fun main() {
    val obj = doIt()
    println(obj?.toString().NullToNothing())
}

fun doIt() = null
fun String?.NullToNothing() = this ?: "nothing"

Answer: "nothing".

Like you, I was expecting the Kotlin behaviour, and it tripped me up for the better part of the day. :(

Tenne answered 11/3, 2020 at 20:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.