提问人:mabeto5p 提问时间:5/28/2020 最后编辑:mabeto5p 更新时间:11/15/2023 访问量:3970
C# 可为 null:使 nullchecking 依赖于另一个属性/变量
C# Nullable: Make nullchecking dependent on another property/variable
问:
我刚刚在我的 .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
}
上述方法用于方法不允许 null 的情况。Verify
但我知道流不是空的,因为DecryptStatus==Ok
if (decryptResponse.Status != DecryptStatus.Ok)
return (decryptResponse, null);
var verifyResponse = Verify(customerId, decryptResponse.Stream);
return (decryptResponse, verifyResponse);
是否有任何标签允许这种逻辑,或者是否需要对代码进行重大重写?
答:
NotNullWhenAttribute
仅用于参数。它告诉编译器,当方法返回指定值(true 或 false)时,(out) 参数不为 null。例如
public bool TryParse(string s, [NotNullWhen(true)] out Person person);
这意味着当方法返回时不会为空。person
true
但是这个属性不适合你想要实现的目标:
- NotNullWhen 不能应用于类属性 - 它只能与方法参数一起使用。
- NotNullWhen 不提供对某些外部值(如类属性)的依赖性——它只能使用方法参数所属的返回值。更重要的是,这个返回值只能是布尔值。
但是你可以尝试使用方法代替
public bool TryDecrypt(Foo bar,
[NotNullWhen(false) out DecryptError error, // wraps error status & message
[NotNullWhen(true)] out Stream stream)
或使用 null-forgiving 运算符
if (decryptResponse.Status == DecryptStatus.Ok)
{
// decryptResponse.Stream!
}
对于 .NET 5 及更高版本:使用 new 属性。MemberNotNullWhen
MemberNotNullWhenAttribute
类型是在 .NET 5.0 和 C# 9.0 中引入的。- (C# 9.0 还引入了
init
属性,这些属性对于不可变类型中的可选属性非常有用,而无需无关的构造函数参数)。
- (C# 9.0 还引入了
通过将其应用于任何 / 属性来使用,其中其 / 值断言某些字段和属性在该属性为 or 时将为 “”(尽管您还不能根据属性直接断言属性将为
null
)MemberNotNullWhen
Boolean
bool
true
false
notnull
true
false
当单个属性指示多个属性时,您可以应用多个属性 - 也可以使用属性 ctor。
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; }
}
当然,这种方法存在一个巨大的漏洞:编译器无法验证 、 和 是否设置正确。在不设置任何属性的情况下,没有什么可以阻止程序执行的操作。这意味着对象处于无效状态。Status
Stream
ErrorMessage
return new DecryptResponse();
你可能认为这不是问题,但如果你需要继续向类添加或删除新属性,最终你会变得粗心大意,忘记设置你需要的必需属性,然后你的程序就会爆炸。
更好的实现是为 2 个互斥的有效状态使用两个单独的构造函数,如下所示:DecryptResponse
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; }
}
然后这样使用:
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
}
长答案(一般用于编写更好的类):
更新到 .NET 5 + C# 9 并避免上述无效状态问题的替代方法是使用更好的类设计,使无效状态无法表示。
我不喜欢可变的结果对象(又名进程内 DTO)——即那些在每个属性上都有的对象),因为如果没有主构造函数,就无法硬性地保证对象实例将被正确初始化。get; set;
(不要将这与 Web 服务 DTO 混淆,尤其是 JSON DTO,其中有充分的理由使每个属性都可变,但这是另一个讨论)
如果我为一个不可用的旧 .NET 平台编写,那么我会像下面这样设计:MemberNotNullWhen
DecryptResponse
public abstract class DecryptResponse
{
public static implicit operator DecryptResponse( Stream okStream )
{
return new DecryptResponse.OK( okStream );
}
public static implicit operator DecryptResponse( ( DecryptStatus status, String errorMessage ) pair )
{
return new DecryptResponse.Failed( pair.status, pair.errorMessage );
}
private DecryptResponse( DecryptStatus status )
{
this.Status = status;
}
public DecryptStatus Status { get; }
public sealed class OK : DecryptResponse
{
public OK( Stream stream )
: base( DecryptStatus.OK )
{
this.Stream = stream ?? throw new ArgumentNullException(nameof(stream));
}
public Stream Stream { get; }
}
public sealed class Failed : DecryptResponse
{
public Failed ( DecryptStatus status, String errorMessage )
: base( status )
{
if( status == DecryptStatus.OK ) throw new ArgumentException( message: "Value cannot be " + nameof(DecryptStatus.OK) + "." );
this.ErrorMessage = errorMessage ?? throw new ArgumentNullException(nameof(errorMessage));
}
public String ErrorMessage { get; }
}
}
(从 CS 理论的角度来看,上面的类是联合类型)。
这种设计的优点很多:
- 类设计清楚地表明,结果数据只有 2 种可能的“形状”:“OK”或“Failed”——每个子类都拥有其特定于上下文的数据成员(和)。
Stream
ErrorMessage
- 类型层次结构是封闭的(基类型有一个构造函数),并且它的两个子类型都是 ,因此不可能有除 或 之外的结果。
abstract
private
sealed
OK
Failed
- 这与“枚举(类)类型”基本相同,例如 Java 的 -classes。而 C# a 更像是一个命名常量,编译器和语言不能保证 C# 值在运行时有效(例如,即使不是定义的值,您也始终可以这样做)。
enum
enum
enum
MyEnum v = (MyEnum)123
123
- 这与“枚举(类)类型”基本相同,例如 Java 的 -classes。而 C# a 更像是一个命名常量,编译器和语言不能保证 C# 值在运行时有效(例如,即使不是定义的值,您也始终可以这样做)。
- and 构造函数中的验证逻辑提供了保证,该保证始终意味着结果类型具有非属性。同样,如果你有一个对象。
OK
Failed
DecryptStatus.OK
DecryptResponse.OK
null
Stream
Status != DecryptStatus.OK
DecryptResponse.Failed
- 运算符定义意味着返回 a 的方法可以直接返回 or,并且 C# 编译器将自动为您执行转换。
implicit
DecryptResponse
Stream
ValueTuple<DecryptStatus,String>
这样的结果类型是这样返回的:
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>`
}
}
或者,如果您想明确:
public DecryptResponse DecryptSomething()
{
Stream someStream = ... // do stuff
if( itWorked )
{
return new DecryptResponse.OK( someStream );
}
else
{
DecryptStatus errorStatus = ...
return new DecryptResponse.Failed( errorStatus, "someErrorMessage" );
}
}
并像这样食用:
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.");
(不幸的是,C# 编译器还不够聪明,无法识别封闭类型的层次结构,因此需要该语句,但希望最终没有必要)。else throw new...
如果你需要支持使用 JSON.net 进行序列化,那么你不需要做任何事情,因为 JSON.NET 很好地支持这些类型的序列化 - 但如果你需要反序列化它们,那么你需要一个自定义的合约解析器,不幸的是 - 但是为封闭类型编写一个通用的合约解析器很简单,一旦你编写了一个,你就不需要再写另一个了。
评论
StatusIsOK
DecryptResponse.OK
NotNullWhen
MaybeNullWhen
const bool
评论
NotNullWhen
仅适用于方法参数,对于属性,您可以使用 Atirbute。或者将其转换为方法并使用某些东西。您可以在本文中找到一些想法 尝试可为 null 的引用类型NotNull
NotNullIfNotNull