La raison de l'avertissement est expliqué dans la section The issue with T?
de Expérimentaux Nullable Types de référence . Pour faire court, si vous utilisez, T?
vous devez spécifier si le type est une classe ou une structure. Vous pouvez finir par créer deux types pour chaque cas.
Le problème le plus profond est que l'utilisation d'un type pour implémenter Result et conserver les valeurs Success et Error ramène les mêmes problèmes que Result devait résoudre, et quelques autres.
- Le même type doit transporter une valeur morte, que ce soit le type ou l'erreur, ou ramener des valeurs nulles
- La correspondance des motifs sur le type n'est pas possible. Vous devez utiliser des expressions de correspondance de motifs positionnels sophistiqués pour que cela fonctionne.
- Pour éviter les valeurs nulles, vous devrez utiliser quelque chose comme Option / Peut-être, similaire aux options de F # . Cependant, vous porteriez toujours un None, que ce soit pour la valeur ou l'erreur.
Résultat (et l'un ou l'autre) en F #
Le point de départ doit être le type de résultat de F # et les unions discriminées. Après tout, cela fonctionne déjà sur .NET.
Un type de résultat en F # est:
type Result<'T,'TError> =
| Ok of ResultValue:'T
| Error of ErrorValue:'TError
Les types eux-mêmes ne portent que ce dont ils ont besoin.
Les DU en F # permettent une correspondance exhaustive des motifs sans nécessiter de valeurs nulles:
match res2 with
| Ok req -> printfn "My request was valid! Name: %s Email %s" req.Name req.Email
| Error e -> printfn "Error: %s" e
Émuler cela en C # 8
Malheureusement, C # 8 n'a pas encore de DU, ils sont prévus pour C # 9. En C # 8, nous pouvons émuler cela, mais nous perdons la correspondance exhaustive:
#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;
}
Et utilisez-le:
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()
};
}
Sans correspondance de modèle exhaustive, nous devons ajouter cette clause par défaut pour éviter les avertissements du compilateur.
Je cherche toujours un moyen d'obtenir une correspondance exhaustive sans introduire de valeurs mortes, même si elles ne sont qu'une option.
Option / Peut-être
La création d'une classe Option par la manière qui utilise une correspondance exhaustive est plus 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;
}
Qui peut être utilisé avec:
string cateGory = someValue switch { Option<Category> (_ ,false) =>"No Category",
Option<Category> (var v,true) => v.Name
};