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

23

Il existe un modèle dans les classes C # illustré par Dictionary.TryGetValueet int.TryParse: une méthode qui retourne 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 nul 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? (Je suis allé chercher des TryParseméthodes. IPAddressEst une classe qui en a une, mais elle n'a pas été convertie sur la branche principale de corefx.)

Et comment le code générique Dictionary.TryGetValuetraite-t-il cela? (Peut-être avec un MaybeNullattribut spécial de ce que j'ai trouvé.) Que se passe-t-il lorsque j'instancie un Dictionaryavec un type de valeur non nullable?

Sebastian Redl
la source
Je ne l'ai pas essayé (c'est pourquoi je n'écris pas cela comme une réponse), mais avec la nouvelle fonction de correspondance de modèle des instructions switch, je suppose qu'une option consiste à renvoyer simplement la référence nullable (pas de modèle Try, retour MyClass?), et faites un interrupteur dessus avec un case MyClass myObj:et (un optionall) case null:.
Filip Milovanović
+1 J'aime vraiment cette question, et quand j'ai dû travailler avec cela, j'ai toujours utilisé une vérification null supplémentaire au lieu de la substitution - ce qui m'a toujours semblé un peu inutile et inélégant, mais ce n'était jamais dans le code critique de performance donc je laisse tomber. Ce serait bien de voir s'il existe une façon plus propre de le gérer!
BrianH

Réponses:

10

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 nullable 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 outparamètres et vous n'avez pas à vous battre avec le compilateur pour le mélange nullavec des références non nullables.

Et comment le code générique Dictionary.TryGetValuetraite-t-il cela?

À 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 sera traité Foo<T>comme non nullable. Mais essayez de le changer Foo<T?>et il voudra que Tcontraint à 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 !) les outparamètres même si votre code ne s’inscrit à aucune valeur nulle,
  3. Utilisez un vrai Maybe<T>type que la valeur de retour, qui est alors jamais nullet enveloppe qui boolet out Tdans HasValueet Valuepropriétés ou quelque,
  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 suis favorable à l'utilisation, Maybe<T>mais à ce qu'il prenne en charge une déconstruction afin qu'il puisse être associé à un motif comme un tuple comme en 4, ci-dessus.

David Arno
la source
2
TryParse(someString) is {} myClass- Cette syntaxe prendra un certain temps pour s'y habituer, mais j'aime l'idée.
Sebastian Redl
TryParse(someString) is var myClassme semble plus facile.
Olivier Jacot-Descombes
2
@ OlivierJacot-Descombes ça peut paraître plus simple ... mais ça ne marchera pas. Le modèle de voiture correspond toujours, il x is var ysera donc toujours vrai, qu'il xsoit nul ou non.
David Arno
15

Si vous arrivez à cela 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 l' System.Diagnostics.CodeAnalysisespace que vous pouvez utiliser pour le modèle 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 obtenez crié si vous ne vérifiez pas 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:

Nick Darvey
la source
3

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

votre objection à

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

est

É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.

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);
Ewan
la source
Je ne pense pas que l' FirstOrDefaulton puisse comparer, car la nullité de sa valeur de retour est le signal principal. Dans les TryParseméthodes, le paramètre out qui n'est pas nul si la valeur de retour est vraie fait partie du contrat de méthode.
Sebastian Redl
cela ne fait pas partie du contrat. la seule chose qui est assurée est que quelque chose est assigné au paramètre out
Ewan
C'est le comportement que j'attends d'une TryParseméthode. Si IPAddress.TryParsejamais retourné vrai mais n'assignait pas non nul à son paramètre out, je le signalerais comme un bug.
Sebastian Redl
Votre attente est compréhensible mais n'est pas appliquée par le compilateur. Donc, bien sûr, la spécification pour IpAddress pourrait dire ne renvoie jamais true et null, mais mon exemple JsonObject montre un cas où le retour de null pourrait être correct
Ewan
"Votre attente est compréhensible mais n'est pas appliquée par le compilateur." - Je sais, mais ma question est de savoir comment écrire au mieux mon code pour exprimer le contrat que j'ai en tête, pas quel est le contrat d'un autre code.
Sebastian Redl