【问题标题】:Best way to avoid warnings for generics with nullable types/fields?避免对具有可为空类型/字段的泛型发出警告的最佳方法?
【发布时间】:2020-03-31 10:28:39
【问题描述】:

考虑以下人为的类:

public sealed class Test<T>
{
    public void SetItem(T item)
    {
        _item = item;
    }

    public T GetItem()
    {
        return _item;
    }

    T _item; // warning CS8618: Non-nullable field '_item' is uninitialized.
}            // Consider declaring the field as nullable.

假设它必须使用以下代码:

var test1 = new Test<int>();
test1.SetItem(1);
Console.WriteLine(test1.GetItem());

var test2 = new Test<string>();
test2.SetItem("One");
Console.WriteLine(test2.GetItem());

进一步假设编译时启用nullable

此实现会产生一个编译器警告(如果像我们一样,您启用了“作为错误的警告”,则会变成一个错误):

警告 CS8618:不可为空的字段“_item”未初始化。考虑将该字段声明为可为空。

我的问题很简单:

避免此警告的最佳方法是什么?

我尝试过的事情:

1) 将 _item 声明为可为空:

T? _item; // Error: A nullable type parameter must be known to be a value type or non-nullable reference type

2) 将类声明为:

public sealed class Test<T> where T: struct

这避免了警告,但现在var test2 = new Test&lt;string&gt;(); 行将无法编译

3) 将类声明为:

public sealed class Test<T> where T: class
...
T? _item;

这也避免了警告,但现在var test1 = new Test&lt;int&gt;(); 行将无法编译。

所以我现在拥有的是这样的:

public sealed class Test<T>
{
    public void SetItem(T item)
    {
        _item = item;
    }

    public T GetItem()
    {
        return _item;
    }

    #pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
    T _item;
    #pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
}

这真的是我最好的选择吗?

注意:我见过this related question,但它没有回答我的问题,因为答案说使用where T : struct,如上所述 - 不能解决问题。


[收到正确答案后编辑]

所以事实证明,对于泛型类型,没有办法使用 nullable 来表达“如果 T 是可空类型,那么它可以为 null”的情况。

但是,Resharper 允许您这样做,所以我将两者结合使用。

当使用 Resharper 支持以及可为空时,该类看起来像这样:

public sealed class Test<T>
{
    public void SetItem([CanBeNull] T item)
    {
        _item = item;
    }

    [CanBeNull] public T GetItem()
    {
        return _item;
    }

    [CanBeNull] T _item = default!;
}

现在当我写这样的代码时:

var test = new Test<string>();
Console.WriteLine(test.GetItem().Length); // Warning: Possible null reference exception.

Resharper 警告我 test.GetItem() 可能返回 null - 即使编译器没有。

(很遗憾,C# 编译器让我无法做到这一点,但至少我们有 Resharper!)


[进一步考虑后编辑]

如果我只使用这样的类:

public sealed class Test<T>
{
    public void SetItem(T item)
    {
        _item = item;
    }

    public T GetItem()
    {
        return _item;
    }

    T _item = default!;
}

然后实例化代码有责任在需要时提供正确的可空类型 T,如下所示:

var test = new Test<string?>();

现在,如果您尝试取消引用可能为空的引用,编译器本身会警告您:

var test = new Test<string?>(); // Note "string?"
Console.WriteLine(test.GetItem().Length); // Warning about test.GetItem() possibly null.

这绝对比我在第二次编辑中使用的 Resharper 注释更可取,因为这实际上是错误的,因为这意味着您不能说这些值不为空。


[希望最终澄清编辑]

除非明确设置,否则实际类不允许访问_item,因此更像这样:

public sealed class Test<T>
{
    public void SetItem(T item)
    {
        _wasSet = true;
        _item = item;
    }

    public T GetItem()
    {
        if (!_wasSet)
            throw new InvalidOperationException("No item set.");

        return _item;
    }

    T _item = default!;

    bool _wasSet;
}

由于真实类的使用方式,要设置的项在对象实例化的时候是不可用的,所以无法在构造函数中设置默认值。

(真正的类是某人编写的提供队列的东西,您可以在其中访问队列中的第一个和第二个项目而无需将它们出列,_item 实际上是用于队列前面的单独值, 队列的其余部分存储在 Queue&lt;T&gt; 中。这不是我做的方式,真的,但这就是我正在使用的......)

【问题讨论】:

  • 如果有人打电话给var test = new Test&lt;string&gt;(); string x = test.GetItem();,你会发生什么?基本上你的代码在可空性上撒谎 - 所以有一个警告是有道理的。如果您可以在构造函数中接受该项目,那将是最好的。
  • @JonSkeet 在这种情况下,我希望它返回 null (当然它确实如此)。问题在于无法表达“如果类型 T 是引用类型,则它可能为空”的情况。 (我展示的类非常做作——实际代码更复杂,我试图将其转换为可为空的,而无需进行太多更改。)
  • 根据我的经验,使用可空性和泛型将是一个令人头疼的问题,正是因为您在代码中看到的问题。似乎没有任何好的方法可以为泛型类型的 T 成员指定不可为空性并且仍然支持结构和类。您最好的选择是将类设计为允许 null,这意味着您将不得不强制编译器接受您的代码。
  • @LasseV.Karlsen 我认为我最近的编辑似乎完全解决了这个问题。我避免了警告,但是当我取消引用它时,如果类型可以为空,编译器仍然会警告我。这也是List&lt;T&gt; 的工作方式,例如List&lt;string?&gt;
  • 好吧,如果你这样做:var x = new Test&lt;string&gt;(); var y = x.GetItem().Length;?编译,没有警告(因为T 不是T? 所以这里有一个承诺你不会得到null,但你是因为default!)。基本上GetItem()返回T,而不是T?,所以它不应该返回null,但是默认值是default!,基本上是“null,不要抱怨!”。

标签: c# c#-8.0 nullable-reference-types


【解决方案1】:

作为一种可能的选择,您可以使用 default 文字和 null-forgiving 运算符的组合,例如

private T _item = default!;

【讨论】:

  • 行得通!我应该想到的。 (我会尽可能标记为答案。)
  • 您正在通过规避该功能将现有代码转换为可为空的。看起来是个坏主意。如果存在使用空值的现有错误,您仍然可以使用它。
  • @Dialectus 在这种情况下是可以的,因为类已经可以返回空值,所以代码已经在处理它了。该类试图表达这样一个事实,即“如果 T 可以为空,则它可能具有空值”。问题在于,对于泛型和可为空的,如果类型参数可以是引用或值类型,则实际上无法表达这一事实。
  • 幸运的是,如果您使用的是 Resharper,那么有一种方法可以表达这一点,所以除了使用可空值之外,我还在使用它。我会将其添加到我的问题中以澄清。
猜你喜欢
  • 1970-01-01
  • 2023-03-31
  • 2021-11-02
  • 1970-01-01
  • 2012-03-27
  • 1970-01-01
  • 2019-12-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多