Les C # ne peuvent pas rendre le type `notnull` nul

9

J'essaie de créer un type similaire à celui de Rust Resultou Haskell Eitheret je suis arrivé jusqu'ici:

public struct Result<TResult, TError>
    where TResult : notnull
    where TError : notnull
{
    private readonly OneOf<TResult, TError> Value;
    public Result(TResult result) => Value = result;
    public Result(TError error) => Value = error;

    public static implicit operator Result<TResult, TError>(TResult result)
        => new Result<TResult, TError>(result);

    public static implicit operator Result<TResult, TError>(TError error)
        => new Result<TResult, TError>(error);

    public void Deconstruct(out TResult? result, out TError? error)
    {
        result = (Value.IsT0) ? Value.AsT0 : (TResult?)null;
        error = (Value.IsT1) ? Value.AsT1 : (TError?)null;
    }  
}

Étant donné que les deux types de paramètres sont restreints notnull, pourquoi se plaint-il (partout où il y a un paramètre de type avec le ?signe nullable après) que:

Un paramètre de type nullable doit être connu pour être un type de valeur ou un type de référence non nullable. Pensez à ajouter une contrainte «classe», «struct» ou type.

?


J'utilise C # 8 sur .NET Core 3 avec les types de référence nullables activés.

Chaussure Diamente
la source
Vous devriez plutôt partir du type de résultat F # et des unions discriminées. Vous pouvez facilement réaliser quelque chose de similaire en C # 8, sans transporter de valeur morte, mais vous n'aurez pas de correspondance exhaustive. Essayer de mettre les deux types dans la même structure se heurtera à un problème après l'autre et ramènera les problèmes mêmes que le résultat était censé résoudre
Panagiotis Kanavos

Réponses:

12

Fondamentalement, vous demandez quelque chose qui ne peut pas être représenté en IL. Les types de valeur Nullable et les types de référence Nullable sont des bêtes très différentes, et bien qu'ils semblent similaires dans le code source, l'IL est très différent. La version nullable d'un type de valeur Test un type différent ( Nullable<T>) tandis que la version nullable d'un type de référence Test du même type, avec des attributs indiquant au compilateur à quoi s'attendre.

Considérez cet exemple plus simple:

public class Foo<T> where T : notnull
{
    public T? GetNullValue() => 
}

C'est invalide pour la même raison.

Si nous contraignons Tà être une structure, alors l'IL généré pour la GetNullValueméthode aurait un type de retour de Nullable<T>.

Si nous contraignons Tà être un type de référence non nullable, alors l'IL généré pour la GetNullValueméthode aurait un type de retour de T, mais avec un attribut pour l'aspect nullabilité.

Le compilateur ne peut pas générer IL pour une méthode qui a un type de retour à la fois Tet Nullable<T>en même temps.

C'est essentiellement le résultat du fait que les types de référence nullables ne sont pas du tout un concept CLR - c'est juste la magie du compilateur pour vous aider à exprimer vos intentions dans le code et faire en sorte que le compilateur effectue une vérification au moment de la compilation.

Le message d'erreur n'est cependant pas aussi clair qu'il pourrait l'être. Test connu pour être "un type de valeur ou un type de référence non nullable". Un message d'erreur plus précis (mais beaucoup plus verbeux) serait:

Un paramètre de type nullable doit être connu pour être un type de valeur, ou connu pour être un type de référence non nullable. Pensez à ajouter une contrainte «classe», «struct» ou type.

À ce stade, l'erreur s'appliquerait raisonnablement à notre code - le paramètre type n'est pas "connu pour être un type de valeur" et il n'est pas "connu pour être un type de référence non nullable". Il est connu pour être l'un des deux, mais le compilateur doit savoir lequel .

Jon Skeet
la source
Il y a aussi la magie de l'exécution - vous ne pouvez pas faire une nullable nullable, même s'il n'y a aucun moyen de représenter cette restriction dans IL. Nullable<T>est un type spécial que vous ne pouvez pas fabriquer vous-même. Et puis il y a le point bonus de la façon dont la boxe se fait avec des types nulles.
Luaan
1
@Luaan: Il existe une magie d'exécution pour les types de valeur nullable, mais pas pour les types de référence nullable.
Jon Skeet
6

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
                                   };
Panagiotis Kanavos
la source