C # 8 et validité nulle. Comment vivons-nous avec cela

Bonjour collègues! Il est temps de mentionner que nous prévoyons de publier le livre fondamental d' Ian Griffiths sur C # 8:


Entre-temps, sur son blog, l' auteur a publié deux articles connexes dans lesquels il examine les subtilités de nouveaux phénomènes tels que la nullité, la nullité ou la conscience nulle. Nous avons traduit les deux articles sous un même titre et suggérons d'en discuter.

La nouvelle fonctionnalité la plus ambitieuse de C # 8.0 est appelée références nullables .

Le but de cette nouvelle fonctionnalité est de lisser les dommages causés par une chose dangereuse, que l'informaticien Tony Hoar a appelé une fois son " erreur d'un milliard de dollars ". C # a un mot-clénull(dont l'équivalent se trouve dans de nombreuses autres langues), et les racines de ce mot-clé remontent à la langue Algol W, à laquelle Hoar a participé. Dans cette langue ancienne (apparue en 1966), les variables se référant à des instances d'un certain type pouvaient recevoir une signification particulière indiquant qu'à l'heure actuelle cette variable n'est référencée nulle part. Cette opportunité a été très largement empruntée et aujourd'hui, de nombreux experts (dont Hoar lui-même) pensent qu'elle est devenue la plus grande source d'erreurs logicielles coûteuses de tous les temps.

Quel est le problème avec l'hypothèse zéro? Dans un monde où n'importe quel lien peut pointer vers zéro, vous devez en tenir compte chaque fois que des liens sont utilisés dans votre code, sinon vous courez le risque d'être refusé au moment de l'exécution. Parfois, ce n’est pas trop lourd; si vous initialisez une variable avec une expression newau même endroit où vous la déclarez, alors vous savez que cette variable n'est pas égale à zéro. Mais même un exemple aussi simple est lourd de charge cognitive: avant la sortie de C # 8, le compilateur ne pouvait pas vous dire si vous faites quoi que ce soit qui puisse convertir cette valeur en null. Mais, dès que vous commencez à assembler différents fragments de code, il devient beaucoup plus difficile de juger avec certitude de telles choses: quelle est la probabilité que cette propriété que je lis maintenant puisse revenir null? Est-il permis de transmettrenulldans cette méthode? Dans quelles situations puis-je être sûr que la méthode que j'appelle définira cet argument outnon pas sur null, mais sur une valeur différente? De plus, la question ne se limite même pas à penser à vérifier de telles choses; ce que vous devez faire si vous tombez sur zéro n'est pas tout à fait clair.

Avec les types numériques en C #, il n'y a pas un tel problème: si vous écrivez une fonction qui prend des nombres en entrée et renvoie un nombre en conséquence, alors vous n'avez pas à vous demander si les valeurs transmises sont vraiment des nombres, et si quelque chose parmi elles peut être mélangé. Lors de l'appel d'une telle fonction, il n'est pas nécessaire de se demander si elle peut renvoyer quelque chose au lieu d'un nombre. A moins qu'une telle évolution des événements ne vous intéresse en option: dans ce cas, vous pouvez déclarer des paramètres ou des résultats du typeint?, indiquant que dans ce cas particulier, vous voulez vraiment autoriser la transmission ou le retour d'une valeur nulle. Ainsi, pour les types numériques et, dans un sens plus général, les types significatifs, la tolérance zéro a toujours été l'une de ces choses qui se font volontairement, en option.

En ce qui concerne les types de référence, avant C # 8.0, la tolérance de zéro était non seulement définie par défaut, mais elle ne pouvait pas non plus être désactivée.

En fait, pour des raisons de compatibilité descendante, la validité zéro continue de fonctionner par défaut même en C # 8.0, car les nouvelles fonctions de langage dans cette zone restent désactivées jusqu'à ce que vous les demandiez explicitement.

Cependant, dès que vous activez cette nouvelle fonctionnalité - tout change. La façon la plus simple de l'activer est de l'ajouter <Nullablegt;enable</Nullablegt;à l'intérieur de l'élément.<PropertyGroup>dans votre dossier .csproj. (Je note que plus de contrôle en filigrane est également disponible. Si vous en avez vraiment besoin, vous pouvez configurer le comportement pour qu'il soit autorisé nullséparément sur chaque ligne. Mais, lorsque nous avons récemment décidé d'inclure cette fonctionnalité dans tous nos projets, il s'est avéré qu'elle serait activée à l'échelle un projet à la fois est une tâche réalisable.)

Lorsque les liens autorisés dans C # 8.0 sont nullentièrement activés, la situation change: maintenant, par défaut, il est supposé que les liens n'autorisent pas null uniquement si vous ne spécifiez pas l'inverse, exactement comme avec les types significatifs ( même la syntaxe est la même: vous pourriez écrire int?, si vous vouliez vraiment que la valeur entière soit facultative. Maintenant, vous écrivez chaîne ?, si vous voulez dire que vous voulez soit une référence de chaîne, soitnull.)

Il s'agit d'un changement très important et, tout d'abord, en raison de son importance, cette nouvelle fonctionnalité est désactivée par défaut. Microsoft aurait pu concevoir cette fonctionnalité de langage différemment: vous pourriez laisser les liens par défaut nullables et introduire une nouvelle syntaxe qui vous permettrait de spécifier que vous voulez vous assurer qu'elle n'est pas autorisée null. Peut-être que cela abaisserait la barre lors de l'exploration de cette possibilité, mais à long terme, une telle solution serait incorrecte, car dans la pratique, la plupart des liens dans l'énorme masse de code C # ne sont pas conçus pour pointer null.

En supposant que zéro est une exception, pas une règle, c'est pourquoi, lorsque cette nouvelle fonctionnalité de langue est activée, empêcher null devient une nouvelle valeur par défaut. Cela se reflète même dans le nom de la fonction d'origine: «références nullables». Le nom est curieux, étant donné que les liens pourraient nullrenvoyer vers C # 1.0. Mais les développeurs ont choisi de souligner que maintenant l'hypothèse nulle entre dans la catégorie des choses qui doivent être explicitement demandées.

C # 8.0 adoucit le processus d'introduction de liens permissifs null, car il vous permet d'introduire cette fonctionnalité progressivement. Il n'est pas nécessaire de faire un choix oui ou non. Ceci est assez différent de la fonctionnalité async/awaitajoutée en C # 5.0, qui avait tendance à se répandre: en fait, les opérations asynchrones obligent l'appelant à êtreasync, et par conséquent, le code qui appelle cet appelant doit être async, et ainsi de suite, tout en haut de la pile. Heureusement, les types qui le permettent nullsont construits différemment: ils peuvent être implémentés de manière sélective et progressive. Vous pouvez parcourir les fichiers un par un, ou même ligne par ligne si nécessaire.

L'aspect le plus important des types permettantnull(grâce à quoi la transition vers eux est simplifiée), c'est que par défaut ils sont désactivés. Sinon, la plupart des développeurs refuseraient d'utiliser C # 8.0, car une telle transition entraînerait des avertissements dans presque toutes les bases de code. Cependant, pour les mêmes raisons, le seuil d'entrée pour utiliser cette nouvelle fonctionnalité semble assez élevé: si une nouvelle fonctionnalité apporte des changements si spectaculaires qu'elle est désactivée par défaut, alors vous ne voudrez probablement pas jouer avec elle, mais il y a des problèmes associés au passage à celle-ci semblera toujours inutile. Mais ce serait dommage, car la fonctionnalité est très précieuse. Cela aide à trouver des bogues dans le code avant que les utilisateurs ne le fassent pour vous.

Donc, si vous envisagez d'introduire des types qui permettentnull, n'oubliez pas de noter que vous pouvez introduire cette fonctionnalité étape par étape.

Avertissements uniquement

Le niveau de contrôle le plus approximatif sur l'ensemble du projet après une simple activation / désactivation est la possibilité d'activer des avertissements indépendamment des annotations. Par exemple, si j'active complètement l'hypothèse zéro pour Corvus.ContentHandling.Json dans notre référentiel Corvus.ContentHandling , en ajoutant <Nullablegt;enable</Nullablegt;au groupe de propriétés dans le fichier de projet, alors dans son état actuel, 20 avertissements du compilateur apparaîtront immédiatement. Cependant, si je l’utilise à la place, je <Nullablegt;warnings</Nullablegt;n’obtiendrai qu’un seul avertissement.

Mais attendez! Pourquoi moins d'avertissements me seront-ils affichés? En fin de compte, ici, je viens de demander des avertissements. La réponse à cette question n'est pas entièrement évidente: le fait est que certaines variables et expressions peuvent être nullnulles.

Null Neutrality

C # prend en charge deux interprétations de la validité nulle. Premièrement, toute variable d'un type de référence peut être déclarée comme admettant ou ne pas admettre null, et deuxièmement, le compilateur conclura nullchaque fois que possible logiquement si cette variable peut être ou non à un point particulier du code. Cet article ne traite que de la première variété de recevabiliténull, c'est-à-dire sur le type statique d'une variable (en fait, cela ne s'applique pas seulement aux variables et aux paramètres et aux champs qui leur sont proches dans l'esprit; la recevabilité statique et logiquement déductible est nulldéterminée pour chaque expression en C #.) En fait, la recevabilité nulldans son premier sens , celui que nous envisageons est une extension du système de types.

Cependant, il s'avère que si nous nous concentrons uniquement sur l'admissibilité nulle pour un type, la situation ne sera pas aussi cohérente qu'on pourrait le supposer. Ce n'est pas seulement un contraste entre "validité nulle" et "invalide"null". En fait, il y a deux autres possibilités. Il existe une catégorie «inconnue», qui est obligatoire en raison de la disponibilité des génériques; si vous avez un paramètre de type illimité, il ne sera pas possible de trouver quoi que ce soit sur sa validité null: le code utilisant la méthode ou le type généralisé approprié peut y substituer un argument, qu'il autorise ou non null. Vous pouvez ajouter des restrictions, mais souvent ces restrictions ne sont pas souhaitables, car elles réduisent la portée du type ou de la méthode généralisée. Ainsi, pour les variables ou expressions d'un paramètre de type illimité, la T(non) validité de zéro doit être inconnue; peut-être, dans chaque cas, la question de la recevabiliténullcela sera décidé séparément pour eux, mais nous ne savons pas quelle option apparaîtra dans le code générique, car cela dépendra de l'argument type.

Cette dernière catégorie est dite «neutre». Par le principe de «neutralité», tout fonctionnait avant l'avènement de C # 8.0, et cela fonctionnera si vous n'activez pas la possibilité de travailler avec des liens annulables. (Fondamentalement, il s'agit d'un exemple de rétroactivité . Même si l'idée de neutralité nulle a été introduite pour la première fois dans C # 8.0 en tant qu'état naturel de code avant d'activer la validité nulle pour les références, les concepteurs C # ont insisté sur le fait que cette propriété n'était jamais vraiment étrangère à C #.)

Peut-être que vous n'avez pas à expliquer ce que signifie la «neutralité» dans ce cas, car c'est dans cette veine que C # a toujours fonctionné, alors vous comprenez vous-même tout ... même si, peut-être, c'est un peu malhonnête. Alors écoutez: dans un monde où l'on connaît l'admissibilité null, la caractéristique la plus importante des nullexpressions -eutrales est qu'elles ne provoquent pas d'avertissement concernant l'acceptabilité nulle. Vous pouvez affecter une expression neutre neutre en tant que nullvariable autorisée , mais non autorisée. Null-les variables neutres (ainsi que les propriétés, les champs, etc.), vous pouvez attribuer des expressions que le compilateur considérait comme «possibles null» ou «non null».

C’est pourquoi, si vous activez simplement les avertissements, il n’y a pas tellement de nouveaux avertissements. Tout le code reste dans le contexte des annotations de validité désactivées null, donc toutes les variables, paramètres, champs et propriétés seront nullneutres, ce qui signifie que vous ne recevrez aucun avertissement si vous essayez de les utiliser en conjonction avec des entités qui prennent en compte null.

Pourquoi alors puis-je recevoir des avertissements? Une raison courante est due à une tentative de se faire des amis de manière inacceptable deux morceaux de code qui prennent en compte null. Par exemple, supposons que j'ai une bibliothèque où les liens permissifs sont entièrement inclus null, et cette bibliothèque a la classe profondément artificielle suivante:

public static class NullableAwareClass
	{
	    public static string? GetNullable() => DateTime.Now.Hour > 12 ? null : "morning";
	

	    public static int RequireNonNull(string s) => s.Length;
	}

De plus, dans un autre projet, je peux écrire ce code dans le contexte où les avertissements de validité nulle sont activés, mais les annotations correspondantes sont désactivées:

static int UseString(string x) => NullableAwareClass.RequireNonNull(x);

Comme les annotations sur la validité nulle sont désactivées, le paramètre xici est nullneutre. Cela signifie que le compilateur ne peut pas déterminer si ce code est vrai ou non. Si le compilateur a émis des avertissements dans les cas où nulldes expressions neutres sont mélangées à celles qui prennent en compte null, une proportion importante de ces avertissements peut être considérée comme douteuse - par conséquent, aucun avertissement n'est émis.

Avec ce wrapper, j'ai en fait caché le fait que le code prend en compte la validité null. Cela signifie que maintenant je peux écrire comme ceci:

	int x = UseString(NullableAwareClass.GetNullable());

Le compilateur sait ce qu'il GetNullablepeut retourner null, mais comme j'ai appelé la méthode avec un paramètre neutre neutre, le programme ne sait pas si c'est bien ou pas. En utilisant le nullwrapper -neutral, j'ai désarmé le compilateur, qui ne voit plus de problème ici. Cependant, si je combinais ces deux méthodes directement, tout serait différent:

int y = NullableAwareClass.RequireNonNull(NullableAwareClass.GetNullable());

Ici, je passe le résultat GetNullabledirectement à RequireNonNull. Si j'essayais de le faire dans un contexte où les hypothèses nulles sont activées, le compilateur générerait un avertissement, que j'active ou désactive le contexte des annotations correspondantes. Dans ce cas particulier, le contexte des annotations n'a pas d'importance, car il n'y a pas de déclarations avec un type de référence. Si vous activez les avertissements concernant l'hypothèse de null, mais désactivez les annotations correspondantes, toutes les déclarations deviendront nullneutres, ce qui, cependant, ne signifie pas que toutes les expressions le deviennent. Donc, nous savons que le résultat GetNullableest nul. Par conséquent, nous recevons un avertissement.

En résumé: puisque toutes les déclarations dans le contexte d'annotations désactivées qui le permettent nullsontnull-neutre, nous n'obtiendrons pas beaucoup d'avertissements, car la plupart des expressions seront null-neutres. Mais le compilateur sera toujours en mesure de détecter les erreurs liées à l'hypothèse nulldans les cas où les expressions ne passent pas par un intermédiaire neutre neutre. De plus, le plus grand avantage dans ce cas sera de détecter les erreurs associées aux tentatives de déréférencement de valeurs nulles potentielles en utilisant ., par exemple:

int z = NullableAwareClass.GetNullable().Length;

Si votre code est bien conçu, il ne devrait pas y avoir un grand nombre d'erreurs de ce type.

Annotation progressive de l'ensemble du projet

Après avoir fait la première étape - il suffit d'activer les avertissements, puis vous pouvez procéder à l'activation progressive des annotations, fichier par fichier. Il est pratique de les inclure immédiatement dans l'ensemble du projet, de voir dans quels fichiers les avertissements apparaissent - puis de sélectionner un fichier dans lequel il y a relativement peu d'avertissements. Encore une fois, désactivez-les au niveau de l'ensemble du projet et écrivez en haut du fichier que vous avez sélectionné #nullable enable. Cela activera entièrement l'hypothèse null(à la fois pour les avertissements et pour les annotations) dans le fichier entier (sauf si vous les désactivez à nouveau à l'aide d'une autre directive#nullable) Ensuite, vous pouvez parcourir l'intégralité du fichier et vous assurer que toutes les entités susceptibles d'être nulles sont annotées comme autorisant null(c'est-à-dire, ajouter ?), puis traiter les avertissements dans ce fichier, le cas échéant.

Il peut s'avérer que l'ajout de toutes les annotations nécessaires est tout ce qui est nécessaire pour éliminer tous les avertissements. L'inverse est également possible: vous remarquerez peut-être que lorsque vous annotez soigneusement un fichier sur la validiténull, d'autres avertissements sont apparus dans d'autres fichiers l'utilisant. En règle générale, il n'y a pas beaucoup d'avertissements de ce type et vous avez le temps de les corriger rapidement. Mais, si pour une raison quelconque, après cette étape, vous vous noyez dans les avertissements, vous avez encore quelques solutions. Tout d'abord, vous pouvez simplement annuler la sélection, quitter ce fichier et en reprendre un autre. Deuxièmement, vous pouvez désactiver de manière sélective les annotations pour les membres qui, selon vous, posent le plus de problèmes. ( #nullableVous pouvez utiliser la directive autant de fois que vous le souhaitez, afin de pouvoir contrôler les paramètres de validité nulle, même ligne par ligne, si vous le souhaitez.) Peut-être que si vous revenez à ce fichier plus tard lorsque vous activez déjà la validité nulle dans la plupart du projet, vous verrez moins avertissements que la première fois.

Il y a des moments où les problèmes ne peuvent pas être résolus d'une manière aussi simple. Ainsi, dans certains scénarios liés à la sérialisation (par exemple, lors de l'utilisation de Json.NET ou Entity Framework), le travail peut être plus difficile. Je pense que ce problème mérite un article séparé.

Les liens avec l'hypothèse nullaméliorent l'expressivité de votre code et augmentent les chances que le compilateur détecte vos erreurs avant que les utilisateurs ne les rencontrent. Par conséquent, il est préférable d'inclure cette fonctionnalité si possible. Et, si vous l'incluez de manière sélective, les avantages de celle-ci commenceront à se sentir plus rapidement.

All Articles