我有一个用于处理错误的IResult<T>
容器。看起来像这样:
public interface IResult<out T>
{
ResultOutcome Outcome { get; } //enum: {Failure, Uncertain, Success}
string Description { get; } //string describing the error, in case of !Success
bool IsSuccess(); //Outcome == Success
T Data { get; } //If success, it contains the data passed on, otherwise NULL
}
您会这样使用它:
IResult<int> GetSomething()
{
try{
int result = //things that might throw...
return Result<int>.Success(result);
}
catch(Exception e)
{
return Result<int>.Failure($"Something went wrong: {e.Message}");
}
}
然后:
var result = GetSomething();
if (!result.IsSuccess()) return result; //<- error passed on.
int resultData = result.Data; //<- no errors, so there is something in here.
直到现在,一切都很好。但是,当我介绍可空类型时,我遇到了一个问题:
public interface IResult<out T> where T : class // unfortunately this is necessary
{
...
T? Data { get; } //If success, it contains the data passed on, otherwise NULL
}
var result = GetSomething();
if (!result.IsSuccess()) return result; //<- error passed on.
int resultData = result.Data; //<- WARNING!!! POSSIBLE DEREFERENCE OF NULL
现在是问题:我确定result.Data
包含某些内容,因为它已通过IsSuccess()
步骤。我该如何向编译器保证?有没有办法或者C#8可为空的概念与此不兼容?还有其他类似方法来处理结果吗? (传递容器而不是异常)。
提前感谢。
P.s。 1请不要使用result.Data!;
。
P.s。 2该代码已经在上千行或更多行中使用,因此,如果可以在界面上而不是在用法上进行更改,那就更好了。
看起来real问题是Result模式的实现。这种模式有两个特点:
诸如Rust之类的某些语言对此具有内置类型。支持选项类型/区分联合的函数式语言,例如F#,只需使用:
即可轻松实现type Result<'T,'TError> =
| Ok of ResultValue:'T
| Error of ErrorValue:'TError
详尽的模式匹配意味着客户必须处理两种情况。这种类型非常普遍,但它已成为语言本身。
C#8
在C#8中,我们可以实现两种类型,而无需进行详尽的模式匹配。目前,类型需要一个通用类,即接口或抽象类,它们实际上并不需要任何成员。有很多方法可以实现它们,例如:
public interface IResult<TSuccess,TError>{}
public class Ok<TSuccess,TError>:IResult<TSuccess,TError>
{
public TSuccess Data{get;}
public Ok(TSuccess data)=>Data=data;
public void Deconstruct(out TSuccess data)=>data=Data;
}
public class Fail<TSuccess,TError>:IResult<TSuccess,TError>
{
public TError Error{get;}
public Fail(TError error)=>Error=error;
public void Deconstruct(out TError error)=>error=Error;
}
我们可以使用结构代替类。
或者,要使用更接近C#9区分的并集的语法,可以嵌套这些类。类型仍然可以是接口,但是我真的不喜欢写new IResult<string,string>.Fail
或命名接口Result
而不是IResult
:
public abstract class Result<TSuccess,TError>
{
public class Ok:Result<TSuccess,TError>
{
public TSuccess Data{get;}
public Ok(TSuccess data)=>Data=data;
public void Deconstruct(out TSuccess data)=>data=Data;
}
public class Fail:Result<TSuccess,TError>
{
public TError Error{get;}
public Fail(TError error)=>Error=error;
public void Deconstruct(out TError error)=>error=Error;
}
//Convenience methods
public static Result<TSuccess,TError> Good(TSuccess data)=>new Ok(data);
public static Result<TSuccess,TError> Bad(TError error)=>new Fail(error);
}
我们可以使用模式匹配来处理Result
值。不幸的是,C#8没有提供详尽的匹配,因此我们也需要添加默认情况。
var result=Result<string,string>.Bad("moo");
var message=result switch { Result<string,string>.Ok (var Data) => $"Got some: {Data}",
Result<string,string>.Fail (var Error) => $"Oops {Error}"
_ => throw new InvalidOperationException("Unexpected result case")
};
C#9
C#9(可能)将通过enum classes添加已区分的并集。我们将能够写:
enum class Result
{
Ok(MySuccess Data),
Fail(MyError Error)
}
并通过模式匹配使用它。只要有匹配的解构函数,此语法就可以在C#8中使用。 C#9将添加详尽的匹配,并且可能还会简化语法:
var message=result switch { Result.Ok (var Data) => $"Got some: {Data}",
Result.Fail (var Error) => $"Oops {Error}"
};
通过DIM更新现有类型
[某些现有功能,例如IsSuccess
和Outcome
只是方便的方法。实际上,F#的选项类型还将值的“种类”公开为tag。我们可以向接口添加此类方法,并从实现中返回固定值:
public interface IResult<TSuccess,TError>
{
public bool IsSuccess {get;}
public bool IsFailure {get;}
public bool ResultOutcome {get;}
}
public class Ok<TSuccess,string>:IResult<TSuccess,TError>
{
public bool IsSuccess =>true;
public bool IsFailure =>false;
public bool ResultOutcome =>ResultOutcome.Success;
...
}
Description
和Data
属性也可以实现,作为一种止步措施-它们破坏了Result模式,并且模式匹配仍然使它们过时了:
public class Ok<TSuccess,TError>:IResult<TSuccess,TError>
{
...
public TError Description=>throw new InvalidOperationException("A Success Result has no Description");
...
}
默认接口成员可用于避免乱扔具体类型:
public interface IResult<TSuccess,TError>
{
//Migration methods
public TSuccess Data=>
(this is Ok<TSuccess,TError> (var Data))
?Data
:throw new InvalidOperationException("An Error has no data");
public TError Description=>
(this is Fail<TSuccess,TError> (var Error))
?Error
:throw new InvalidOperationException("A Success Result has no Description");
//Convenience methods
public static IResult<TSuccess,TError> Good(TSuccess data)=>new Ok<TSuccess,TError>(data);
public static IResult<TSuccess,TError> Bad(TError error)=>new Fail<TSuccess,TError>(error);
}