Rick de Water
Rick de Water

Reputation: 2632

Difficulty defining nullability constraints

I have an extension function called TryGetValueAs which basically combines TryGetValue with a cast. The problem is that I keep getting nullability warnings and I can't seem to get it right.

I have the following code:

using System.Diagnostics.CodeAnalysis;

public static class Extensions
{
    public static bool TryGetValueAs<TOut, TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, [MaybeNullWhen(false)] out TOut value)
        where TOut : TValue
    {
        if (dictionary.TryGetValue(key, out var v))
        {
            value = (TOut)v!;
            return true;
        }
        else
        {
            value = default;
            return false;
        }
    }
}

public interface IFoo {}
public class Foo : IFoo {}

class Program
{
    public static void Main(string[] args)
    {
        var dict = new Dictionary<string, IFoo>();
        if (dict.TryGetValueAs("foo", out Foo foo))
        {
            Console.WriteLine(foo.ToString());
        }
    }
}

I tried changing out Foo foo to out Foo? foo, but that only results in more warnings. How can I write this function so that it correctly handles the nullability, while also being compatible with both values and references?

Upvotes: 0

Views: 70

Answers (2)

Guru Stron
Guru Stron

Reputation: 141565

If I understand the logic correctly then using NotNullWhenAttribute for true return value with TOut? value argument should do the trick:

public static class Extensions
{
    public static bool TryGetValueAs<TOut, TKey, TValue>(this IDictionary<TKey, TValue> dictionary, 
        TKey key, 
        [NotNullWhen(true)] out TOut? value) where TOut : TValue
    {
        if (dictionary.TryGetValue(key, out var v))
        {
            value = (TOut)v!;
            return true;
        }

        value = default;
        return false;
    }
}

if (dict.TryGetValueAs("foo", out Foo? foo))
{
    Console.WriteLine(foo.ToString()); // no warnings
}
else
{
    Console.WriteLine(foo.ToString()); // warning
}

Note that probably you will won't to perform type test and not cast directly, i.e. instead of value = (TOut)v!; use something like :

if (dictionary.TryGetValue(key, out var v) && v is TOut result)
{
    value = result;
    return true;
}
...

Upvotes: 0

tmaj
tmaj

Reputation: 34947

Here is my attempt:

public static bool TryGetValueAs<TOut, TKey, TValue>(
    this IDictionary<TKey, TValue> dictionary, 
    TKey key, 
    [NotNullWhen(true)] out TOut? value)
    where TOut : TValue
{
    if (dictionary.TryGetValue(key, out var v)
        && v is TOut)
    {
        value = (TOut)v;
        return true;
    }

    value = default;
    return false;
}

Notes:

  1. out TOut value -> out TOut? value - this is required because this method can return null.
  2. Changed [MaybeNullWhen(false)] to [NotNullWhen(true)] - this tells the compiler that TryGetValueAs returns true then foo is not null. I think this better represents this function than MaybeNullWhen. We can see the benefit of this change when I use foo.GetType() and don't get a complier warning;
  3. Added && v is TOut to handle the case when the desired and actual type don't match. While this is not strictly related to the topic of this question I think this a big one: Try method really should not throw.

Let's test it:

var dict = new Dictionary<string, IFoo>()
{
    {"food", new Food()},
    {"foot", new Foot()},
    {"bigfoot", new BigFoot()},
};

Try<Food>("food");
Try<Foot>("food");
Try<Food>("foot");
Try<Foot>("foot");
Try<Foot>("bigfoot");

void Try<T>(string key) where T: IFoo
{
    if(dict.TryGetValueAs(key, out T? foo))
        Console.WriteLine($"Success! '{key}' retrieved as {typeof(T)} (is:{foo.GetType()}), value: '{foo}'");
    else
        Console.WriteLine($"Unable to retrieve '{key}' as {typeof(T)}");
}

public interface IFoo {}
public class Food : IFoo {}
public class Foot : IFoo {}
public class BigFoot : Foot {}

The output looks good:

Success! 'food' retrieved as Food (is:Food), value: 'Food'
Unable to retrieve 'food' as Foot
Unable to retrieve 'foot' as Food
Success! 'foot' retrieved as Foot (is:Foot), value: 'Foot'
Success! 'bigfoot' retrieved as Foot (is:BigFoot), value: 'BigFoot'

Upvotes: 2

Related Questions