29

As you might know, DateTime? does not have a parametrized ToString (for the purposes of formatting the output), and doing something like

DateTime? dt = DateTime.Now;
string x;
if(dt != null)
    x = dt.ToString("dd/MM/yyyy");

will throw

No overload for method 'ToString' takes 1 arguments

But, since C# 6.0 and the Elvis (?.) operator, the above code can be replaced with

x = dt?.ToString("dd/MM/yyyy");

which.... works! Why?

Tamir Vered
  • 10,187
  • 5
  • 45
  • 57
iuliu.net
  • 6,666
  • 6
  • 46
  • 69
  • 1
    After reading the answers, I can't help but feel that the real solution to your problem goes like, "Just use `dt.Value.ToString()` already" – Mr Lister Jan 29 '16 at 17:39
  • Ya, just calling dt.Value.ToString() will blow up if dt is null; you definitely want to use the Elvis Operator, not only because it has an awesome name but because it handles the NULL check for you. dt?.Value.ToString() essentially says "If dt is not null, then access it's Value property and convert it to a String; if it IS null, ignore the call and do nothing" –  Jan 29 '16 at 20:37
  • There was already a null check. So there would be no problem using `.Value` in this scenario. – Travis J Feb 01 '16 at 19:34
  • user4650451, The whole point is that you _cannot_ say `dt?.Value` because while `Nullable<>` has a property called `Value`, the underlying type `DateTime` has no member called `Value`, and since `?.` when used on a `Nullable<>` does not access the members of the `Nullable<>` itself but instead the members of the struct "inside" it, `dt?.Value` makes no sense. However, if the underlying struct itself has a `Value` member, such as with a `KeyValuePair? nullableKvp` (nullable key-value pair from `int` to `int`), can you see what `nullableKvp?.Value` is, compared to `nullableKvp.Value`? – Jeppe Stig Nielsen Jul 12 '17 at 08:35

3 Answers3

20

Because Nullable<T> is implemented in C# in a way that makes instances of that struct appear as nullable types. When you have DateTime? it's actually Nullable<DateTime>, when you assign null to that, you're setting HasValue to false behind the scenes, when you check for null, you're checking for HasValue, etc. The ?. operator is just implemented in a way that it replaces the very same idioms that work for reference types also for nullable structs. Just like the rest of the language makes nullable structs similar to reference types (with regard to null-ness).

Joey
  • 344,408
  • 85
  • 689
  • 683
14

Short answer:

DateTime? is only a sweet syntax for Nullable<DateTime> which doesn't contain DateTime's properties and method while the Elvis operator works on the not-Nullable Nullable<DateTime>.Value.


Explanation:

The following code:

DateTime? dt = DateTime.Now;
string x;
if (dt != null)
    x = dt?.ToString("dd/MM/yyyy");

When decompiles as C# 5.0 yields the following result:

DateTime? nullable = new DateTime?(DateTime.Now);
if (nullable.HasValue)
{
    string str = nullable.HasValue ? nullable.GetValueOrDefault().ToString("dd/MM/yyyy") : null;
}

Side note: the string seems declared inside the if is irrelevant because of hoisting at the MSIL level, and since the value is not used later on the decompiler shows it as if it was declared inside that if scope.

As you see, and since DateTime? is only a sweet syntax for Nullable<DateTime>, C# has a specific reference for Nullable<T>s with the Elvis operator, making its return value the non-nullable T itself.

The result of the whole Elvis operator must be Nullable therefore, if you wanted to receive a non-string value it would have to be either Nullable<T> or a ReferenceType but this doesn't change the fact that if the operator has managed to get the Nullable<DateTime> value itself - the returned DateTime is not Nullable<DateTime> anymore.

Tamir Vered
  • 10,187
  • 5
  • 45
  • 57
  • 4
    Down voter please explain so I can improve my answer. – Tamir Vered Jan 29 '16 at 10:50
  • you should remove the `if` around the string assignment. It negates the ternary operator within. – Jens Meinecke Jan 29 '16 at 10:51
  • No, since the `string` is not used later on, this is a compiler optimization. This is the actual decompiled code in reflector... – Tamir Vered Jan 29 '16 at 10:52
  • 1
    If you have tested for nullable.HasValue, why do you test for it again with the ternary operator. It will **ALWAYS** return the `nullable.GetValueOrDefault().ToString("dd/MM/yyyy")`, so the ternary operator is useless. – Jens Meinecke Jan 29 '16 at 10:54
  • Again, ***THIS IS A DECOMPILATION***, I will edit to add the original source code if it will make it clearer. – Tamir Vered Jan 29 '16 at 10:56
  • Reformatted, do you understand why it looks like that now? – Tamir Vered Jan 29 '16 at 11:03
  • 1
    How did you produce that code? TryRoslyn gave me one with [string optimized away](http://goo.gl/cDVjFn) in weird manner (tho JIT should take care of it) - without optimization it was [more or less the same](http://goo.gl/kyKG6U) – PTwr Jan 29 '16 at 11:41
  • I've used `Reflector` but notice you have asked for `Release` instead of `Debug`: [Here is the same one in Debug](http://tryroslyn.azurewebsites.net/#K4Zwlgdg5gBAygTxAFwKYFsDcAoAxgGwEMQQYAHAJwHsoLD1sBvbGVmFQ5MXGANyrAATGOkKQAFAEoWbZm3kwAIp1QAVMOlQB+GIOQwAvEpXrNAOgByVAO44FbFBUiwAHnfswwAMxji9MAEIjCGB8fEkPGBlI1hdDXWQtM1UqOGQnaHEAIkFBAHoAWQK8hFKELMl3VgBfbGqgA==) – Tamir Vered Jan 29 '16 at 11:45
  • I was just looking at debug code ;) Do you get same duplicate `dateTime.HasValue && dateTime.HasValue` on release with Reflector? – PTwr Jan 29 '16 at 11:46
  • `TryRoslyn` also compiles the give source, `Reflector` takes an already compiled one, The `Debug`/`Release` compilation result is determined by the `Visual Studio` at my set-up. – Tamir Vered Jan 29 '16 at 11:51
  • 4
    Haha, compiled it with `Release` on `Visual Studio` now and it does give the same double check, missing optimization I guess... – Tamir Vered Jan 29 '16 at 11:53
  • @BlueRaja-DannyPflughoeft Read the other comments, this is a ***decompilation***. – Tamir Vered Jan 29 '16 at 17:28
  • @BlueRaja-DannyPflughoeft That is true, I just copied the code from the question. although I would expect the compiler to optimize it away anyway. To be fair the `JIT` probably optimizes it away, I didn't exmine the `assembly` code that is compiled from that. – Tamir Vered Jan 29 '16 at 17:35
  • 1
    JIT is almost smart. On `Release` it left only `DateTime? dt = DateTime.Now` from sample code :) With `WriteLine(x)` it removed only `if (dt != null)` as predicted. Nullable seems to be JITed away as well `x = dt?.ToString("dd/MM/yyyy");` bytecode is different (shorter) when it is guaranteed to never be null than when it can randomly become null. Also branch prediction loves to put half of line after end of `if`` body, I am too sober to make sense of it tonight. – PTwr Jan 29 '16 at 21:47
2

Considering:

DateTime? dt = DateTime.Now;
string x;
if(dt != null)
    x = dt.ToString("dd/MM/yyyy");

Here dt is a DateTime? or Nullable<DateTime> witch is not IFormatable and don't have a ToString(string format) method.

So it throw.

Now considering:

x = dt?.ToString("dd/MM/yyyy");

The ?. is a syntactic sugar for:

dt.HasValue ? dt.Value.ToString("dd/MM/yyyy"): null

Here dt.Value is a DateTime witch is IFormatable and have a ToString(string format) method.

Finally the good way to write the first code in C# 5.0 is:

DateTime? dt = DateTime.Now;
string x;
if(dt.HasValue)
    x = dt.Value.ToString("dd/MM/yyyy");
Orace
  • 7,822
  • 30
  • 45