web-dev-qa-db-fra.com

Références non nulles C # 8 et le modèle Try

Il existe un modèle dans les classes C # illustré par Dictionary.TryGetValue et int.TryParse: une méthode qui renvoie un booléen indiquant le succès d'une opération et un paramètre out contenant le résultat réel; si l'opération échoue, le paramètre out est défini sur null.

Supposons que j'utilise des références C # 8 non nullables et que je souhaite écrire une méthode TryParse pour ma propre classe. La signature correcte est la suivante:

public static bool TryParse(string s, out MyClass? result);

Étant donné que le résultat est null dans le faux cas, la variable out doit être marquée comme nullable.

Cependant, le modèle Try est généralement utilisé comme ceci:

if (MyClass.TryParse(s, out var result))
{
  // use result here
}

Étant donné que je n'entre dans la branche que lorsque l'opération réussit, le résultat ne doit jamais être nul dans cette branche. Mais parce que je l'ai marqué comme nullable, je dois maintenant vérifier cela ou utiliser ! pour remplacer:

if (MyClass.TryParse(s, out var result))
{
  Console.WriteLine("Look: {0}", result.SomeProperty); // compiler warning, could be null
  Console.WriteLine("Look: {0}", result!.SomeProperty); // need override
}

C'est moche et un peu peu ergonomique.

En raison du modèle d'utilisation typique, j'ai une autre option: mentir sur le type de résultat:

public static bool TryParse(string s, out MyClass result) // not nullable
{
   // Happy path sets result to non-null and returns true.
   // Error path does this:
   result = null!; // override compiler complaint
   return false;
}

Maintenant, l'utilisation typique devient plus agréable:

if (MyClass.TryParse(s, out var result))
{
  Console.WriteLine("Look: {0}", result.SomeProperty); // no warning
}

mais l'utilisation atypique ne reçoit pas l'avertissement qu'elle devrait:

else
{
  Console.WriteLine("Fail: {0}", result.SomeProperty);
  // Yes, result is in scope here. No, it will never be non-null.
  // Yes, it will throw. No, the compiler won't warn about it.
}

Maintenant, je ne sais pas où aller ici. Y a-t-il une recommandation officielle de l'équipe de langage C #? Existe-t-il un code CoreFX déjà converti en références non nullables qui pourrait me montrer comment procéder? (J'ai cherché des méthodes TryParse. IPAddress est une classe qui en a une, mais elle n'a pas été convertie sur la branche master de corefx.)

Et comment le code générique comme Dictionary.TryGetValue occupe-toi de ça? (Peut-être avec un attribut spécial MaybeNull de ce que j'ai trouvé.) Que se passe-t-il lorsque j'instancie un Dictionary avec un type de valeur non nullable?

24
Sebastian Redl

Le modèle bool/out-var ne fonctionne pas bien avec les types de référence nullables, comme vous le décrivez. Donc, plutôt que de combattre le compilateur, utilisez la fonctionnalité pour simplifier les choses. Ajoutez les fonctionnalités améliorées de correspondance de modèles de C # 8 et vous pouvez traiter une référence annulable comme un "type peut-être de pauvre":

public static MyClass? TryParse(string s) => …

…

if (TryParse(someString) is {} myClass)
{
    // myClass wasn't null, we are good to use it
}

De cette façon, vous évitez de jouer avec les paramètres out et vous n'avez pas à vous battre avec le compilateur pour mélanger null avec des références non nulles.

Et comment le code générique comme Dictionary.TryGetValue occupe-toi de ça?

À ce stade, ce "type peut-être du pauvre" tombe. Le défi auquel vous serez confronté est que lorsque vous utilisez des types de référence nullables (NRT), le compilateur traitera Foo<T> comme non nullable. Mais essayez de le changer en Foo<T?> et il faudra que T contraint à une classe ou à une structure en tant que types de valeur nullable soit une chose très différente du point de vue du CLR. Il existe une variété de solutions à ce problème:

  1. N'activez pas la fonction NRT,
  2. Commencez à utiliser default (avec !) pour les paramètres out même si votre code ne s’inscrit à aucune valeur nulle,
  3. Utilisez un vrai Maybe<T> tapez comme valeur de retour, qui n'est alors jamais null et encapsule que bool et out T dans HasValue et Value propriétés ou certaines de ces propriétés,
  4. Utilisez un tuple:
public static (bool success, T result) TryParse<T>(string s) => …
…

if (TryParse<MyClass>(someString) is (true, var result))
{
    // result is valid here, as success is true
}

Personnellement, je préfère utiliser Maybe<T> mais en le prenant en charge une déconstruction afin qu'il puisse être mis en correspondance avec un motif comme un tuple comme dans 4, ci-dessus.

11
David Arno

Si vous y arrivez un peu tard, comme moi, il s'avère que l'équipe .NET l'a abordé à travers un tas d'attributs de paramètres comme MaybeNullWhen(returnValue: true) dans le System.Diagnostics.CodeAnalysis espace que vous pouvez utiliser pour le motif d'essai.

Par exemple:

comment le code générique comme Dictionary.TryGetValue gère-t-il cela?

bool TryGetValue(TKey key, [MaybeNullWhen(returnValue: false)] out TValue value);

ce qui signifie que vous êtes crié si vous ne vérifiez pas un true

// This is okay:
if(myDictionary.TryGetValue("cheese", out var result))
{
  var more = result * 42;
}

// But this is not:
_ = myDictionary.TryGetValue("cheese", out var result);
var more = result * 42;
// "CS8602: Dereference of a potentially null reference"

Plus de détails:

18
Nick Darvey

Je ne pense pas qu'il y ait de conflit ici.

votre objection à

public static bool TryParse(string s, out MyClass? result);

est

Parce que je n'entre dans la branche que lorsque l'opération réussit, le résultat ne doit jamais être nul dans cette branche.

Cependant, en fait, rien n'empêche l'affectation de null au paramètre out dans les anciennes fonctions TryParse.

par exemple.

MyJsonObject.TryParse("null", out obj) //sets obj to a null MyJsonObject and returns true

L'avertissement donné au programmeur lorsqu'il utilise le paramètre out sans vérification est correct. Vous devriez vérifier!

Il y aura des tas de cas où vous serez obligé de retourner des types nullables où la branche principale du code retourne un type non nullable. L'avertissement est juste là pour vous aider à les rendre explicites. c'est à dire.

MyClass? x = (new List<MyClass>()).FirstOrDefault(i=>i==1);

La manière non nullable de le coder lèvera une exception là où il y aurait eu un null. Que vous analysiez, obteniez ou commenciez

MyClass x = (new List<MyClass>()).First(i=>i==1);
3
Ewan