20

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?

  • Didn't try it (which is why I'm not writing this as an answer), but with the new pattern matching feature of switch statements, I'm guessing one option is to simply return the nullable reference (no Try-pattern, return MyClass?), and do a switch on it with a case MyClass myObj: and (an optionall) case null:. – Filip Milovanović Feb 25 at 14:30
  • +1 I really like this question, and when I've had to work with this I've always just used an extra null check instead of the override - which always felt a little unnecessary and inelegant, but it was never in performance critical code so I just let it go. Would be nice to see if there is a cleaner way of handling it! – BrianH Feb 25 at 14:50
10

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:

  1. Don't enable the NRT feature,
  2. Start using default (along with !) for out parameters even though your code is signing up to no nulls,
  3. Use a real Maybe<T> type as the return value, which is then never null and wraps that bool and out T into HasValue and Value properties or some such,
  4. 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 Feb 26 at 9:42
  • TryParse(someString) is var myClass looks easier to me. – Olivier Jacot-Descombes May 16 at 14:55
  • 1
    @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, whether x is null or not. – David Arno May 16 at 15:03
11

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:

3

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 the TryParse methods, the out parameter not being null if the return value is true is a part of the method contract. – Sebastian Redl Feb 26 at 9:41
  • its not part of the contract. the only thing thats assured is that something is assigned to the out parameter – Ewan Feb 26 at 9:48
  • It's the behavior I expect from a TryParse method. If IPAddress.TryParse ever returned true but didn't assign non-null to its out parameter, I would report it as a bug. – Sebastian Redl Feb 26 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 correct – Ewan Feb 26 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 Feb 26 at 11:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.