Alv*_*tor 5 c# generics c#-8.0 nullable-reference-types
我有一个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
}
Run Code Online (Sandbox Code Playgroud)
你会像这样使用它:
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}");
}
}
Run Code Online (Sandbox Code Playgroud)
进而:
var result = GetSomething();
if (!result.IsSuccess()) return result; //<- error passed on.
int resultData = result.Data; //<- no errors, so there is something in here.
Run Code Online (Sandbox Code Playgroud)
到现在为止,一切都很好。但是,当我引入可空类型时,我遇到了一个问题:
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
}
Run Code Online (Sandbox Code Playgroud)
var result = GetSomething();
if (!result.IsSuccess()) return result; //<- error passed on.
int resultData = result.Data; //<- WARNING!!! POSSIBLE DEREFERENCE OF NULL
Run Code Online (Sandbox Code Playgroud)
现在的问题是:我确定它result.Data包含了一些东西,因为它通过了这IsSuccess()一步。我怎样才能让编译器放心呢?有没有办法或 C#8 可空概念与此不兼容?
是否有其他方法以类似的方式处理结果?(传递容器而不是异常)。
Ps 1
请不要建议使用result.Data!;.
Ps 2
这段代码已经用了一千行以上了,所以如果能改在界面上,而不是在用法上,那就更好了。
更新
如果您确实更改了用法并转换IsSuccess为属性,则可以摆脱可空性问题并获得详尽匹配。这个 switch 表达式是详尽的,即编译器可以检查是否满足所有可能性。它确实要求每个分支只检索一个有效的属性:
var message=result switch { {IsSuccess:true,Data:var data} => $"Got some: {data}",
{IsSuccess:false,Description:var error} => $"Oops {error}",
};
Run Code Online (Sandbox Code Playgroud)
如果您的方法接受并返回IResult<T>对象,您可以编写如下内容:
IResult<string> Doubler(IResult<string> input)
{
return input switch { {IsSuccess:true,Data:var data} => new Ok<string>(data+ "2"),
{IsSuccess:false} => input
};
}
...
var result2=new Ok<string>("3");
var message2=Doubler(result2) switch {
{IsSuccess:true,Data:var data} => $"Got some: {data}",
{IsSuccess:false,Description:var error} => $"Oops {error}",
};
Run Code Online (Sandbox Code Playgroud)
原答案
看起来真正的问题是结果模式的实现。这种模式有两个特点:
像 Rust 这样的一些语言有一个内置的类型。支持选项类型/区分联合(如 F#)的函数式语言,只需使用以下命令即可轻松实现:
type Result<'T,'TError> =
| Ok of ResultValue:'T
| Error of ErrorValue:'TError
Run Code Online (Sandbox Code Playgroud)
详尽的模式匹配意味着客户端必须同时处理这两种情况。尽管这种类型非常普遍,但它已成为语言本身。
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;
}
Run Code Online (Sandbox Code Playgroud)
我们可以使用结构而不是类。
或者,要使用更接近 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);
}
Run Code Online (Sandbox Code Playgroud)
我们可以使用模式匹配来处理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")
};
Run Code Online (Sandbox Code Playgroud)
C# 9
C# 9(可能)将通过枚举类添加可区分的联合。我们将能够写:
enum class Result
{
Ok(MySuccess Data),
Fail(MyError Error)
}
Run Code Online (Sandbox Code Playgroud)
并通过模式匹配使用它。只要有匹配的解构器,此语法就已经在 C# 8 中起作用。C# 9 将添加详尽的匹配并可能也简化语法:
var message=result switch { Result.Ok (var Data) => $"Got some: {Data}",
Result.Fail (var Error) => $"Oops {Error}"
};
Run Code Online (Sandbox Code Playgroud)
通过 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;
...
}
Run Code Online (Sandbox Code Playgroud)
该Description和Data性能也得以实现,作为权宜之计-他们打破了结果模式和模式匹配,使他们过时反正:
public class Ok<TSuccess,TError>:IResult<TSuccess,TError>
{
...
public TError Description=>throw new InvalidOperationException("A Success Result has no Description");
...
}
Run Code Online (Sandbox Code Playgroud)
默认接口成员可用于避免乱扔具体类型:
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);
}
Run Code Online (Sandbox Code Playgroud)
添加详尽匹配的修改
如果我们只使用一个标志和迁移属性,我们可以避免模式匹配异常中的默认情况:
public interface IResult<TSuccess,TError>
{
public bool IsSuccess{get;}
public bool IsFailure=>!IsSuccess;
//Migration methods
...
}
var message2=result switch { {IsSuccess:true,Data:var data} => $"Got some: {data}",
{IsSuccess:false,Description:var error} => $"Oops {error}",
};
Run Code Online (Sandbox Code Playgroud)
这次编译器检测到只有两种情况,都覆盖了。迁移属性允许编译器检索正确的类型。消费代码必须更改并使用正确的模式,但我怀疑它已经起作用了
在 c# 9 中,有一个MemberNotNullWhen属性可以在IsSuccess选中时隐藏相应的警告
public interface IResult<out T>
{
[MemberNotNullWhen(true, nameof(Data))]
bool IsSuccess();
T? Data { get; }
}
IResult<string> res = GetSomeResult();
if(!res.IsSuccess())
throw new Exception(); // or just return something else
var len = res.Data.Length; // no nullability warning
Run Code Online (Sandbox Code Playgroud)
微软官方文档尚未更新。我在本节中收到了更多有关它的可空性信息。为了使用上述属性,您必须使用.net50,或将csproj文件中的语言版本设置为c#9。向后移植这些属性的另一种方法是使用Nullable 包。
| 归档时间: |
|
| 查看次数: |
2273 次 |
| 最近记录: |