Reputation: 4731
Here is an example C# program with nullable reference types enabled:
using System;
using System.Collections.Generic;
using System.Linq;
MyStruct myStruct = new("A");
List<MyStruct> list = new() { myStruct };
MyStruct found = list.FirstOrDefault(item => item.Str == "B");
Console.WriteLine(found.Str.Length);
struct MyStruct
{
public readonly string Str;
public MyStruct(string str)
{
Str = str;
}
}
Note that MyStruct
contains a non-nullable Str
field. In theory this means that the Str
field should never be nullable and the compiler will in almost all cases warn you if you leave it null.
However, one case in which a null value can slip in is if a non-initialized struct is returned via a generic method, such as with the FirstOrDefault
call above. In this case, the Str
field will be null but the C# compiler will give no warnings, either when accessing Str
or when assigning the found
variable, and thus the program crashes with a NullReferenceException
when it tries to access found.Str.Length
. (Another case if when reading a struct from an array.)
To make matters worse, some code analysis tools will falsely caution against checking to make sure found.Str
isn't null. (For example, if I add if(found.Str != null)
then Resharper will report that as "Expression is always true" and prompt to remove it even though it is definitely not true in this case.)
This seems like a major enough "leak" in C#'s nullability analysis that I have to wonder if I'm missing something about how to make the compiler understand this situation. Is there any way to "tell" the compiler that the found
struct's fields might be null even though they are decalared non-nullable?
EDIT: To clarify, I'm aware of both this article and the answers to this question, which explain why this happens. But what I'm interested in is what to do about it. Specifically, is there any way to tell the compiler that a certain instance field may be null even though it is marked non-nullable, without changing the actual declaration of that field to be nullable. Similar to how you can postfix as expression with !
to tell the compiler, "trust me, this isn't null even though it is marked nullable", I'm looking to do the inverse and say "trust me, this may be null even though it's marked non-nullable". (Bonus points if there's a way to do this automagically with all fields of a struct instance, but I'm doubtful that's possible.)
Upvotes: 3
Views: 1099
Reputation: 81
The inverse to the !
(null-forgiving) operator is the [MaybeNull] attribute.
To get this attribute to work in .netstandard2.0
, you can put a copy of NullabilityAttributes.cs into your project:
#if !NETCOREAPP3_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER && !NET5_0_OR_GREATER
namespace System.Diagnostics.CodeAnalysis{
// copy of `NullableAttributes.cs` goes here:
// https://source.dot.net/#System.Private.CoreLib/NullableAttributes.cs,68093cc4b5713519
}
#endif
Using the .NET 6 NullabilityInfo
APIs shows that the [MaybeNull]
attribute sets the ReadState
to Nullable
while the WriteState
stays NotNull
.
For example:
public readonly struct With_QuestionMark {
public readonly string? Value;
public readonly int Length;
public With_QuestionMark(object? obj) {
Value = obj?.ToString(); // ❌ no warning, because it's OK to assign `null` to `string? Value`
Length = Value.Length; // ✅ warning, because `Value` might be `null`
}
}
public readonly struct With_MaybeNull {
[MaybeNull]
public readonly string Value;
public readonly int Length;
public With_MaybeNull(object? obj) {
Value = obj?.ToString(); // ✅ warning, because we should never _intentionally_ set `string Value` to `null`
Length = Value.Length; // ✅ warning, because `Value` might be `null`
}
}
I believe this demonstrates your KeyValuePair
scenario:
public static class MaybeNullExample {
public static void NullableKey() {
var dic_nullable = new Dictionary<string?, int>(); // ❌ this gives a warning, because `TKey` has a `notnull` constraint
var key_nullable = dic_nullable.FirstOrDefault().Key; // ✅ implicitly typed to `string?`
Console.WriteLine(key_nullable.Length); // ✅ gives the warning we expect
}
public static void NonNullKey() {
var dic_nonnull = new Dictionary<string, int>(); // ✅ no warning, because we satisfy the `notnull` constraint
var key_nonnull = dic_nonnull.FirstOrDefault().Key; // ❌ implicitly typed to `string`, even though the value is `null`
Console.WriteLine(key_nonnull.Length); // ❌ no warning, even though the value is `null`
}
public static void MaybeNullKey() {
// Unfortunately we can't apply attributes to local variables, but we _can_ apply them to local functions
[return: MaybeNull]
static string GetKey(KeyValuePair<string, int> kvp) {
return kvp.Key;
}
var dic_maybe = new Dictionary<string, int>(); // ✅ no warning; `notnull` constraint is satisfied
var key_maybe = GetKey(dic_maybe.FirstOrDefault()); // ✅ implicitly typed to `string?` because of the `[MaybeNull]` attribute
Console.WriteLine(key_maybe.Length); // ✅ gives the warning we expect
}
///<remarks>
/// You can make a generic extension method if you find this occurs often.
/// This will preserve the type of `TIn` while still letting the compiler
/// treat it as nullable.
///</remarks>
[return: MaybeNull]
static TOut GetMaybeNull<TIn, TOut>(this TIn self, Func<TIn, TOut> extractor) {
return extractor(self);
}
public static void MaybeNullExtension() {
var dic = new Dictionary<string, int>(); // ✅ `notnull` constraint is satisfied
var key = dic.FirstOrDefault().GetMaybeNull(it => it.Key); // ✅ implicitly typed to `string?`
Console.WriteLine(key.Length); // ✅ gives the expected warning
}
}
Upvotes: 3