C# 8可空类型 + Dictionary<>

3

我的代码大致如下:

#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

编译器对此不是很满意,它给了我一个警告。
type 'KEY' cannot be used as type parameter 'TKey' in the generic type or method 'Dictionary<TKey, TValue>

我完全理解这一点。问题在于,对于 Process() 方法的 'key' 参数,发送 null 是完全有效的,所以我不能向该类添加“where KEY:notnull”约束。(并且 MyClass 需要接受类和结构体作为 KEY 类型参数)

我能想到的唯一方法是:

#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

这样做可以让编译器更加满意,但是我就无法使用 C# 8 中提供的所有漂亮的空值检查了。例如,这使我能够编写以下代码:

Map[default] = item;

编译器毫不犹豫。

我如何告诉编译器,Dictionary<>中的“KEY”类型参数应该禁止为null,但仍允许外部类中的KEY值为null?

编辑

我想使用新的C# 8空值功能,以便能尽可能多地在编译时捕获空指针(而不是等待运行时异常)。

进一步编辑

目前,我正在朝着在Dictionary周围放置一个薄层来强制执行空限制并使用其代替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();
}

1
你能让错误信息和源代码保持一致,并且能说明错误发生在哪一行吗?你应该将错误描述为“类型 'Key' 无法用作类型参数 'TKey' 在...”,但是字符串中的 Key(大写 "K",小写 "ey")在你的源代码中并没有出现。 - Flydog57
1
有没有什么理由不将“KEY”设置为“notnull”约束,但在允许空值时使用“KEY?”?(顺便说一下,我强烈建议遵循正常的.NET约定,将类型参数名称命名为“TKey”和“TItem”。) - Jon Skeet
实际上,您可能希望使用“class”约束来限制“KEY”,以便使用“KEY?”。如果您还想允许值类型,则这显然不起作用... - Jon Skeet
我已经编辑了我的代码,使得 KEY/Key 的区分不那么令人困惑。是的,我确实需要让 MyClass 能够接受结构体和类。 - Betty Crokker
我认为你基本上遇到了 C# 8 可空引用类型的限制 - 与泛型的交互确实很棘手 :( (这不是设计团队的错 - 这只是在一个将近20年历史的语言上添加一个巨大新功能的难度的一部分。) - Jon Skeet
2个回答

2
我认为在您的情况下,可以使用以下方法:
  • 将类型参数 TKey 约束为 notnull。这样一来,编译器会强制执行针对 TKey 的 null 检查。
  • 在方法 Process 的参数 TKey key 上添加 AllowNullAttribute。这样一来,向方法 Process 传递 null key 的代码将不会产生警告。

以下是带有注释的代码:

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;
}

因此,使用这种方法我们:

  • MyClass中强制执行了对TKey的空值检查;
  • 允许传递null keyProcess方法而不会收到警告。

1

我发现了同样的问题,我的解决方案是将键包装在一个1元组中:

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;
    }
}

通过这种方式,任何值都可以添加到字典中(即null),而不会禁用规则,从而使编译器满意。


我证明。它有效! - T.S.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接