This page shows the source for this entry, with WebCore formatting language tags and attributes highlighted.
Title
When [NotNull] is null
Description
I prefer to be very explicit about nullability of references, wherever possible. Happily, most modern languages support this feature non-nullable references natively (e.g. TypeScript, Swift, Rust, Kotlin).
As of version 8, C# also supports non-nullable references, but we haven't migrated to using that enforcement yet. Instead, we've used the JetBrains nullability annotations for years.<fn>
Recently, I ended up with code that returned a <c>null</c> even though R# was convinced that the value could never be <c>null</c>.
The following code <i>looks like</i> it could never produce a null value, but somehow it does.
<code>
[NotNull] <hl>// The R# checker will verify that the method does not return null</hl>
public DynamicString GetCaption()
{
var result = GetDynamic() ?? GetString() ?? new DynamicString();
}
[CanBeNull]
private DynamicString GetDynamic() { ... }
[CanBeNull]
private string GetString() { ... }
</code>
So, here we have a method <c>GetCaption()</c> whose result can never be <c>null</c>. It calls two methods that <i>may</i> return <c>null</c>, but then ensures that its own result can <i>never</i> be null by creating a new object if neither of those methods produces a string. The nullability checker in ReSharper is understandably happy with this.
At runtime, though, a call to <c>GetCaption()</c> was returning <c>null</c>. How can this be?
<h>The Culprit: An Implicit Operator</h>
There is a bit of code missing that explains everything. A <c>DynamicString</c> declares <i>implicit operators</i> that allow the compiler to convert objects of that type to and from a <c>string</c>.
<code>
public class DynamicString
{
// ...Other stuff
[CanBeNull]
public static implicit operator string([CanBeNull] DynamicString dynamicString) => dynamicString?.Value;
}
</code>
A <c>DynamicString</c> contains zero or more key/value pairs mapping a language code (e.g. "en") to a value. If the object has no translations, then it is equivalent to <c>null</c> when converted to a <c>string</c>. Therefore, a <c>null</c> or empty <c>DynamicString</c> converts to <c>null</c>.
If we look at the original call, the compiler does the following:
<ol>
The call to <c>GetDynamic()</c> sets the type of the expression to <c>DynamicString</c>.
The compiler can only apply the <c>??</c> operator if <i>both sides are of the same type</i>; otherwise, the code is in error.
Since <c>DynamicString</c> can be <i>coerced</i> to <c>string</c>, the compiler decides on <c>string</c> for the type of the first coalesced expression.
The next coalesce operator (<c>??</c>) triggers the same logic, coercing the right half (<c>DynamicString</c>) to the type it has in common with the left half (<c>string</c>, from before).
Since the type of the expression must be <c>string</c> in the end, even if we fall back to the <c>new DynamicString()</c>, it is coerced to a <c>string</c> and thus, <c>null</c>.
</ol>
Essentially, what the compiler builds is:
<code>
var result =
(string)GetDynamic() ??
GetString() ??
(string)new DynamicString();
</code>
The R# nullability checker sees only that the final argument in the expression is a <c>new</c> expression and determines that the <c>[NotNull]</c> constraint has been satisfied. The compiler, on the other hand, executes the final cast to <c>string</c>, converting the empty <c>DynamicString</c> to <c>null</c>.
<h>The Fix: Avoid Implicit <c>DynamicString</c>-to-<c>string</c> Conversion</h>
To fix this issue, I avoided the <c>??</c> coalescing operator. Instead, I rewrote the code to return <c>DynamicString</c> wherever possible and to implicitly convert from <c>string</c> to <c>DynamicString</c>, where necessary (instead of in the other direction).
<code>
public DynamicString GetCaption()
{
var d = GetDynamic();
if (d != null)
{
return d;
}
var s = GetString();
if (s != null)
{
return s; <hl>// Implicit conversion to DynamicString</hl>
}
return GetDefault();
}
</code>
<h>Conclusion</h>
The takeaway? Use features like implicit operators sparingly and only where absolutely necessary. A good rule of thumb is to define such operators only for <c>structs</c> which are values and can never be <c>null</c>.
I think the convenience of being able to use a <c>DynamicString</c> as a <c>string</c> outweighs the drawbacks in this case, but YMMV.
<hr>
<ft>Java also has <c>@NonNull</c> and <c>@Nullable</c> annotations, although it's <a href="https://stackoverflow.com/questions/4963300/which-notnull-java-annotation-should-i-use" source="StackOverflow">unclear which standard you're supposed to use.</a></ft>