对于.NET 5及更高版本:请使用新的
MemberNotNullWhen
属性。
{{link1:.NET 5.0和C# 9.0引入了MemberNotNullWhenAttribute
类型。
- (C# 9.0还引入了
init
属性,对于不需要额外构造函数参数的不可变类型中的可选属性非常有用)。
将MemberNotNullWhen
应用于任何Boolean
/bool
属性,其中true
/false
值断言当该属性为true
或false
时,某些字段和属性将是"notnull
"(尽管您目前无法直接断言基于属性的属性将是null
)
当单个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();
。这意味着对象处于无效状态。
你可能认为这不是个问题,但是如果你需要不断地向一个类添加或删除属性,最终你会粗心大意地忘记设置一个必需的属性,然后你的程序就会崩溃。
一个更好的
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 );
}
else
{
ShowErrorMessage( response.ErrorMessage );
}
长答案(为了更好地编写类):
更新到.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; }
}
}
(从计算机科学理论的角度来看,
上述类是一个联合类型)。
这种设计的优点有很多:
类设计清楚地表明了结果数据只有两种可能的“形状”:“OK”或“Failed”,而每个子类都拥有其特定上下文的数据成员(分别是“Stream”和“ErrorMessage”)。
类型层次结构是封闭的(基本的“抽象”类型有一个私有构造函数),它的两个子类型都是“sealed”,这使得除了“OK”或“Failed”之外的结果是不可能的。
这基本上与“枚举(类)类型”相同,就像Java的“enum”类一样。而C#的“enum”更像是一个命名常量,编译器和语言不保证C#的“enum”值在运行时是有效的(例如,即使“123”不是一个定义的值,你仍然可以执行“MyEnum v = (MyEnum)123”)。
“OK”和“Failed”构造函数中的验证逻辑保证了“DecryptStatus.OK”始终意味着结果类型是“DecryptResponse.OK”,并且具有非“null”的“Stream”属性。同样,如果“Status != DecryptStatus.OK”,则得到的是一个“DecryptResponse.Failed”对象。
“implicit”操作符的定义意味着返回“DecryptResponse”的方法可以直接返回“Stream”或“ValueTuple”,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>`
}
}
或者如果你想更明确一些:
public DecryptResponse DecryptSomething()
{
Stream someStream = ...
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 )
{
}
}
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完全支持这些类型的序列化 - 但如果您需要反序列化它们,那么您将需要一个自定义的合同解析器,不幸的是 - 但编写一个通用的合同解析器来处理封闭类型是很简单的,一旦您编写了一个,您就不需要再编写另一个。)
NotNullWhen
仅适用于方法参数,对于属性,您可以使用NotNull
属性。或将其转换为方法并使用NotNullIfNotNull
。您可以在此文章中找到一些想法尝试可空引用类型。 - Pavel Anikhouski