【问题标题】:c# Indicate nullability of generic memberc# 表示泛型成员的可空性
【发布时间】:2020-06-16 15:39:41
【问题描述】:

在这个人为的 C# 8 示例中:

#nullable enable
class Fred<T>
{
    T Value;  // If T is a nullable type, Value can be null.
    public Fred()                 { }
    public void SetValue(T value) { Value = value; }
    public T GetValue()           { return Value; }
    public string Describe()      { return Value.ToString() ?? "oops"; }
}
class George
{
    George()
    {
        Fred<George> fredGeorge = new Fred<George>();
        George g = fredGeorge.GetValue();
        Fred<float> fredFloat = new Fred<float>();
        float f = fredFloat.GetValue();
    }
}

我有三个设计目标:

  1. 如果我为 Fred 编写任何盲目假设“Value”永远不会为 null 的方法,编译器会警告我
  2. 如果我在 Fred 之外(比如在 George 中)编写任何盲目假设“GetValue”永远不会返回 null 的方法,编译器会警告我。
  3. 没有编译器警告(如果我编写的代码不会盲目假设值不会为空)

所以这个第一个版本还不错,我在 Fred 中收到一条警告,说 Describe() 可能正在取消引用空引用(满足目标 #1),但我也收到一条警告,指出在 Fred 的构造函数中 Value 未初始化(违反目标#3) 并且 George 编译时没有任何警告(违反目标 #2)。如果我进行此更改:

public Fred() { Value = default; }

George 仍然在没有警告的情况下进行编译(违反目标 #2),我在 Fred 的构造函数中收到关于“可能的空引用分配”的不同警告(违反目标 #3)。

我可以通过使用 null-forgiving 运算符摆脱可能的 null 引用分配:

public Fred() { Value = default!; }

现在 Fred 只有正确的警告(可能在 Describe() 中取消引用),但 George 也编译时没有警告(违反目标 #2)。

如果我试图表明“值”可以为空:

T? Value;

我收到一个编译器错误,提示“必须知道可以为空的类型参数是值类型或不可为空的引用类型”,所以这不好。

如果我回到

T Value;

并添加“MaybeNull”属性:

[return: MaybeNull]
public T GetValue() { return Value; }

我收到两个警告 - 一个在 Fred.Describe() 中警告可能为 null 取消引用(正确),另一个在 George 中警告 fredGeorge.GetValue() 可能为 null(正确)。没有关于 fredFloat.GetValue() 为空(正确)的警告。

所以在添加代码以期望空引用之后,我最终得到的是:

class Fred<T>
{
    T Value;

    public Fred()
    {
        Value = default!;
    }

    public void SetValue(T value)
    {
        Value = value;
    }

    [return: MaybeNull]
    public T GetValue()
    {
        return Value;
    }

    public string Describe()
    {
        return (Value == null) ? "null" : (Value.ToString() ?? "ToString is null");
    }
}

class George
{
    George()
    {
        Fred<George> fredGeorge = new Fred<George>();
        George? g = fredGeorge.GetValue();
        Fred<float> fredFloat = new Fred<float>();
        float f = fredFloat.GetValue();
    }
}

这是该功能的正确模式吗?

【问题讨论】:

  • 为什么使用Value 字段和GetValueSetValue 两种方法而不是常规属性?此外,您的代码没有考虑到null 值可以传递给方法SetValue。如果您在George 类中编写代码fredGeorge.SetValue(null),您将收到警告:Cannot convert null literal to non-nullable reference type.
  • 我以这种方式编写 GetValue() 和 SetValue() 只是为了探索和理解 C# 可空性特性。是的,在真正的应用程序中,我只会使用常规属性。关于 SetValue(null),你说得很对 - 你知道我需要做什么来解决这个问题吗?

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


【解决方案1】:

System.Diagnostics.CodeAnalysis 中有一个属性AllowNullAttribute 指定允许null 作为输入,即使相应的类型不允许它也是如此。我将在您的最终示例中使用此属性:

  • 装饰字段Value这将允许我们在分配Value = default 中删除null-forgiving operator。编译器不会警告我们Possible null reference assignment,因为现在它知道null 值可以分配给属性Value
  • 装饰方法SetValue 的参数T value它将允许将null 值传递给方法SetValue 而不会收到编译器警告Cannot convert null literal to non-nullable reference type(目前,如果我们将 null 值传递给方法 SetValue,我们将收到此警告)

这是带有建议更改的最终示例:

class Fred<T>
{
    // AllowNull attribute says that a null value
    // can be assigned to the field Value.
    [AllowNull]
    private T Value;

    public Fred()
    {
        // Now we can delete null-forgiving operator, because compiler knows
        // that null value can be assigned to the field Value.
        Value = default;
    }

    // AllowNull attribute says that a null value
    // can be passed to the method SetValue.
    public void SetValue([AllowNull] T value)
    {
        Value = value;
    }

    [return: MaybeNull]
    public T GetValue()
    {
        return Value;
    }

    public string Describe()
    {
        return (Value == null) ? "null" : (Value.ToString() ?? "ToString is null");
    }
}

class George
{
    George()
    {
        Fred<George> fredGeorge = new Fred<George>();
        George? g = fredGeorge.GetValue();

        // Compiler does not warn us "Cannot convert null literal to
        // non-nullable reference type" because it knows that a null
        // value can be passed to the method SetValue.
        fredGeorge.SetValue(null);

        Fred<float> fredFloat = new Fred<float>();
        float f = fredFloat.GetValue();
    }
}

如果我们使用常规属性而不是字段Value 和一对方法GetValueSetValue,那么我们可以以更清晰的方式重写最终样本:

class Fred<T>
{
    // Here we tell that:
    // 1) a null value can be assigned;
    // 2) a null value can be returned.
    [AllowNull, MaybeNull]
    public T Value { get; set; }

    public Fred()
    {
        // Compiler does not warn us "Possible null reference assignment".
        // It knows that a null value can be assigned. It is correct.
        // We can delete null-forgiving operator.
        Value = default;
    }

    public string Describe()
    {
        // If we delete null checking, then we get a warning "Dereference of
        // a possibly null reference". It is correct. Compiler helps us to avoid
        // NullReferenceException.
        return (Value == null) ? "null" : (Value.ToString() ?? "ToString is null");
    }
}

class George
{
    George()
    {
        Fred<George> fredGeorge = new Fred<George>();

        // Compiler warns us "Converting null literal or possible null
        // value to non-nullable type". It is correct.
        // We should use nullable reference type George?.
        George g = fredGeorge.Value;

        // Compiler does not warn us "Cannot convert null literal to
        // non-nullable reference type". It knows that a null value
        // can be passed to the method SetValue. It is correct.
        fredGeorge.Value = null;

        Fred<float> fredFloat = new Fred<float>();
        float f = fredFloat.Value;
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-03-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-06
    相关资源
    最近更新 更多