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 null
est que chaque type de référence obtient cet état supplémentaire dans son espace qui est généralement indésirable. Une string
variable pourrait être n'importe quelle séquence de caractères, ou ce pourrait être cette null
valeur supplémentaire folle qui ne correspond pas à mon domaine problématique. Un Triangle
objet a trois Point
s, qui ont eux-mêmes X
et des Y
valeurs, mais malheureusement le Point
s ou le Triangle
lui-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 Person
a un FirstName
et un LastName
, mais seulement certaines personnes ont un MiddleName
s, alors je voudrais dire quelque chose comme
class Person
private string FirstName
private Option<string> MiddleName
private string LastName
où string
ici est supposé être un type non nul. Ensuite, il n'y a pas d'invariants délicats à établir et pas de NullReferenceException
s inattendus lorsque vous essayez de calculer la longueur du nom d'une personne. Le système de type garantit que tout code traitant des MiddleName
comptes en rend compte None
, tandis que tout code traitant des FirstName
peut 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 p
premier / 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 FirstName
ne peut pas l'être null
, a string
peut 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 NonEmptyString
type). ce dernier est peut-être mal avisé (les "bons" types sont souvent "fermés" sur un ensemble d'opérations courantes, et par exemple NonEmptyString
ne 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.)