Powodem ostrzeżenia jest wyjaśnione w rozdziale The issue with T?
o wypróbować Nullable typów referencyjnych . Krótko mówiąc, jeśli używasz T?
, musisz określić, czy typ jest klasą, czy strukturą. Możesz w końcu stworzyć dwa typy dla każdej sprawy.
Głębszy problem polega na tym, że użycie jednego typu do zaimplementowania wyniku i utrzymania zarówno wartości sukcesu, jak i błędu przywraca te same problemy, które wynik miał rozwiązać, i kilka innych.
- Ten sam typ musi przenosić wartość martwą, typ lub błąd, lub przywracać wartości zerowe
- Dopasowanie wzorca do typu nie jest możliwe. Aby to zadziałało, musisz użyć fantazyjnych wyrażeń dopasowujących wzorce pozycyjne.
- Aby uniknąć null będziesz musiał użyć czegoś jak Option / Być może, podobnie jak F # 's Opcje . Nadal będziesz nosić przy sobie Brak, zarówno z powodu wartości, jak i błędu.
Wynik (i oba) w F #
Punktem wyjścia powinien być typ wyniku F # i dyskryminowane związki. W końcu działa to już w .NET.
Typ wyniku w F # to:
type Result<'T,'TError> =
| Ok of ResultValue:'T
| Error of ErrorValue:'TError
Same typy niosą tylko to, czego potrzebują.
DU w F # pozwalają na wyczerpujące dopasowanie wzorca bez konieczności zerowania:
match res2 with
| Ok req -> printfn "My request was valid! Name: %s Email %s" req.Name req.Email
| Error e -> printfn "Error: %s" e
Emulując to w C # 8
Niestety C # 8 nie ma jeszcze DU, są zaplanowane na C # 9. W C # 8 możemy to naśladować, ale tracimy wyczerpujące dopasowanie:
#nullable enable
public interface IResult<TResult,TError>{}
struct Success<TResult,TError> : IResult<TResult,TError>
{
public TResult Value {get;}
public Success(TResult value)=>Value=value;
public void Deconstruct(out TResult value)=>value=Value;
}
struct Error<TResult,TError> : IResult<TResult,TError>
{
public TError ErrorValue {get;}
public Error(TError error)=>ErrorValue=error;
public void Deconstruct(out TError error)=>error=ErrorValue;
}
I użyj tego:
IResult<double,string> Sqrt(IResult<double,string> input)
{
return input switch {
Error<double,string> e => e,
Success<double,string> (var v) when v<0 => new Error<double,string>("Negative"),
Success<double,string> (var v) => new Success<double,string>(Math.Sqrt(v)),
_ => throw new ArgumentException()
};
}
Bez wyczerpującego dopasowania wzorca musimy dodać tę domyślną klauzulę, aby uniknąć ostrzeżeń kompilatora.
Wciąż szukam sposobu na wyczerpujące dopasowanie bez wprowadzania martwych wartości, nawet jeśli są one tylko Opcją.
Opcja / Może
Tworzenie klasy Option przy użyciu wyczerpującego dopasowania jest prostsze:
readonly struct Option<T>
{
public readonly T Value {get;}
public readonly bool IsSome {get;}
public readonly bool IsNone =>!IsSome;
public Option(T value)=>(Value,IsSome)=(value,true);
public void Deconstruct(out T value,out bool isSome)=>(value,isSome)=(Value,IsSome);
}
//Convenience methods, similar to F#'s Option module
static class Option
{
public static Option<T> Some<T>(T value)=>new Option<T>(value);
public static Option<T> None<T>()=>default;
}
Które mogą być używane z:
string cateGory = someValue switch { Option<Category> (_ ,false) =>"No Category",
Option<Category> (var v,true) => v.Name
};