简体   繁体   中英

c# 8 nullable + Dictionary<>

My code looks something like this:

#nullable enable
class MyClass<KEY, ITEM>
{
    readonly Dictionary<KEY, ITEM> Map = new Dictionary<KEY, ITEM>();
    public void Process(KEY key, ITEM item)
    {
        if (key != null)
        {
            Map[key] = item;
        }
    }
}
#nullable disable

The compiler is not thrilled with this, it gives me the warning

type 'KEY' cannot be used as type parameter 'TKey' in the generic type or method 'Dictionary<TKey, TValue>

which I can certainly understand. The problem is, sending null for the 'key' parameter to Process() is perfectly valid so I can't add the "where KEY: notnull" constraint to the class. (and MyClass needs to accept both classes and structs for the KEY type parameter)

The only thing I can think of is this:

#nullable enable
class MyClass<KEY, ITEM>
{
#nullable disable
    readonly Dictionary<KEY, ITEM> Map = new Dictionary<KEY, ITEM>();
#nullable enable
    public void Process(KEY key, ITEM item)
    {
        if (key != null)
        {
            Map[key] = item;
        }
    }
}
#nullable disable

Which keeps the compiler happy, but then I don't have all those nice C# 8 null checks. For example, it allows me to write this code:

Map[default] = item;

and the compiler doesn't bat an eye.

How can I tell the compiler that the 'KEY' type parameter to Dictionary<> should disallow nulls, but still allow KEY values to be null in the outer class?

EDIT

I want to use the new C# 8 nullability features so that I catch as many null pointers at compile time as possible (instead of waiting for runtime exceptions).

FURTHER EDIT

The direction I'm headed right now, is to put a thin layer around Dictionary to enforce the null restrictions and use it instead of Dictionary<>

#nullable enable
public class CheckDictionary<KEYTYPE, VALUETYPE>
{
#nullable disable
    readonly Dictionary<KEYTYPE, VALUETYPE> Dictionary = new Dictionary<KEYTYPE, VALUETYPE>();
#nullable enable

    public VALUETYPE this[[DisallowNull] KEYTYPE key]
    {
        get { return Dictionary[key]; }
        set { Dictionary[key] = value; }
    }

    public bool Remove([DisallowNull] KEYTYPE key)
    { return Dictionary.Remove(key); }

    public bool TryGetValue([DisallowNull] KEYTYPE key, out VALUETYPE value)
    { return Dictionary.TryGetValue(key, out value); }

    public List<VALUETYPE> Values => Dictionary.Values.ToList();
}

I think that in your case the next approach can be used:

  • Constrain type parameter TKey to be notnull . As a result the compiler will enfoce null checks against TKey .
  • Add AllowNullAttribute to the parameter TKey key of the method Process . As a result code that passes null key to the method Process will not produce warnings.

Here is the code with comments:

class MyClass<TKey, TItem> where TKey : notnull
{
    // With "notnull" constraint type parameter "TKey" matches type constraint
    // of the class Dictionary<TKey, TValue>, therefore compiler does not
    // generate the next warning:
    //   The type 'TKey' cannot be used as type parameter 'TKey' in the 
    //   generic type or method 'Dictionary<TKey, TValue>'. Nullability
    //   of type argument 'TKey' doesn't match 'notnull' constraint.
    readonly Dictionary<TKey, TItem> Map = new Dictionary<TKey, TItem>();

    public void Process([System.Diagnostics.CodeAnalysis.AllowNull] TKey key, TItem item)
    {
        // "TKey key" is marked with [AllowNull] attribute. Therefore if you delete
        // null check "key != null" compiler will produce the next warning on the line
        // "Map[key] = item":
        //   Possible null reference argument for parameter 'key' in
        //   'TItem Dictionary<TKey, TItem>.this[TKey key]'.
        if (key != null) 
            Map[key] = item;

        // Because "TKey" is constrained to be "notnull", this line of code
        // produces the next warning:
        //   Possible null reference argument for parameter 'key' in
        //   'TItem Dictionary<TKey, TItem>.this[TKey key]'.
        Map[default] = item;
    }
}

static class DemoClass
{
    public static void Demo()
    {
        MyClass<string, int> mc1 = new MyClass<string, int>();
        // This line does not produce a warning, because "TKey key" is marked
        // with [AllowNull] attribute.
        mc1.Process(null, 0);
        // This line does not produce a warning too.
        mc1.Process(GetNullableKey(), 0);

        // Usage of "MyClass" with value type "TKey" is also allowed.
        // Compiler does not produce warnings.
        MyClass<int, int> mc2 = new MyClass<int, int>();
        mc2.Process(0, 1);
    }

    public static string? GetNullableKey() => null;
}

So using such approach we:

  • enfoced null checks against TKey in the MyClass ;
  • allowed to pass null key to the Process method without getting warnings.

I found the same problem, my solution is to wrap the keys in a 1-tuple:

class MyClass<TKey, TItem>
{
    readonly Dictionary<ValueTuple<TKey>, TItem> Map = new Dictionary<ValueTuple<TKey>, TItem>();

    public void Process(TKey key, TItem item)
    {
        Map[ValueTuple.Create(key)] = item;
    }
}

In this way any value can be added to the dictionary (ie null) and the compiler is satisfied without disabling rules.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM