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
MemberNotNullWhen属性。该MemberNotNullWhenAttribute类型是在 .NET 5.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)
当然,这种方法存在一个巨大的漏洞:编译器无法验证Status、Stream、 和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理论的角度来看,上面的类是联合类型)。
这种设计的优点很多:
Stream和ErrorMessage)。abstract类型有一个private构造函数),并且它的两个子类型都是sealed,因此不可能有除OKor之外的结果Failed。
enum类。而 C# aenum更像是一个命名常量,编译器和语言不保证 C#enum值在运行时有效(例如,MyEnum v = (MyEnum)123即使123不是定义值,您也始终可以这样做)。OK和构造函数中的验证逻辑提供了始终意味着结果类型具有 非属性的Failed保证。同样,如果你有一个对象。DecryptStatus.OK DecryptResponse.OKnull StreamStatus != DecryptStatus.OKDecryptResponse.Failedimplicit符定义意味着返回 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 很好地支持这些类型的序列化 - 但如果您需要反序列化它们,那么您将需要一个自定义合约解析器,不幸的是 - 但为封闭类型编写一个通用的契约解析器是很简单的,一旦你编写了一个,你就不需要再编写另一个了。
NotNullWhenAttribute仅适用于参数。当方法返回指定值(true 或 false)时,它告诉编译器(out)参数不为 null。例如
public bool TryParse(string s, [NotNullWhen(true)] out Person person);
Run Code Online (Sandbox Code Playgroud)
这意味着person方法返回时不会为null true。
但此属性不适合您想要实现的目标:
但你可以尝试使用方法来代替
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)