Reputation: 2632
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
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
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:
out TOut value
-> out TOut? value
- this is required because this method can return null.[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;&& 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