Bonjour les codeurs du jour ! Vous est-il déjà arrivé de douter, comme quand on vous a parlé pour la première fois de physique quantique ? Eh bien en Swift aussi on peut être incertain, et je parle du point de vue du code. Peut-on vraiment connaître le contenu d’une variable avant de l’avoir lue ? Qu’arrive-t-il si une fonction ne parvient pas à calculer un résultat ? Si aucune donnée n’est lue ? Aujourd’hui on va s’amuser un peu à subodorer des valeurs de types ambigus, alors faites le plein de quantas d’énergie, on s’aventure dans le monde chaotique des variables conditionnelles !
Le chat de Schrödinger se nourrit de votre incompréhension ! Crédit : Leo Amaral
Attends, quoi ?
Comment ça « incertain » ? Alors pour bien comprendre on va devoir utiliser un tableau, par exemple un tableau d’entiers vide :
1 | let tableau: [Int] = [] |
Si plus loin dans notre code nous demandons à lire le premier élément du tableau, Xcode va nous laisser faire tranquillement, en tout cas jusqu’a ce que l’on appuie sur play :
1 2 | let tableau: [Int] = [] print(tableau[0]) |
Une fois le programme lancé (ou dans un playground), Xcode va nous afficher une erreur assez effrayante et difficilement lisible :
error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).. Pour comprendre ce qu’il s’est passé, il faut ouvrir la console de déboggage qui nous indique plus clairement :
1 | Fatal error: Index out of range |
Bon évidemment une erreur avec Fatal comme premier mot ça ne peut pas être quelque chose de bon, mais le plus important c’est que ce type d’erreur aboutie à un plantage immédiat de tout le programme. En l’occurence car nous avons voulu lire une valeur qui n’existait pas. En réalité ce qu’il se passe c’est que l’endroit de la RAM qui devait stocker la valeur ne correspond pas à une donnée cohérente (pas le bon type par exemple).
« Pourquoi est-ce si important ? », vous entend-je déjà demander en buvant votre café . En effet, il suffit de faire attention à ce que ce genre de scénarios ne se produisent pas dans notre code, c’est pour ça que nous sommes des bon programmeurs. Mais ça ne suffit pas, trop souvent les tableaux vont avoir des longueurs variables (et c’est bien là leur utilité) et ce n’est pas le seul problème.
Assez tergiversé
Il existe un mot-clé pour définir ce qui n’a pas de valeur en swift : nil
qui est l’abréviation de null souvent utilisé dans les autres langages. Littéralement : nul dans le sens qui n’existe pas, qui se réduit à rien. On va pouvoir attribuer ce mot-clé en valeur d’une variable pour indiquer que justement la variable n’a pas de valeur. Alors essayons un peu :
1 | let variable:Int = nil |
Nous allons fissa nous faire remballer par notre propre outil de développement (se rebelle-t-il ?) : ‘nil’ cannot initialize specified type ‘Int’, cela pour nous rappeler que « rien » n’est pas un nombre. Mais ce n’est pas tout, il nous propose de corriger lui-même le problème avec une solution curieuse, ajouter un point d’interrogation à la suite du type et ainsi le transformer en type optionnel.
1 | let variable:Int? = nil |
Alors que vient-il de se passer, nous venons en réalité de modifier le type de notre variable qui est simplement le type Int mais pouvant accepter de ne pas avoir de valeur (donc d’avoir nil pour valeur). En ce faisant, nous utilisons un type différent que Int, un entier optionnel.
Si on tente de s’en servir d’une manière « classique » on va se heurter à des problèmes :
1 2 3 4 | let nombre: Int = 28 let variable:Int? = nil print(nombre + variable) |
Value of optional type ‘Int?’ must be unwrapped to a value of type ‘Int’
Comme le type Int est différent de Int?, ce dernier ne convient pas pour être utilisé par exemple pour une addition de deux entiers. Et il s’agit bien ici d’une erreur de compilation, ce n’est pas comme tout à l’heure lors de l’exécution qui nous induisait un crash complet du programme. Cela permet par conséquent d’interdire tout simplement de créer de potentielles erreurs dans le code lorsque des valeurs ne sont pas disponibles. C’est très utilisé dès lors qu’il y a une entrée utilisateur, des données à lire / écrire / échanger, de manière générale lorsque qu’une donnée est générée par le programme et pas écrite par le développeur (oui, vous !). Autant dire que c’est indispensable.
Un p’tit wrap !
Ce qui se cache derrière le terme « unwrapped » (dans l’erreur de compilation) c’est faire en sorte que le type de la variable redevienne non-nul. Il y a principalement deux moyens pour parvenir à cela, le plus simple mais aussi le plus dangereux est d’ajouter un point d’exclamation après le nom de la variable. De cette façon la variable sera considérée comme non-nulle lors de la compilation mais si une valeur nulle est trouvée lors de l’exécution alors nous retrouverons notre erreur fatale du début.
1 2 3 4 | let nombre: Int = 28 let variable:Int? = 5 print(nombre + variable!) |
C’est comme dire à Xcode que l’on sait ce que l’on fait et que la variable aura toujours une valeur lors de sa lecture, cela arrive notamment lorsque une variable n’est lue qu’après l’exécution de la fonction qui la détermine (qui lui donne sa valeur). Néanmoins il vaut mieux éviter d’en mettre trop souvent, même si cela devient vite tentant.
La deuxième option est d’effectuer un simple test conditionnel simple de création d’une variable de type non-nul. Ce que ça veut dire c’est que l’on va essayer de créer une variable non nulle à partir de la valeur dans une condition if :
1 2 3 4 5 6 7 8 9 10 11 | let nombre: Int = 28 let variable:Int? = nil if let variableNonNulle = variable { print(nombre + variableNonNulle) } // --------- OU ---------- if variable != nil { print(nombre + variable!) } |
Dans le deuxième exemple on peut facilement forcer la désoptionnalisation (est-ce un vrai mot ?) de la variable étant donné que l’on teste qu’elle ne soit pas nulle
Les variables gardées
Imaginons que nous soyons dans une fonction assez longue qui ne pourrait s’exécuter que si une variable ou même plusieurs ont pu être désoptionnalisées. On pourrait écrire tout le code qui nécessite cette variable à l’intérieur de la condition mais cela peut vite devenir difficilement lisible. Prenons par exemple une fonction qui nous permettrait de calculer la racine carrée d’un nombre optionnel (parce que pourquoi pas). On a donc bien notre code de calcul mais en premier on doit vérifier que notre variable n’est pas nulle, de préférence en la désoptionnalisant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | func racineCarree(_ nombre: Float?) -> Float { if let entree: Float = nombre { var sortie: Float = 1 var ecart: Float = 1 var intermediaire: Float = 0 while ecart > 0.001 { intermediaire = (sortie + entree / sortie) / 2 if intermediaire > sortie { ecart = intermediaire - sortie } else { ecart = sortie - intermediaire } sortie = intermediaire } return sortie } else { return 0 } } let nombre: Float? = 16.0 let racine = racineCarree(nombre) print("La racine carrée de", nombre, "est", racine); |
Ici, on teste bien que notre variable soit non nulle sinon on retourne tout simplement 0. Mais déjà alors que le contenu de la fonction n’est pas si grand que ça on voit qu’il devient difficile de lire correctement cette condition. Un des moyens de régler ce problème serait d’exécuter la vérification de la non-nullité de la variable en début de fonction, et la quitter si la variable est nulle. Cela va pouvoir se faire grâce aux variables gardées, j’ai fait tout un exemple un peu alambiqué pour vous expliquer leur utilité, mais vous allez voir que leur fonctionnement est extrêmement simple.
Il s’agit en clair d’une condition inversée sur la déclaration d’une variable. Au lieu de créer une variable non-nulle à l’intérieur d’une condition, on va créer une variable gardée qui sera non-nulle à l’extérieur de la condition. Cela permet de rendre le code drastiquement plus lisible, voici comment on les déclare :
1 2 3 4 5 6 | guard let entree = nombre else { return 0 // Code à exécuter si la variable est nulle } // Si la création de la variable non-nulle a pu se faire alors // elle est accessible sur le reste du code |
De cette façon, on peut tester toutes les variables qui pourraient court-circuiter notre code au début de celui-ci et ainsi s’assurer plus de lisibilité que des if à répétitions. Notre fonction d’exemple devient ainsi :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | func racineCarree(_ nombre: Float?) -> Float { guard let entree = nombre else { return 0 } var sortie: Float = 1 var ecart: Float = 1 var intermediaire: Float = 0 while ecart > 0.001 { intermediaire = (sortie + entree / sortie) / 2 if intermediaire > sortie { ecart = intermediaire - sortie } else { ecart = sortie - intermediaire } sortie = intermediaire } return sortie } |
Et on comprend bien mieux ce qu’il se passe. Notons que le else devra obligatoirement contenir une fin de code que ce soit un return ou un throw (que l’on a pas encore vu).
Un autre moyen (eh oui !) de faire cela serait de donner une valeur par défaut à notre variable si celle-ci est nulle. Pour ce faire, rien de plus simple, on déclare une variable non-nulle, qui serait égale à la variable nulle ou bien la valeur par défaut. Ce ou bien s’écrit avec deux points d’interrogations :
1 2 | let entree = nombre ?? 0 // Variable non nulle = variable optionnelle sinon valeur par défaut |
Avoir la classe
Dans une classe, on peut définir le type d’une variable sur son type optionnel dès sa déclaration ce qui nous évitera d’avoir à lui attribuer une valeur dans le init(). Ensuite on peut s’en servir comme de n’importe quelle autre variable dans la classe, tant qu’on n’oublie pas d’y juxtaposer un point d’exclamation lorsque le type se doit d’être non-nul :
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Classe { var nombre: Int? init() { } func ajouter(_ ajout: Int) { print(nombre! + ajout) } } |
C’est bien juste après le nom de la variable que l’on doit positionner le point d’exclamation
Je vois qu’il vous reste encore un petit peu de café, courage on a encore deux choses à voir ! Si on est certain qu’une variable aura toujours une valeur mais qu’en même temps elle puisse être nulle (ne me regardez pas comme ça, ca arrive vraiment) on peut alors directement la déclarer avec le point d’exclamation dans la classe, ce qui indiquera au compilateur d’Xcode de la traiter comme une variable non-nulle :
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Classe { var nombre: Int! init() { } func ajouter(_ ajout: Int) { print(nombre + ajout) } } |
Dernière chose, il y a un raccourci pour accéder aux méthodes d’un type optionnel, n’oublions pas qu’il s’agit d’une classe avant tout. Si par exemple on stocke notre classe dans une variable optionnelle :
1 | var variable: Classe? = Classe() |
Eh bien si on tente d’exécuter la fonction de la classe depuis la variable optionnelle, il nous suffit d’ajouter notre point d’interrogation pour que le compilateur ne demande pas l’exécution de la fonction si la valeur est nulle (ouch). Bon bon bon, si jamais on fait ça :
1 2 | var variable: Classe? = Classe() variable?.ajout(3) |
Alors si la valeur est non-nulle lors de l’exécution la fonction s’exécutera normalement, si la valeur est nulle alors la fonction ne sera pas exécutée et aucun crash n’aura lieu. C’est comme si la ligne de code elle-même était optionnelle…
STOP ! Ca suffit pour aujourd’hui je croit, il est grand temps de faire une pause. Bravo à tous ceux qui ont lu en entier, félicitations à ceux qui ont compris et merci à ceux qui n’ont pas compris et me le diront 😉