Je pense que le résumé succinct des raisons pour lesquelles la nullité est indésirable est que les États dénués de sens ne devraient pas être représentables .
Supposons que je modélise une porte. Il peut être dans l'un des trois états: ouvert, fermé mais déverrouillé et fermé et verrouillé. Maintenant, je pourrais le modéliser selon les
class Door
private bool isShut
private bool isLocked
et il est clair comment mapper mes trois états dans ces deux variables booléennes. Mais cela laisse une quatrième état indésirable disponible: isShut==false && isLocked==true. Étant donné que les types que j'ai sélectionnés comme représentation admettent cet état, je dois déployer des efforts mentaux pour m'assurer que la classe n'entre jamais dans cet état (peut-être en codant explicitement un invariant). En revanche, si j'utilisais un langage avec des types de données algébriques ou des énumérations vérifiées qui me permettaient de définir
type DoorState =
| Open | ShutAndUnlocked | ShutAndLocked
alors je pourrais définir
class Door
private DoorState state
et il n'y a plus de soucis. Le système de types s'assurera qu'il n'y a que trois états possibles pour une instance de class Door. C'est ce à quoi les systèmes de types sont bons - excluant explicitement toute une classe d'erreurs au moment de la compilation.
Le problème avec nullest que chaque type de référence obtient cet état supplémentaire dans son espace qui est généralement indésirable. Une stringvariable pourrait être n'importe quelle séquence de caractères, ou ce pourrait être cette nullvaleur supplémentaire folle qui ne correspond pas à mon domaine problématique. Un Triangleobjet a trois Points, qui ont eux-mêmes Xet des Yvaleurs, mais malheureusement le Points ou le Trianglelui-même peuvent être cette valeur nulle folle qui n'a pas de sens pour le domaine graphique dans lequel je travaille. Etc.
Lorsque vous avez l'intention de modéliser une valeur possiblement inexistante, vous devez y adhérer explicitement. Si la façon dont j'ai l'intention de modéliser les gens est que chacun Persona un FirstNameet un LastName, mais seulement certaines personnes ont un MiddleNames, alors je voudrais dire quelque chose comme
class Person
private string FirstName
private Option<string> MiddleName
private string LastName
où stringici est supposé être un type non nul. Ensuite, il n'y a pas d'invariants délicats à établir et pas de NullReferenceExceptions inattendus lorsque vous essayez de calculer la longueur du nom d'une personne. Le système de type garantit que tout code traitant des MiddleNamecomptes en rend compte None, tandis que tout code traitant des FirstNamepeut supposer en toute sécurité qu'il y a une valeur.
Ainsi, par exemple, en utilisant le type ci-dessus, nous pourrions créer cette fonction idiote:
let TotalNumCharsInPersonsName(p:Person) =
let middleLen = match p.MiddleName with
| None -> 0
| Some(s) -> s.Length
p.FirstName.Length + middleLen + p.LastName.Length
sans soucis. En revanche, dans une langue avec des références nullables pour des types comme chaîne, puis en supposant
class Person
private string FirstName
private string MiddleName
private string LastName
vous finissez par créer des trucs comme
let TotalNumCharsInPersonsName(p:Person) =
p.FirstName.Length + p.MiddleName.Length + p.LastName.Length
qui explose si l'objet Personne entrant n'a pas l'invariant de tout étant non nul, ou
let TotalNumCharsInPersonsName(p:Person) =
(if p.FirstName=null then 0 else p.FirstName.Length)
+ (if p.MiddleName=null then 0 else p.MiddleName.Length)
+ (if p.LastName=null then 0 else p.LastName.Length)
ou peut-être
let TotalNumCharsInPersonsName(p:Person) =
p.FirstName.Length
+ (if p.MiddleName=null then 0 else p.MiddleName.Length)
+ p.LastName.Length
en supposant que le ppremier / dernier est là mais que le milieu peut être nul, ou peut-être que vous faites des vérifications qui lèvent différents types d'exceptions, ou qui sait quoi. Tous ces choix d'implémentation fous et toutes les choses à penser surgissent parce qu'il y a cette stupide valeur représentable dont vous ne voulez pas ou dont vous n'avez pas besoin.
Null ajoute généralement une complexité inutile. La complexité est l'ennemi de tous les logiciels, et vous devez vous efforcer de réduire la complexité chaque fois que cela est raisonnable.
(Notez bien que même ces exemples simples sont plus complexes. Même si a FirstNamene peut pas l'être null, a stringpeut représenter ""(la chaîne vide), qui n'est probablement pas non plus un nom de personne que nous avons l'intention de modéliser. En tant que tel, même avec des non chaînes nullables, il se peut que nous représentions des valeurs sans signification. Encore une fois, vous pouvez choisir de combattre cela soit via des invariants et du code conditionnel lors de l'exécution, soit en utilisant le système de type (par exemple pour avoir un NonEmptyStringtype). ce dernier est peut-être mal avisé (les "bons" types sont souvent "fermés" sur un ensemble d'opérations courantes, et par exemple NonEmptyStringne sont pas fermés sur.SubString(0,0)), mais cela démontre plus de points dans l'espace de conception. En fin de compte, dans tout système de type donné, il y a une certaine complexité dont il sera très bon de se débarrasser, et une autre complexité qui est intrinsèquement plus difficile à éliminer. La clé de cette rubrique est que dans presque tous les systèmes de types, le passage de "références annulables par défaut" à "références non nullables par défaut" est presque toujours un simple changement qui rend le système de type beaucoup plus efficace pour lutter contre la complexité et excluant certains types d'erreurs et d'états dénués de sens. Il est donc assez fou que tant de langues continuent de répéter cette erreur encore et encore.)