El motivo de la advertencia se explica en la sección The issue with T?
de Prueba de tipos de referencia anulables . Para resumir, si lo usa T?
, debe especificar si el tipo es una clase o estructura. Puede terminar creando dos tipos para cada caso.
El problema más profundo es que usar un tipo para implementar Result y mantener los valores Success y Error devuelve los mismos problemas que se suponía que Result solucionaría, y algunos más.
- El mismo tipo tiene que llevar un valor muerto, ya sea el tipo o el error, o devolver nulos
- La coincidencia de patrones en el tipo no es posible. Tendría que usar algunas expresiones de coincidencia de patrones posicionales elegantes para que esto funcione.
- Para evitar nulos, tendrá que usar algo como Opción / Quizás, similar a las Opciones de F # . Sin embargo, todavía llevaría un None, ya sea por el valor o por error.
Resultado (y cualquiera) en F #
El punto de partida debe ser el tipo de resultado de F # y las uniones discriminadas. Después de todo, esto ya funciona en .NET.
Un tipo de resultado en F # es:
type Result<'T,'TError> =
| Ok of ResultValue:'T
| Error of ErrorValue:'TError
Los tipos en sí solo llevan lo que necesitan.
Los DU en F # permiten una concordancia exhaustiva de patrones sin requerir nulos:
match res2 with
| Ok req -> printfn "My request was valid! Name: %s Email %s" req.Name req.Email
| Error e -> printfn "Error: %s" e
Emulando esto en C # 8
Desafortunadamente, C # 8 aún no tiene DU, están programados para C # 9. En C # 8 podemos emular esto, pero perdemos una coincidencia exhaustiva:
#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;
}
Y úsalo:
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()
};
}
Sin una exhaustiva coincidencia de patrones, tenemos que agregar esa cláusula predeterminada para evitar advertencias del compilador.
Todavía estoy buscando una manera de obtener una concordancia exhaustiva sin introducir valores muertos, incluso si son solo una Opción.
Opción / Quizás
Crear una clase de Opción por la forma en que usa una concordancia exhaustiva es más simple:
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;
}
Que se puede usar con:
string cateGory = someValue switch { Option<Category> (_ ,false) =>"No Category",
Option<Category> (var v,true) => v.Name
};