C# Nullable:使 null 检查依赖于另一个属性/变量

mab*_*o5p 11 c# null attributes nullable

我刚刚在我的 .net core 3.1 项目中启用了 null 检查。

问题是我有一个响应类

public class DecryptResponse
{
    public DecryptStatus Status { get; set; }

    //This is the attribute in question
    [NotNullWhen(Status==DecryptStatus.Ok)]
    public Stream? Stream { get; set; }

    public string? ErrorMessage { get; set; }
}

public enum DecryptStatus
{
    Ok,
    InvalidData,
    KeyChecksumFailure,
    NoData,
    UnhandledError
}
Run Code Online (Sandbox Code Playgroud)

上面的方法用于该Verify方法不允许空值的情况。

但我知道该流不为空,因为DecryptStatus==Ok

if (decryptResponse.Status != DecryptStatus.Ok)
    return (decryptResponse, null);

var verifyResponse = Verify(customerId, decryptResponse.Stream);
return (decryptResponse, verifyResponse);
Run Code Online (Sandbox Code Playgroud)

是否有任何标签允许这种逻辑,或者是否需要对代码进行重大重写?

Dai*_*Dai 21

对于 .NET 5 及更高版本:使用新MemberNotNullWhen属性。

  • MemberNotNullWhenAttribute类型是在 .NET 5.0 和 C# 9.0 中引入的。

    • (C# 9.0 还引入了init属性,这对于不可变类型中的可选属性很有用,不需要额外的构造函数参数)。
  • MemberNotNullWhen通过将其应用于任何Boolean/bool属性来使用,其中true/值断言当该属性是or时false某些字段和属性将为“ ” (尽管您还不能直接断言属性基于属性)notnulltruefalsenull

  • 当单个bool属性指示多个属性时,null或者notnull您可以应用多个[MemberNotNullWhen]属性 - 或者您可以使用params String[]属性构造器。

  • 您可能会注意到您的Status属性不是bool- 这意味着您需要添加一个新属性,使该Status属性适应与bool一起使用的值[MemberNotNullWhen]

...就像这样:

public class DecryptResponse
{
    public DecryptStatus Status { get; init; }

    [MemberNotNullWhen( returnValue: true , nameof(DecryptResponse.Stream))]
    [MemberNotNullWhen( returnValue: false, nameof(DecryptResponse.ErrorMessage))]
    private Boolean StatusIsOK => this.Status == DecryptStatus.Ok;

    public Stream? Stream { get; init; }

    public string? ErrorMessage { get; init; }
}
Run Code Online (Sandbox Code Playgroud)

当然,这种方法存在一个巨大的漏洞:编译器无法验证StatusStream、 和ErrorMessage是否设置正确。return new DecryptResponse();如果不设置任何属性,没有什么可以阻止您的程序执行操作。这意味着该对象处于无效状态。

您可能认为这不是问题,但如果您需要继续向类添加或删除新属性,最终您会粗心并忘记设置所需的必需属性,然后您的程序就会崩溃。

更好的实现DecryptResponse将使用两个单独的构造函数来实现 2 个互斥的有效状态,如下所示:

public class DecryptResponse
{
    public DecryptResponse( Stream stream )
    {
        this.Status = DecryptStatus.OK;
        this.Stream = stream ?? throw new ArgumentNullException(nameof(stream));
        this.ErrorMessage = null;
    }

    public DecryptResponse( DecryptStatus error, String errorMessage )
    {
        if( error == DecryptStatus.OK ) throw new ArgumentException( paramName: nameof(error), message: "Value cannot be 'OK'." );
        
        this.Status       = error;
        this.Stream       = null;
        this.ErrorMessage = errorMessage ?? throw new ArgumentNullException(nameof(errorMessage ));
    }

    public DecryptStatus Status { get; }

    [MemberNotNullWhen( returnValue: true , nameof(DecryptResponse.Stream))]
    [MemberNotNullWhen( returnValue: false, nameof(DecryptResponse.ErrorMessage))]
    private Boolean StatusIsOK => this.Status == DecryptStatus.Ok;

    public Stream? Stream { get; }

    public String? ErrorMessage { get; }
}
Run Code Online (Sandbox Code Playgroud)

然后像这样使用:

DecryptResponse response = Decrypt( ... );
if( response.StatusIsOK )
{
    DoSomethingWithStream( response.Stream ); // OK! The compiler "knows" that `response.Stream` is not `null` here.
}
else
{
     ShowErrorMessage( response.ErrorMessage ); // ditto
}
Run Code Online (Sandbox Code Playgroud)

长答案(一般来说是为了编写更好的类):

更新到 .NET 5 + C# 9 并避免上述无效状态问题的替代方法是使用更好的类设计,使无效状态无法表示

我不喜欢可变结果对象(又名进程内 DTO)——即那些具有get; set;每个属性的对象),因为如果没有主构造函数,就无法硬性保证对象实例将被正确初始化。

(不要与 Web 服务 DTO 混淆,尤其是 JSON DTO,其中可能有充分的理由使每个属性可变,但这是另一个讨论)

如果我正在为不可用的较旧的 .NET 平台编写内容MemberNotNullWhen,那么我会进行DecryptResponse如下设计:

DecryptResponse response = Decrypt( ... );
if( response.StatusIsOK )
{
    DoSomethingWithStream( response.Stream ); // OK! The compiler "knows" that `response.Stream` is not `null` here.
}
else
{
     ShowErrorMessage( response.ErrorMessage ); // ditto
}
Run Code Online (Sandbox Code Playgroud)

(从CS理论的角度来看,上面的类是联合类型)。

这种设计的优点很多:

  • 类设计清楚地表明结果数据只有两种可能的“形状”:“OK”或“Failed” - 每个子类都拥有其特定于上下文的数据成员(分别为StreamErrorMessage)。
  • 类型层次结构是封闭的(基abstract类型有一个private构造函数),并且它的两个子类型都是sealed,因此不可能有除OKor之外的结果Failed
    • 这与“枚举(类)类型”基本相同,例如 Java 的enum类。而 C# aenum更像是一个命名常量,编译器和语言不保证 C#enum值在运行时有效(例如,MyEnum v = (MyEnum)123即使123不是定义值,您也始终可以这样做)。
  • OK和构造函数中的验证逻辑提供了始终意味着结果类型具有 非属性的Failed保证。同样,如果你有一个对象。DecryptStatus.OK DecryptResponse.OKnull StreamStatus != DecryptStatus.OKDecryptResponse.Failed
  • 运算implicit符定义意味着返​​回 a 的方法DecryptResponse可以直接返回Streamor,ValueTuple<DecryptStatus,String>并且 C# 编译器将自动为您执行转换。

这样的结果类型返回如下:

public DecryptResponse DecryptSomething()
{
    Stream someStream = ... // do stuff
    if( itWorked )
    {
        return someStream; // Returning a `Stream` invokes the DecryptResponse conversion operator method.
    }
    else
    {
        DecryptStatus errorStatus = ...
        return ( errorStatus, "someErrorMessage" ); // ditto for `ValueTuple<DecryptStatus,String>`
    }
}
Run Code Online (Sandbox Code Playgroud)

或者如果你想明确:

public DecryptResponse DecryptSomething()
{
    Stream someStream = ... // do stuff
    if( itWorked )
    {
        return new DecryptResponse.OK( someStream );
    }
    else
    {
        DecryptStatus errorStatus = ...
        return new DecryptResponse.Failed( errorStatus, "someErrorMessage" );
    }
}
Run Code Online (Sandbox Code Playgroud)

并像这样消费:

DecryptResponse response = DecryptSomething();
if( response is DecryptResponse.OK ok )
{
    using( ok.Stream )
    {
        // do stuff
    }
}
else if( response is DecryptResponse.Failed fail )
{
    Console.WriteLine( fail.ErrorMessage );
}
else throw new InvalidOperationException("This will never happen.");
Run Code Online (Sandbox Code Playgroud)

(不幸的是,C# 编译器还不够智能,无法识别封闭类型层次结构,因此需要该语句else throw new...但希望最终不需要)。

如果您需要使用 JSON.net 支持序列化,那么您不需要执行任何操作,因为 JSON.NET 很好地支持这些类型的序列化 - 但如果您需要反序列化它们,那么您将需要一个自定义合约解析器,不幸的是 - 但为封闭类型编写一个通用的契约解析器是很简单的,一旦你编写了一个,你就不需要再编写另一个了。


Ser*_*kiy 1

NotNullWhenAttribute仅适用于参数。当方法返回指定值(true 或 false)时,它告诉编译器(out)参数不为 null。例如

public bool TryParse(string s, [NotNullWhen(true)] out Person person);
Run Code Online (Sandbox Code Playgroud)

这意味着person方法返回时不会为null true

但此属性不适合您想要实现的目标:

  • NotNullWhen 不能应用于类属性 - 它只能与方法参数一起使用。
  • NotNullWhen 不提供对某些外部值(如类属性)的依赖 - 它只能使用参数所属方法的返回值。更重要的是,这个返回值只能是布尔值。

但你可以尝试使用方法来代替

public bool TryDecrypt(Foo bar,
    [NotNullWhen(false) out DecryptError error, // wraps error status & message
    [NotNullWhen(true)] out Stream stream)
Run Code Online (Sandbox Code Playgroud)

或者使用 null-forgiving 运算符

if (decryptResponse.Status == DecryptStatus.Ok)
{
    // decryptResponse.Stream!
}
Run Code Online (Sandbox Code Playgroud)