There is a pattern in C# classes exemplified by Dictionary.TryGetValue
and int.TryParse
: a method that returns a boolean indicating success of an operation and an out parameter containing the actual result; if the operation fails, the out parameter is set to null.
Let's assume I'm using C# 8 non-nullable references and want to write a TryParse method for my own class. The correct signature is this:
public static bool TryParse(string s, out MyClass? result);
Because the result is null in the false case, the out variable must be marked as nullable.
However, the Try pattern is generally used like this:
if (MyClass.TryParse(s, out var result))
{
// use result here
}
Because I only enter the branch when the operation succeeds, result should never be null in that branch. But because I marked it as nullable, I now have to either check for that or use !
to override:
if (MyClass.TryParse(s, out var result))
{
Console.WriteLine("Look: {0}", result.SomeProperty); // compiler warning, could be null
Console.WriteLine("Look: {0}", result!.SomeProperty); // need override
}
This is ugly and a bit unergonomic.
Because of the typical usage pattern, I have another option: lie about the result type:
public static bool TryParse(string s, out MyClass result) // not nullable
{
// Happy path sets result to non-null and returns true.
// Error path does this:
result = null!; // override compiler complaint
return false;
}
Now the typical usage becomes nicer:
if (MyClass.TryParse(s, out var result))
{
Console.WriteLine("Look: {0}", result.SomeProperty); // no warning
}
but atypical usage doesn't get the warning it should:
else
{
Console.WriteLine("Fail: {0}", result.SomeProperty);
// Yes, result is in scope here. No, it will never be non-null.
// Yes, it will throw. No, the compiler won't warn about it.
}
Now I'm not sure which way to go here. Is there an official recommendation from the C# language team? Is there any CoreFX code already converted to non-nullable references that could show me how to do this? (I went looking for TryParse
methods. IPAddress
is a class that has one, but it hasn't been converted on the master branch of corefx.)
And how does generic code like Dictionary.TryGetValue
deal with this? (Possibly with a special MaybeNull
attribute from what I found.) What happens when I instantiate a Dictionary
with a non-nullable value type?
3 Answers 3
The bool/out-var pattern doesn't work nicely with nullable reference types, as you describe. So rather than fight the compiler, use the feature to simplify things. Throw in the improved pattern matching features of C# 8 and you can treat a nullable reference as a "poor man's maybe type":
public static MyClass? TryParse(string s) => ...
...
if (TryParse(someString) is {} myClass)
{
// myClass wasn't null, we are good to use it
}
That way, you avoid messing around with out
parameters and you don't have to fight with the compiler over mixing null
with non-nullable references.
And how does generic code like
Dictionary.TryGetValue
deal with this?
At this point, that "poor man's maybe type" falls down. The challenge you'll face is that when using nullable reference types (NRT), the compiler will treat Foo<T>
as non-nullable. But try and change it to Foo<T?>
and it'll want that T
constrained to a class or struct as nullable value types are a very different thing from the CLR's point of view. There are a variety of work-arounds to this:
- Don't enable the NRT feature,
- Start using
default
(along with!
) forout
parameters even though your code is signing up to no nulls, - Use a real
Maybe<T>
type as the return value, which is then nevernull
and wraps thatbool
andout T
intoHasValue
andValue
properties or some such, - Use a tuple:
public static (bool success, T result) TryParse<T>(string s) => ...
...
if (TryParse<MyClass>(someString) is (true, var result))
{
// result is valid here, as success is true
}
I personally am favouring using Maybe<T>
but having it support a deconstruct so that it can be pattern matched as a tuple as in 4, above.
-
2
TryParse(someString) is {} myClass
- This syntax will take some getting used to, but I like the idea.Sebastian Redl– Sebastian Redl2019年02月26日 09:42:42 +00:00Commented Feb 26, 2019 at 9:42 -
TryParse(someString) is var myClass
looks easier to me.Olivier Jacot-Descombes– Olivier Jacot-Descombes2019年05月16日 14:55:27 +00:00Commented May 16, 2019 at 14:55 -
3@OlivierJacot-Descombes it May look easier ... but it won’t work. The car pattern always matches, so
x is var y
will always be true, whetherx
is null or not.David Arno– David Arno2019年05月16日 15:03:06 +00:00Commented May 16, 2019 at 15:03 -
Tuple example still begs
T?
in order to work withsuccess == false
case correctly.Sedat Kapanoglu– Sedat Kapanoglu2020年12月14日 01:11:08 +00:00Commented Dec 14, 2020 at 1:11 -
If you want to stay consistent with the terminology used by the .NET base class library, an
...OrDefault
suffix is generally used rather than aTry...
prefix for this pattern.Heinzi– Heinzi2024年08月07日 16:22:25 +00:00Commented Aug 7, 2024 at 16:22
If you're arriving at this a little late, like me, it turns out the .NET team addressed it through a bunch of parameter attributes like MaybeNullWhen(returnValue: true)
in the System.Diagnostics.CodeAnalysis
space which you can use for the try pattern.
For example:
how does generic code like Dictionary.TryGetValue deal with this?
bool TryGetValue(TKey key, [MaybeNullWhen(returnValue: false)] out TValue value);
which means you get yelled at if you don't check for a true
// This is okay:
if(myDictionary.TryGetValue("cheese", out var result))
{
var more = result * 42;
}
// But this is not:
_ = myDictionary.TryGetValue("cheese", out var result);
var more = result * 42;
// "CS8602: Dereference of a potentially null reference"
Further details:
-
5Excellent answer. You could extend it by mentioning
NotNullWhen
, which is for the Try pattern when a true result definitely means non-null.Sebastian Redl– Sebastian Redl2020年01月22日 19:03:05 +00:00Commented Jan 22, 2020 at 19:03
I don't think there is a conflict here.
your objection to
public static bool TryParse(string s, out MyClass? result);
is
Because I only enter the branch when the operation succeeds, result should never be null in that branch.
However, in fact there is nothing preventing the assignment of null to the out parameter in the old style TryParse functions.
eg.
MyJsonObject.TryParse("null", out obj) //sets obj to a null MyJsonObject and returns true
The warning given to the programmer when they use the out parameter without checking is correct. You should be checking!
There are going to be loads of cases where you will be forced to return nullable types where the main branch of the code returns a non-nullable type. The warning is just there to help you make these explicit. ie.
MyClass? x = (new List<MyClass>()).FirstOrDefault(i=>i==1);
The non-nullable way to code it will throw an exception where there would have been a null. Whether your parsing, getting or firsting
MyClass x = (new List<MyClass>()).First(i=>i==1);
-
I don't think
FirstOrDefault
can be compared, because the nullness of its return value is the main signal. In theTryParse
methods, the out parameter not being null if the return value is true is a part of the method contract.Sebastian Redl– Sebastian Redl2019年02月26日 09:41:37 +00:00Commented Feb 26, 2019 at 9:41 -
its not part of the contract. the only thing thats assured is that something is assigned to the out parameterEwan– Ewan2019年02月26日 09:48:28 +00:00Commented Feb 26, 2019 at 9:48
-
It's the behavior I expect from a
TryParse
method. IfIPAddress.TryParse
ever returned true but didn't assign non-null to its out parameter, I would report it as a bug.Sebastian Redl– Sebastian Redl2019年02月26日 09:51:48 +00:00Commented Feb 26, 2019 at 9:51 -
Your expectation is understandable but its not enforced by the compiler. So sure, the spec for IpAddress might say never returns true and null, but my JsonObject example shows a case where returning null might be correctEwan– Ewan2019年02月26日 10:13:27 +00:00Commented Feb 26, 2019 at 10:13
-
"Your expectation is understandable but its not enforced by the compiler." - I know, but my question is how to best write my code to express the contract I have in mind, not what the contract of some other code is.Sebastian Redl– Sebastian Redl2019年02月26日 11:09:29 +00:00Commented Feb 26, 2019 at 11:09
MyClass?
), and do a switch on it with acase MyClass myObj:
and (an optionall)case null:
.