C# 8 可空值和结果容器
C# 8 nullables and Result container
我有一个 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
这段代码已经在上千行或更多行中使用,所以如果可以在界面上进行更改,而不是在用法上进行更改,那就更好了。
更新
如果您 确实 更改了用法,并将 IsSuccess
转换为 属性,您可以摆脱可空性问题 并且 得到穷举匹配。这个 switch 表达式是详尽无遗的,即编译器可以检查是否满足所有可能性。它确实要求每个分支只检索有效的 属性 虽然 :
var message=result switch { {IsSuccess:true,Data:var data} => $"Got some: {data}",
{IsSuccess:false,Description:var error} => $"Oops {error}",
};
如果你的方法接受 return 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}",
};
原答案
看起来真正的问题是Result模式的实现。这种模式有两个特点:
- 它防止在类型级别使用无效的结果值。它通过使用两种不同的类型来表示好的和坏的结果来做到这一点。通过这样做,每种类型只携带它需要的东西。
- 它强制客户端处理所有情况或明确忽略它们。
某些语言(如 Rust)为此内置了类型。支持选项 types/discriminated 联合的函数式语言,如 F#,只需 :
即可轻松实现它
type Result<'T,'TError> =
| Ok of ResultValue:'T
| Error of ErrorValue:'TError
详尽的模式匹配意味着客户端必须处理这两种情况。尽管这种类型很常见,但它已成为语言本身。
C#8
在 C# 8 中,我们可以实现这两种类型,而无需穷举模式匹配。现在,这些类型需要一个公共 class,一个接口或抽象 class,它实际上不需要任何成员。有很多方法可以实现它们,例如:
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;
}
我们可以使用结构而不是 classes。
或者,要使用更接近 C# 9 的可区分联合的语法,可以嵌套 classes。类型仍然可以是接口,但我真的不喜欢写 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# 的选项类型还将值的 "kind" 公开为 tag 。我们可以将这样的方法添加到接口和 return 来自实现的固定值:
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
属性也可以实现,作为权宜之计——它们打破了结果模式,模式匹配使它们无论如何都过时了:
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);
}
修改以添加详尽匹配
如果我们只使用 one 标志和迁移属性,我们可以避免模式匹配异常中的默认情况:
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}",
};
这次编译器检测到只有两种情况,都被覆盖了。迁移属性允许编译器检索正确的类型。消费代码必须更改 和 使用正确的模式,但我怀疑它已经工作
这是上面的 C# 8.0 部分(太棒了!)@PanagiotisKanavos 一篇回答:
using System;
#nullable enable
namespace ErrorHandling {
public interface IResult<TSuccess, TError> {
public bool OK { get; }
public TSuccess Data => (this is Ok<TSuccess, TError>(var Data)) ? Data : throw new InvalidOperationException("An Error has no data");
public TError Error => (this is Fail<TSuccess, TError>(var Error)) ? Error : throw new InvalidOperationException("A Success Result has no Description");
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);
}
public class Ok<TSuccess, TError> : IResult<TSuccess, TError> {
public bool OK => true;
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 bool OK => false;
public TError Error { get; }
public Fail(TError error) => Error = error;
public void Deconstruct(out TError error) => error = Error;
}
class Main {
public IResult<int, string> F() {
if (DateTime.Now.Year < 2020) return IResult<int, string>.Good(3);
return IResult<int, string>.Bad("error");
}
public void F1() {
var message = F() switch {
{ OK: true, Data: var data } => $"Got some: {data}",
{ OK: false, Error: var error } => $"Oops {error}",
};
Console.WriteLine(message);
}
public void F2() {
if (F() is { OK: false, Error: var error }) {
Console.WriteLine(error);
return;
}
if (F() is { OK: true, Data: var data }) { // Is there a way to get data without a new scope ?
Console.WriteLine(data);
}
}
}
}
在 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
官方microsoft docs暂未更新。
我在 this 章节中收到了更多关于它的可空性信息。
为了使用上述属性,您必须使用.net50,或将csproj 文件中的语言版本设置为c#9。向后移植这些属性的另一种方法是使用 Nullable package.
我有一个 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
这段代码已经在上千行或更多行中使用,所以如果可以在界面上进行更改,而不是在用法上进行更改,那就更好了。
更新
如果您 确实 更改了用法,并将 IsSuccess
转换为 属性,您可以摆脱可空性问题 并且 得到穷举匹配。这个 switch 表达式是详尽无遗的,即编译器可以检查是否满足所有可能性。它确实要求每个分支只检索有效的 属性 虽然 :
var message=result switch { {IsSuccess:true,Data:var data} => $"Got some: {data}",
{IsSuccess:false,Description:var error} => $"Oops {error}",
};
如果你的方法接受 return 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}",
};
原答案
看起来真正的问题是Result模式的实现。这种模式有两个特点:
- 它防止在类型级别使用无效的结果值。它通过使用两种不同的类型来表示好的和坏的结果来做到这一点。通过这样做,每种类型只携带它需要的东西。
- 它强制客户端处理所有情况或明确忽略它们。
某些语言(如 Rust)为此内置了类型。支持选项 types/discriminated 联合的函数式语言,如 F#,只需 :
即可轻松实现它type Result<'T,'TError> =
| Ok of ResultValue:'T
| Error of ErrorValue:'TError
详尽的模式匹配意味着客户端必须处理这两种情况。尽管这种类型很常见,但它已成为语言本身。
C#8
在 C# 8 中,我们可以实现这两种类型,而无需穷举模式匹配。现在,这些类型需要一个公共 class,一个接口或抽象 class,它实际上不需要任何成员。有很多方法可以实现它们,例如:
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;
}
我们可以使用结构而不是 classes。
或者,要使用更接近 C# 9 的可区分联合的语法,可以嵌套 classes。类型仍然可以是接口,但我真的不喜欢写 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# 的选项类型还将值的 "kind" 公开为 tag 。我们可以将这样的方法添加到接口和 return 来自实现的固定值:
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
属性也可以实现,作为权宜之计——它们打破了结果模式,模式匹配使它们无论如何都过时了:
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);
}
修改以添加详尽匹配
如果我们只使用 one 标志和迁移属性,我们可以避免模式匹配异常中的默认情况:
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}",
};
这次编译器检测到只有两种情况,都被覆盖了。迁移属性允许编译器检索正确的类型。消费代码必须更改 和 使用正确的模式,但我怀疑它已经工作
这是上面的 C# 8.0 部分(太棒了!)@PanagiotisKanavos 一篇回答:
using System;
#nullable enable
namespace ErrorHandling {
public interface IResult<TSuccess, TError> {
public bool OK { get; }
public TSuccess Data => (this is Ok<TSuccess, TError>(var Data)) ? Data : throw new InvalidOperationException("An Error has no data");
public TError Error => (this is Fail<TSuccess, TError>(var Error)) ? Error : throw new InvalidOperationException("A Success Result has no Description");
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);
}
public class Ok<TSuccess, TError> : IResult<TSuccess, TError> {
public bool OK => true;
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 bool OK => false;
public TError Error { get; }
public Fail(TError error) => Error = error;
public void Deconstruct(out TError error) => error = Error;
}
class Main {
public IResult<int, string> F() {
if (DateTime.Now.Year < 2020) return IResult<int, string>.Good(3);
return IResult<int, string>.Bad("error");
}
public void F1() {
var message = F() switch {
{ OK: true, Data: var data } => $"Got some: {data}",
{ OK: false, Error: var error } => $"Oops {error}",
};
Console.WriteLine(message);
}
public void F2() {
if (F() is { OK: false, Error: var error }) {
Console.WriteLine(error);
return;
}
if (F() is { OK: true, Data: var data }) { // Is there a way to get data without a new scope ?
Console.WriteLine(data);
}
}
}
}
在 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
官方microsoft docs暂未更新。 我在 this 章节中收到了更多关于它的可空性信息。 为了使用上述属性,您必须使用.net50,或将csproj 文件中的语言版本设置为c#9。向后移植这些属性的另一种方法是使用 Nullable package.