C#8空值和结果容器

问题描述 投票:2回答:1

我有一个用于处理错误的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该代码已经在上千行或更多行中使用,因此,如果可以在界面上而不是在用法上进行更改,那就更好了。

c# generics containers nullable c#-8.0
1个回答
1
投票

看起来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更新现有类型

[某些现有功能,例如IsSuccessOutcome只是方便的方法。实际上,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;
    ...
}

DescriptionData属性也可以实现,作为一种止步措施-它们破坏了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);

}
© www.soinside.com 2019 - 2024. All rights reserved.