可空引用类型和 Either 模式
Nullable Reference Types and Either Pattern
我正在试用新的 Nullable Reference Types in C# 8.0,我遇到了以下问题。
鉴于此结构:
public readonly struct Either<TReturn, TError>
where TReturn : struct
where TError : struct
{
public TError? Error { get; }
public TReturn? Response { get; }
public Either(TError? error, TReturn? response)
{
if (error == null && response == null)
{
throw new ArgumentException("One argument needs not to be null.");
}
if (error != null && response != null)
{
throw new ArgumentException("One argument must be null.");
}
Error = error;
Response = response;
}
}
如何告诉编译器 either Error
or Response
不为空,并且他们不能 both 为空?有没有办法用新属性做这样的事情?
实现Either
monad时,你应该使用两个不同的构造函数。这样,您就可以轻松摆脱这些检查,因为您的实现将确保无法同时分配两个属性。
public readonly class Either<TReturn, TError>
{
bool _successful;
private TError _error { get; }
private TReturn _response { get; }
public Either(TError error)
{
_error = error;
}
public Either(TReturn response)
{
_successful = true;
_response = response;
}
}
除此之外,您需要添加一个方法(到结构),用于从结构中提取值,并将其转换为常见的 return 类型:
public Match<T>(Func<TError, T> errorFunc, Func<TResponse, T> successFunc)
=> _successful ? successFunc(_response) : errorFunc(_error);
这样,您就强制用户处理这两种情况(成功、错误)并提供将转换为通用类型的函数:
var errorEither = new Either<string, int>(10); // example of error code
var successEither = new Either<string, int>("success"); // example of success
var commonValueError = errorEither.Match<bool>(err => false, succ => true);
var commonValueSuccess = successEither.Match<bool>(err => false, succ => true);
结构更新
当结果类型更改为结构时,代码不会更改。要使用结构类型参数,必须将以下约束添加到接口和类型中:
where TResult : struct
where TError : struct
当我想到 Either 模式时,我想到的是 F#、模式匹配和可区分联合,而不是空值。事实上,Either
是 避免 空值的一种方法。事实上,问题的代码看起来像是试图创建一个 Result type, not just an Either. Scott Wlaschin's Railway Oriented Programming 展示了如何使用这种类型来实现函数式语言中的错误处理。
在 F# 中,Result 类型定义为:
type Result<'T,'TError> =
| Ok of ResultValue:'T
| Error of ErrorValue:'TError
我们还不能在 C# 8 中这样做,因为没有可区分的联合。这些是为 C# 9 计划的。
模式匹配
我们可以做的是使用模式匹配来获得相同的行为,例如:
interface IResult<TResult,TError>{} //No need for an actual implementation
public class Success<TResult,TError>:IResult<TResult,TError>
{
public TResult Result {get;}
public Success(TResult result) { Result=result;}
}
public class Error<TResult,TError>:IResult<TResult,TError>
{
public TError ErrorValue {get;}
public Error(TError error) { ErrorValue=error;}
}
这样就无法创建一个既成功又错误的IResult<>
。这可以与模式匹配一起使用,例如:
IResult<int,string> someResult=.....;
if(someResult is Success<int,string> s)
{
//Use s.Result here
}
简化表达式
鉴于 C# 8 的 property patterns,这可以重写为:
if(someResult is Success<int,string> {Result: var result} )
{
Console.WriteLine(result);
}
或者,使用 switch 表达式,典型的铁路式调用:
IResult<int,string> DoubleIt(IResult<int,string> data)
{
return data switch { Error<int,string> e=>e,
Success<int,string> {Result: var result}=>
new Success<int,string>(result*2),
_ => throw new Exception("Unexpected type!")
};
}
F# 不需要 throw
,因为 Result<'T,'TError>
不可能是 Ok
或 Error
以外的东西。在 C# 中,我们还 没有该功能。
switch 表达式允许穷举匹配。我认为如果 default 子句也丢失,编译器会生成警告。
有解构函数
如果类型有析构函数,表达式可以更简化一点,例如:
public class Success<TResult,TError>:IResult<TResult,TError>
{
public TResult Result {get;}
public Success(TResult result) { Result=result;}
public void Deconstruct(out TResult result) { result=Result;}
}
public class Error<TResult,TError>:IResult<TResult,TError>
{
public TError ErrorValue {get;}
public Error(TError error) { ErrorValue=error;}
public void Deconstruct(out TError error) { error=ErrorValue;}
}
在这种情况下,表达式可以写成:
return data switch {
Error<int,string> e => e,
Success<int,string> (var result) => new Success<int,string>(result*3),
_ => throw new Exception("Unexpected type!")
};
为空性
问题从可空引用类型开始,那么可空性呢?如果我们尝试传递 null,我们会在 C# 8 中收到警告吗?
是的,只要启用 NRT。此代码:
#nullable enable
void Main()
{
IResult<string,string> data=new Success<string,string>(null);
var it=Append1(data);
Console.WriteLine(it);
}
IResult<string,string> Append1(IResult<string,string> data)
{
return data switch { Error<string,string> e=>e,
Success<string,string> (var result)=>
new Success<string,string>(result+"1"),
_ => throw new Exception("Unexpected type!")
};
}
生成 CS8625: Cannot convert null literal to non-nullable reference type
正在尝试
string? s=null;
IResult<string,string> data=new Success<string,string>(s);
生成 CS8604: Possible null reference argument ....
您可以使用 Resharper ContractAnnotation
执行类似的操作。它不是特定于 C# 8 的,(但是......我不认为你的例子真的使用了可为空的引用类型,是吗?你正在使用可为空的结构。)
[ContractAnnotation(
"=> pError: null, pResponse: notnull; => pError: notnull, pResponse: null"
)]
public void Get(out TError? pError, out TReturn? pResponse) {
pError = Error;
pResponse = Response;
}
(字符串的意思是=>
左边是输入条件,=>
右边是输出条件,;
区分不同的情况,一个未标记的值指的是方法 return 值。所以在这种情况下:无论输入如何,输出条件都是 null/notnull 或 notnull/null 的以太。)
然后使用 C# 7 功能 out var
:
GetAnEitherFromSomewhere()
.Get(out var error, out var response);
if (error != null) {
// handle error
return;
}
// response is now known to be not null, because we can only get here if error was null
老实说,我发现 JetBrains 的注解 [NotNull]
、[CanBeNull]
和 [ContractAnnotation]
比可为 null 的引用类型更灵活(尽管更冗长)。基本上它们允许中间情况,其中存在值可以是 null
的情况,但也存在值不能是 null
的情况,并且这些情况在 运行 时间是可区分的.对于可为空的引用类型,我无法指定中间情况,我必须选择绝对可为空或绝对不可为空。
甚至像 TryParse
:
这样常见的东西
// should the result be nullable with nullable reference types on?
// you have to either lie with ! or else use ? and assume it can always be null
public bool TryParse(string pString, out SomeClass pResult) {
if (<cant parse>) {
pResult = null;
return false;
}
pResult = value;
return true;
}
// works great with JetBrains annotations and nullable reference types off
// now you can know that the result is null or notnull
// based on testing the bool return value
[ContractAnnotation("=> true, pResult: notnull; => false, pResult: null")]
public bool TryParse(string pString, out SomeClass pResult) {
...
}
我正在试用新的 Nullable Reference Types in C# 8.0,我遇到了以下问题。
鉴于此结构:
public readonly struct Either<TReturn, TError>
where TReturn : struct
where TError : struct
{
public TError? Error { get; }
public TReturn? Response { get; }
public Either(TError? error, TReturn? response)
{
if (error == null && response == null)
{
throw new ArgumentException("One argument needs not to be null.");
}
if (error != null && response != null)
{
throw new ArgumentException("One argument must be null.");
}
Error = error;
Response = response;
}
}
如何告诉编译器 either Error
or Response
不为空,并且他们不能 both 为空?有没有办法用新属性做这样的事情?
实现Either
monad时,你应该使用两个不同的构造函数。这样,您就可以轻松摆脱这些检查,因为您的实现将确保无法同时分配两个属性。
public readonly class Either<TReturn, TError>
{
bool _successful;
private TError _error { get; }
private TReturn _response { get; }
public Either(TError error)
{
_error = error;
}
public Either(TReturn response)
{
_successful = true;
_response = response;
}
}
除此之外,您需要添加一个方法(到结构),用于从结构中提取值,并将其转换为常见的 return 类型:
public Match<T>(Func<TError, T> errorFunc, Func<TResponse, T> successFunc)
=> _successful ? successFunc(_response) : errorFunc(_error);
这样,您就强制用户处理这两种情况(成功、错误)并提供将转换为通用类型的函数:
var errorEither = new Either<string, int>(10); // example of error code
var successEither = new Either<string, int>("success"); // example of success
var commonValueError = errorEither.Match<bool>(err => false, succ => true);
var commonValueSuccess = successEither.Match<bool>(err => false, succ => true);
结构更新
当结果类型更改为结构时,代码不会更改。要使用结构类型参数,必须将以下约束添加到接口和类型中:
where TResult : struct
where TError : struct
当我想到 Either 模式时,我想到的是 F#、模式匹配和可区分联合,而不是空值。事实上,Either
是 避免 空值的一种方法。事实上,问题的代码看起来像是试图创建一个 Result type, not just an Either. Scott Wlaschin's Railway Oriented Programming 展示了如何使用这种类型来实现函数式语言中的错误处理。
在 F# 中,Result 类型定义为:
type Result<'T,'TError> =
| Ok of ResultValue:'T
| Error of ErrorValue:'TError
我们还不能在 C# 8 中这样做,因为没有可区分的联合。这些是为 C# 9 计划的。
模式匹配
我们可以做的是使用模式匹配来获得相同的行为,例如:
interface IResult<TResult,TError>{} //No need for an actual implementation
public class Success<TResult,TError>:IResult<TResult,TError>
{
public TResult Result {get;}
public Success(TResult result) { Result=result;}
}
public class Error<TResult,TError>:IResult<TResult,TError>
{
public TError ErrorValue {get;}
public Error(TError error) { ErrorValue=error;}
}
这样就无法创建一个既成功又错误的IResult<>
。这可以与模式匹配一起使用,例如:
IResult<int,string> someResult=.....;
if(someResult is Success<int,string> s)
{
//Use s.Result here
}
简化表达式
鉴于 C# 8 的 property patterns,这可以重写为:
if(someResult is Success<int,string> {Result: var result} )
{
Console.WriteLine(result);
}
或者,使用 switch 表达式,典型的铁路式调用:
IResult<int,string> DoubleIt(IResult<int,string> data)
{
return data switch { Error<int,string> e=>e,
Success<int,string> {Result: var result}=>
new Success<int,string>(result*2),
_ => throw new Exception("Unexpected type!")
};
}
F# 不需要 throw
,因为 Result<'T,'TError>
不可能是 Ok
或 Error
以外的东西。在 C# 中,我们还 没有该功能。
switch 表达式允许穷举匹配。我认为如果 default 子句也丢失,编译器会生成警告。
有解构函数
如果类型有析构函数,表达式可以更简化一点,例如:
public class Success<TResult,TError>:IResult<TResult,TError>
{
public TResult Result {get;}
public Success(TResult result) { Result=result;}
public void Deconstruct(out TResult result) { result=Result;}
}
public class Error<TResult,TError>:IResult<TResult,TError>
{
public TError ErrorValue {get;}
public Error(TError error) { ErrorValue=error;}
public void Deconstruct(out TError error) { error=ErrorValue;}
}
在这种情况下,表达式可以写成:
return data switch {
Error<int,string> e => e,
Success<int,string> (var result) => new Success<int,string>(result*3),
_ => throw new Exception("Unexpected type!")
};
为空性
问题从可空引用类型开始,那么可空性呢?如果我们尝试传递 null,我们会在 C# 8 中收到警告吗?
是的,只要启用 NRT。此代码:
#nullable enable
void Main()
{
IResult<string,string> data=new Success<string,string>(null);
var it=Append1(data);
Console.WriteLine(it);
}
IResult<string,string> Append1(IResult<string,string> data)
{
return data switch { Error<string,string> e=>e,
Success<string,string> (var result)=>
new Success<string,string>(result+"1"),
_ => throw new Exception("Unexpected type!")
};
}
生成 CS8625: Cannot convert null literal to non-nullable reference type
正在尝试
string? s=null;
IResult<string,string> data=new Success<string,string>(s);
生成 CS8604: Possible null reference argument ....
您可以使用 Resharper ContractAnnotation
执行类似的操作。它不是特定于 C# 8 的,(但是......我不认为你的例子真的使用了可为空的引用类型,是吗?你正在使用可为空的结构。)
[ContractAnnotation(
"=> pError: null, pResponse: notnull; => pError: notnull, pResponse: null"
)]
public void Get(out TError? pError, out TReturn? pResponse) {
pError = Error;
pResponse = Response;
}
(字符串的意思是=>
左边是输入条件,=>
右边是输出条件,;
区分不同的情况,一个未标记的值指的是方法 return 值。所以在这种情况下:无论输入如何,输出条件都是 null/notnull 或 notnull/null 的以太。)
然后使用 C# 7 功能 out var
:
GetAnEitherFromSomewhere()
.Get(out var error, out var response);
if (error != null) {
// handle error
return;
}
// response is now known to be not null, because we can only get here if error was null
老实说,我发现 JetBrains 的注解 [NotNull]
、[CanBeNull]
和 [ContractAnnotation]
比可为 null 的引用类型更灵活(尽管更冗长)。基本上它们允许中间情况,其中存在值可以是 null
的情况,但也存在值不能是 null
的情况,并且这些情况在 运行 时间是可区分的.对于可为空的引用类型,我无法指定中间情况,我必须选择绝对可为空或绝对不可为空。
甚至像 TryParse
:
// should the result be nullable with nullable reference types on?
// you have to either lie with ! or else use ? and assume it can always be null
public bool TryParse(string pString, out SomeClass pResult) {
if (<cant parse>) {
pResult = null;
return false;
}
pResult = value;
return true;
}
// works great with JetBrains annotations and nullable reference types off
// now you can know that the result is null or notnull
// based on testing the bool return value
[ContractAnnotation("=> true, pResult: notnull; => false, pResult: null")]
public bool TryParse(string pString, out SomeClass pResult) {
...
}