1. Avant-propos d'Andrei Alexandrescu

Ceux qui, parmi nous, connaissent Ali peuvent remarquer que son livre sur le D est imprégné de sa personnalité : direct, patient, et sympathique sans donner dans la flatterie.

Il y a un but dans chaque phrase et, avec chacune d'elles, un pas en avant ; ni trop rapide, ni trop lent. « Notez que opApply() elle-même est implémentée avec une boucle foreach. Ainsi, la boucle foreach dans main() finit par faire un usage indirect de foreach sur le membre points »[1]. Et ainsi de suite, avec juste le nombre de mots nécessaires. Et dans le bon ordre, aussi ; Ali fait un travail remarquable pour présenter les concepts du langage — qui, surtout pour un débutant, arrivent « en parallèle » de manière écrasante — de façon séquentielle.

Mais il y a aussi quelque chose que j'aime beaucoup avec Programmer en D: il s'agit d'un bon livre pour apprendre la programmation en général. Vous voyez, un bon livre introductif sur Haskell enseigne la programmation fonctionnelle en cours de route, de façon implicite ; un sur le C vient avec des notions de programmation système ; un sur le Python avec du scripting, et ainsi de suite. Qu'enseignerait, alors, un bon texte introductif au D ? Idéalement, la Programmation avec un P majuscule.

D encourage une attitude du style « utiliser le bon outil pour la tâche à réaliser » et permet à son utilisateur de taper dans une grande étendue de techniques de programmation, sans mettre trop de particularités en travers du chemin. La manière la plus amusante d'appréhender la programmation en D est de le faire avec un esprit ouvert, parce que pour chaque façon de concevoir qui devient non naturelle, il y a la possibilité de la retravailler pour obtenir la bonne architecture en choisissant une implémentation, une approche, un paradigme différents. Pour choisir au mieux ce qui correspond le mieux, l'ingénieur doit connaître l'étendue des possibles — et Programmer en D est un excellent moyen de munir quelqu'un de cette connaissance. L'assimiler n'aide pas seulement à écrire un bon code en D, mais écrire un bon code tout court.

Il y a de bons conseils stratégiques, aussi, qui agrémentent l'enseignement de la programmation et des concepts du langage. Enseignement intemporel sur éviter la duplication de code, choisir de bons noms, rechercher la bonne décomposition des étapes et plus — tout est là, des bidouilles rapides et grossières progressivement transformées en solutions robustes, exactement comme elles devraient l'être dans la pratique. Au lieu de tomber dans le piège du « fait rapidement », Programmer en D se concentre sur le « fait proprement », au plus grand bénéfice de son lecteur.

J'ai longtemps pensé que le D est un bon premier langage de programmation à apprendre. Il expose ses utilisateurs à une variété de concepts — systèmes, fonctionnel, orienté objet, généricité, génératif — honnêtement, simplement et sans prétention. Tout comme le livre d'Ali, qui, pour moi, représente une excellente occasion de le faire.

Andrei Alexandrescu
San Francisco, Mai 2015

2. Préface

D est un langage de programmation multi-paradigme qui combine un large panel de concepts de programmation, du plus bas au plus haut niveau. Il met l'accent sur la sécurité des accès à la mémoire, la correction des programmes et le pragmatisme.

Le but principal de ce livre est d'enseigner le D à des lecteurs qui sont débutants en programmation. Même si une expérience dans d'autres langages de programmation peut aider, ce livre commence de zéro.

Afin que ce livre soit utile, vous aurez besoin d'un environnement pour écrire, compiler et lancer vos programmes D. Cet environnement de développement doit inclure au moins un compilateur D et un éditeur de texte. Nous allons apprendre à installer un compilateur et à compiler un programme dans le chapitre suivant.

Chaque chapitre est basé sur le contenu des précédents, introduisant aussi peu de nouveaux concepts que possible. Je recommande que vous lisiez le livre de façon linéaire, sans sauter de chapitre. Même si ce livre a été écrit avec les débutants à l'esprit, il couvre presque toutes les fonctionnalités du D. Les programmeurs les plus expérimentés peuvent utiliser le libre comme un référence du langage D en utilisant l'index.

Certains chapitres incluent des exercices et leurs solutions pour que vous puissiez écrire des petits programmes et comparer vos méthodes aux miennes.

La programmation est une occupation satisfaisante qui implique la découverte et l'apprentissage continus de nouveaux outils, nouvelles techniques et nouveaux concepts. Je suis sûr que vous aimerez programmer en D autant que moi. Apprendre à programmer est plus facile et plus amusant quand on le partage avec les autres. Profitez du forum D.lean pour suivre les discussions et pour poser et répondre à des questions.

Ce livre est également disponible dans d'autres langues comme le turc et l'anglais

2-1. Remerciements de l'auteur original (Ali Çehreli)

Je suis redevable envers les personnes suivantes qui ont joué un rôle important durant l'évolution de ce livre :

Mert Ataol, Zafer Çelenk, Salih Dinçer, Can Alpay Çiftçi, Faruk Erdem Öncel, Muhammet Aydın (aka Mengü Kağan), Ergin Güney, Jordi Sayol, David Herberth, Andre Tampubolon, Gour-Gadadhara Dasa, Raphaël Jakse, Andrej Mitrović, Johannes Pfau, Jerome Sniatecki, Jason Adams, Ali H. Çalışkan, Paul Jurczak, Brian Rogoff, Михаил Страшун (Mihails Strasuns), Joseph Rushton Wakeling, Tove, Hugo Florentino, Satya Pothamsetti, Luís Marques, Christoph Wendler, Daniel Nielsen, Ketmar Dark, Pavel Lukin, Jonas Fiala, Norman Hardy, Rich Morin, Douglas Foster, Paul Robinson, Sean Garratt, Stéphane Goujet, Shammah Chancellor, Steven Schveighoffer, Robbin Carlson, Bubnenkov Dmitry Ivanovich, Bastiaan Veelo, Stéphane Goujet, Olivier Pisano et Dave Yost.

Remerciements spécifiques à Luís Marques qui, avec son travail conséquent, a amélioré chaque chapitre de ce livre. Si vous trouvez un quel passage de ce livre utile, c'est certainement grâce à ses modifications assidues.

Merci à Luís Marques, Steven Schveighoffer, Andrej Mitrović, Robbin Carlson, and Ergin Güney pour leurs suggestions qui ont élevé mon englais au rang d'anglais.

Je suis reconnaissant envers la communauté D entière pour avoir garder mon enthousiasme et ma motivation à un niveau élevé. D a une communauté extraordinaire d'individus infatigables tels que bearophile et Kenji Hara.

Ebru, Damla et Derin, merci d'avoir été si patients et encourageants pendant que j'étais perdu à écrire ces chapitres.

Ali Çehreli

Mountain View, Août 2015

2-2. Note du traducteur (Raphaël Jakse)

http://ddili.org/ders/d.en/ est sous la licence Creative Commons BY-NC-SA, j'ai choisi de conserver cette licence pour cette traduction.

Merci à Munrek pour permettre la publication de cette traduction sur le Web grâce à dlang-fr et merci à Ali pour son œuvre de qualité et l'accueil qu'il a réservé à cette traduction.

Merci à sclytrac (forum D) pour ses corrections.

Merci à Hassan Azi pour sa traduction du chapitre sur les fonctions membres.

Merci à Olivier Pisano pour une traduction d'excellente qualité de pas mal de chapitres.

Merci à Stéphane Goujet pour ses relectures attentives et pointilleuses qui ont permise d'élever la qualité et la cohérence de la traduction.

3. Introduction

Cet ouvrage a pour but d'enseigner le langage D aux lecteurs novices en programmation. Même si de l'expérience dans d'autres langages de programmation faciliterait les choses, ce livre commence de zéro. Si vous êtes intéressé-e par apprendre à programmer, j'espère que vous trouverez ce livre utile.

Pour que ce livre soit utile, vous aurez besoin d'un environnement pour écrire, compiler et lancer vos programmes D. Cet environnement de développement doit comprendre au moins :

  • un éditeur de texte ;
  • un compilateur D.

Vous pouvez également utiliser un Environnement de Développement Intégré (IDE) plutôt que des programmes séparés. Vous pouvez trouver des informations à propos des éditeurs et des IDEs pour le langage D sur les pages Editors et IDEs de dlang.org. Vous ne pouvez pas apprendre à programmer en D sans éditeur de texte ou compilateur. Nous verrons comment installer un compilateur et comment compiler des programmes dans des chapitres ultérieurs.

La version originale de l'ouvrage explique comment installer et utiliser le compilateur dmd.

Chaque chapitre du livre introduit le moins de nouveaux concepts possible. La plupart des chapitres ont quelques exercices à la fin. Ces exercices sont corrigés, vous pouvez ainsi comparer vos solutions aux miennes. Les corrigés sont à la fin du livre.

Je vous suggère de ne pas sauter de chapitre. si vous arrivez à un chapitre que vous trouvez particulièrement difficile, c'est peut-être que le livre n'a par erreur pas introduit tous les concepts nécessaire. Dans ce cas, merci de me contacter pour rendre ce livre plus utile (NdT: ou de contacter le traducteur, qui transmettra à l'auteur).

Ce livre ne couvre pas la programmation d'interfaces graphique (GUI). Même si beaucoup de programmes sont plus utilisables avec une interface graphique, les interfaces graphiques ne sont pas directement relatives aux langages de programmation. De plus, les choix de conception et de style des interfaces graphiques peuvent se heurter à ceux du langage de programmation et de sa librairie standard et rendre difficile l'apprentissage du langage. Pour cette raison, ce livre ne traite que des programmes console. Une fois que vous aurez appris le D et sa librairie standard, Phobos, vous serez capable d'utiliser la librairie graphique que vous voudrez.

Les chapitres de ce livre sont mis en ligne une fois qu'ils sont traduits en anglais à partir du turc. Vous pouvez suivre le fil RSS pour être informé quand de nouveaux chapitres sont disponibles.

Apprendre à programmer est plus amusant quand c'est partagé avec d'autres personnes. Allez sur le groupe de discussions D.learn sur http://forum.dlang.org/ pour suivre les conversations, poser et répondre à des questions.

N'hésitez pas à contacter l'auteur ou le traducteur par courrier électronique pour tout commentaire ou toute correction à propos de cet ouvrage ou de sa traduction. Merci !

4. L'art de la programmation

La définition de l'art de la programmation est quelque peu discutable, mais son aspect conception est très important. Quelques idées sur la programmation :

  • C'est la tâche de concevoir des programmes de manière à ce que l'ordinateur se comporte comme on le veut.
  • Comme elle nécessite des outils et que l'utilisation de ces outils est guidé par l'expérience des meilleurs programmeurs, c'est un métier.
  • Comme ça implique la résolution de problèmes sous contraintes, c'est de l'ingénierie.
  • C'est très amusant et satisfaisant.
  • Ce n'est pas un art à proprement parler mais autant que c'est possible dans tout ce que les humains font, les programmes peuvent être des œuvres d'art.
  • Ce n'est pas une science, mais les méthodes que ça utilise viennent de la science des ordinateurs qu'est l'informatique.

4-1. Programmer peut être très difficile à apprendre et à enseigner.

La programmation a été enseignée depuis les années 1950. Il n'y a toujours pas de méthode efficace et fructueuse pour le faire à ce jour.

Malheureusement, apprendre à programmer peut être une tâche difficile pour une bonne moitié de tous les étudiants. Selon un article de la recherche, ceux qui peuvent apprendre à programmer facilement sont ceux qui sont capable de créer des modèles consistants pour décrire les situations inconnues qu'ils rencontrent.

Certaines difficultés en programmation viennent de la quantité de détails techniques qu'il est nécessaire d'apprendre.

5. Hello World !

Le premier programme montré dans la plupart des livres qui présentent un langage de programmation est le programme hello world. C'est un programme très simple et très court qui affiche "hello world" et qui se termine (NdT : "hello world" signifie "bonjour le monde"). Ce programme est important parce qu'il présente quelques concepts essentiels de ce langage.

Voilà ce que ça donne en langage D :

 
Sélectionnez
import std.stdio;
 
void main()
{
    writeln("Hello world!");
}

Le code source ci-dessus doit être compilé par un compilateur D pour obtenir un programme exécutable par l'ordinateur, l'ordinateur ne pouvant pas directement exécuter le code source.

5-1. Installation du compilateur.

Lors de l'écriture de ce chapitre, nous avons le choix entre trois compilateurs :

dmd, le compilateur de Digital Mars ;

gdc, le compilateur de GCC ;

ldc, le compilateur adapté à l'architecture LLVM.

dmd est le compilateur qui est utilisé pendant la conception et le développement du langage D depuis des années. Tous les exemples de ce livre ont été testés avec. (NdT : l'auteur affirme que pour cette raison, ce serait plus facile pour vous de commencer avec dmd et d'essayer d'autres compilateurs seulement en cas de besoin spécifique. Personnellement, j'utilise plutôt gdc, parce qu'il est libre, parce que je connais déjà GCC et parce qu'il est plus simple à installer chez moi, mais je n'ai pas revérifié si tous les codes du tutoriel fonctionnaient avec. À priori oui, d'autant que c'est avec ce livre que j'ai appris le D.)

Pour installer dmd, rendez-vous sur la page de téléchargement de Digital Mars et sélectionnez la version qui correspond à votre système. n'installez pas un compilateur fait pour la version 1 du langage D. Ce livre ne couvre que la version 2 du D. Le processus d'installation ne devrait pas être très compliqué. (NdT : les utilisateurs de Linux devraient pouvoir installer gdc comme n'importe quel autre logiciel, il se trouve dans les dépôts de n'importe quelle distribution répandue.)

5-2. Fichier source

Le fichier que le programmeur écrit et qui est compilé par le compilateur D s'appelle le fichier source. Comme D est un langage compilé, le fichier source lui-même n'est pas un programme exécutable. Il doit être converti en un programme exécutable par le compilateur.

Comme avec n'importe quel fichier, le fichier source doit avoir un nom. Même si le nom peut être n'importe quoi, il est conseillé d'utiliser l'extension .d pour le fichier source parce que les environnements de développement, les outils, les programmeurs s'attendent tous à ce que ce soit le cas. Par exemple, test.d, game.d, invoice.d, etc. sont des noms de fichiers sources appropriés.

5-3. Compiler le programme hello world

Copiez le code du programme hello world précédent dans un fichier texte et enregistrez-le sous le nom hello.d (NdT : je vous conseille de vous créer un dossier dans lequel vous placerez tout ce dont vous aurez besoin lors de votre apprentissage du langage D).

Le compilateur va bientôt vérifier la syntaxe de ce code source (c.-à-d. si elle est valide selon les règles du langage) et produire un programme à partir de ce code en le traduisant en code machine. Pour compiler le programme, suivez ces étapes :

  1. Ouvrez un terminal.
  2. Déplacez-vous vers le dossier où vous avez placé votre fichier hello.d.
  3. Entrez la commande suivante :
  • Pour les utilisateurs de dmd : dmd hello.d
  • Pour les utilisateurs de gdc :

    • si vous êtes sous Windows : gdc hello.d -o hello.exe
    • sinon :gdc hello.d -o hello

Si vous n'avez fait aucune erreur, vous pouvez penser qu'il ne s'est rien passé. Au contraire, ça veut dire que tout s'est bien passé. Il devrait y avoir un exécutable nommé hello (ou hello.exe sous Windows) qui vient d'être créé par le compilateur.

Si par contre le compilateur a affiché des messages, vous avez probablement fait une erreur. Identifiez la, corrigez la et réessayez de compiler. Vous allez régulièrement faire beaucoup d'erreurs en programmant, le processus de correction-compilation vous deviendra habituel.

Une fois le programme créé, tapez le nom de l'exécutable pour le lancer. Vous devriez voir « Hello world » s'afficher à l'écran :

 
Sélectionnez
$ ./hello      # exécution du programme.
Hello world!   # le message qu'il affiche

Le dollar ($) est ici pour séparer la commande de sa sortie. Il ne faut pas l'inclure dans la commande lorsqu'on l'exécute.

Félicitations ! Votre programme fonctionne comme prévu.

5-4. Paramètres du compilateur

(NdT : les mentions de gdc dans cette sous-partie n'apparaissent pas dans l'œuvre originale, elle ont été ajoutée par le traducteur.)

Le compilateur a beaucoup de paramètres en lignes de commande qui sont utilisés pour influencer la compilation du programme.
Pour voir une liste des paramètres, les utilisateurs de dmd taperont simplement le nom de leur compilateur :

 
Sélectionnez
$ dmd    #  Entrez seulement le nom
DMD64 D Compiler v2.059
Copyright (c) 1999-2012 by Digital Mars written by Walter Bright
Documentation: http://www.dlang.org/index.html
Usage:
dmd files.d ... { -switch }
 
files.d      D source files
...
-unittest    compile in unit tests
...
-w           enable warnings
...

NdT : les utilisateurs de gdc sont invités à lire la page de manuel de gdc et celle de gcc, gdc acceptant la plupart des options de gcc (pour quitter le manuel, appuyez sur "q") :

 
Sélectionnez
$ man gdc
$ man gcc

L'extrait de sortie ci-avant montre seulement les paramètres que je vous recommande d'utiliser à chaque fois. Même si ça ne change rien pour le programme "hello world" de ce chapitre, la ligne de commande suivante compilera le programme en activant les avertissements et les tests unitaires. Nous verront à quoi cela correspond ainsi que d'autres paramètres du compilateur plus en détail dans les chapitres suivants :

 
Sélectionnez
dmd hello.d -w -unittest

NdT : les utilisateurs de gdc pourront utiliser cette ligne :

  • Sous Windows :

    • gdc hello.d -o hello.exe -Wall -funittest
  • autre systèmes :

    • gdc hello.d -o hello -Wall -funittest

La liste complète des paramètres de dmd peut être trouvée dans la documentation du Compilateur DMD.

Un autre paramètre de la ligne de commande que vous pouvez trouver utile est -run. Il compile le code source, produit l'exécutable et le lance en une seule commande :

 
Sélectionnez
$ dmd -run hello.d -w -unittest
Hello world!     ← Le programme est automatiquement exécuté

5-5. IDE

En plus du compilateur, vous pouvez aussi considérer l'installation d'un EDI (Environnement de Développement Intégré, IDE en anglais). Les EDI sont conçu pour rendre le développement de programmes plus facile en simplifiant les étapes d'écriture, de compilation et de débogage.

Si vous installez un EDI, compiler et exécuter un programme sera aussi simple qu'appuyer sur une touche ou cliquer sur un bouton. Je vous recommande tout de même de vous familiariser avec la compilation manuelle en ligne de commande.

Si vous décidez d'installer un EDI, allez sur la page de dlang.org sur les EDI pour voir la liste des EDI disponibles.

5-6. Contenu du programme hello world

Voici une petite liste des quelques concepts du langage D qui apparaissent dans ce petit programme.

5-6-1. Bases du langage

Chaque langage définit sa propre syntaxe, ses types fondamentaux, mots-clés, règles, etc. Tous ces éléments font partie des fondements du langage. Les parenthèses, les points virgules, des mots comme main ou void font tous partie des règles du langage D. Ils sont similaires aux règles du français : sujet, verbe, ponctuation, structures de la phrase, etc.

5-6-2. Bibliothèques et fonctions

Les bases du langage ne définissent que sa structure. Elles sont utilisées pour définir des fonctions et des types de l'utilisateur, qui sont à leur tour utilisés pour créer des bibliothèques. Les bibliothèques sont des collections de parties de programmes réutilisables qui sont liées à vos programmes pour les aider à atteindre leur but.

Ci-dessus, writeln est une fonction de la bibliothèque standard du langage D. Elle est utilisée, comme son nom l'indique, pour écrire une ligne ("write line").

5-6-3. Modules

Les bibliothèques sont composées de parties regroupant des codes formant une certaine unité par leurs rôles, les tâches qu'ils accomplissent, etc. Ces parties forment ce qu'on appelle des modules. Le seul module que ce programme utilise est std.stdio, qui gère les entrées et sorties de données.

5-6-4. Caractères et chaînes

Les expressions du type "Hello world" sont appelées chaînes de caractères (ou simplement "chaînes"), et les éléments des chaînes sont des caractères. La seule chaîne de ce programme contient les caractères 'H', 'e', '!', entre autres.

5-6-5. Ordre des opérations

Les programmes accomplissent leurs tâches en exécutant des opérations dans un certain ordre. Ces tâches commencent avec les opérations qui sont écrites dans la fonction nommée main. La seule opération dans ce programme écrit "Hello world!".

5-6-6. Sens des lettres minuscules et majuscules

Vous pouvez choisir de taper n'importe quel caractère dans des chaînes, mais vous devez taper les autres caractères exactement tels qu'ils apparaissent dans le programme. C'est parce que les majuscules et les minuscules sont significatives dans les programmes D. Par exemple, writeln et Writeln sont deux noms différents.

5-6-7. Mots-clés

Les mots spéciaux qui font partie du langage de base sont appelés mots-clés. Il y a deux mots-clés dans ce programmes : import, qui est utilisé pour inclure un module au programme, et void, qui veut dire "ne retourne rien".

La liste complète des mots-clés de D est : abstract, alias, align, asm, assert, auto, body, bool, break, byte, case, cast, catch, cdouble, cent, cfloat, char, class, const, continue, creal, dchar, debug, default, delegate, delete, deprecated, do, double, else, enum, export, extern, false, final, finally, float, for, foreach, foreach_reverse, function, goto, idouble, if, ifloat, immutable, import, in, inout, int, interface, invariant, ireal, is, lazy, long, macro, mixin, module, new, nothrow, null, out, override, package, pragma, private, protected, public, pure, real, ref, return, scope, shared, short, static, struct, super, switch, synchronized, template, this, throw, true, try, typedef, typeid, typeof, ubyte, ucent, uint, ulong, union, unittest, ushort, version, void, volatile, wchar, while, with, FILE, MODULE, LINE, FUNCTION, PRETTY_FUNCTION, __gshared, __traits, __vector, et __parameters.

Nous les découvrirons tous dans les chapitres suivants à l'exception de asm et de __vector, qui sortent du cadre de ce livre; delete, typedef, et volatile sont dépréciés; et macro n'est pas utilisé pour le moment.

5-6-8. Exercices

  1. Modifiez le programme pour qu'il affiche autre chose.
  2. Changez le programme pour qu'il affiche plus d'une ligne.
  3. Essayez de compiler le programme après avoir apporté d'autres modifications ; par exemple, supprimez le point-virgule à la fin de la ligne qui contient writeln et observez les erreurs de compilation.

SolutionLe programme Hello world - Correction.

6. writeln et write

Dans le chapitre précédent, nous avons vu que writeln prend une chaîne de caractères entre parenthèses et affiche la chaîne.

Les parties de programmes qui effectuent une tâche sont appelées fonctions et les informations dont elles ont besoin pour accomplir cette tâche sont appelées paramètres. L'acte de donner ces informations aux fonctions est appelé "passer des valeurs en paramètres de celles-ci". Les paramètres sont passés aux fonctions dans des parenthèses, séparés par des virgules.

Note : le mot "paramètre" décrit l'information qui est passée à une fonction d'un point de vue conceptuel. L'information concrète qui est effectivement passée pendant l'exécution du programme est appelé "argument". Même si c'est incorrect, ces termes sont parfois confondus dans le monde du logiciel.

writeln peut prendre plus d'un argument. Elle les affiche les uns après les autres sur la même ligne :

 
Sélectionnez
import std.stdio;
 
void main()
{
    writeln("Hello world!", "Hello fish!");
}

Parfois, toutes les informations qui doivent être affichées sur la même ligne peuvent ne pas être disponibles au même moment. Dans de tels cas, les premières parties de la ligne peuvent être affichées avec write et la dernière partie de la ligne peut être affichée avec writeln.

writeln passe à la ligne, write reste sur la même ligne :

 
Sélectionnez
import std.stdio;
 
void main()
{
    // Affichons ce qu'on a déjà :
    write("Hello");
 
    // ... ici on fait des calculs, des opérations ...
 
    write("world!");
 
    // ... et finalement:
    writeln();
}

Appeler writeln sans aucun paramètre sert simplement à passer à la ligne.

Les lignes qui commencent par // sont appelées lignes de commentaires ou juste commentaires. Un commentaire ne fait pas partie du code du programme dans le sens où il n'affecte pas le comportement du programme.

Sa seule utilité est d'expliquer ce que le code fait dans cette section particulière du programme. Le commentaire est adressé à toute personne qui sera amenée à lire le code plus tard, y compris le programmeur qui a écrit ce commentaire lui-même.

6-1. Exercices

  1. Les deux programmes de ce chapitre affichent les chaînes sans aucun espace entre eux. Modifiez ces programmes pour qu'il y ait un espace entre les arguments comme dans "Hello world!".
  2. Essayez d'appeler write avec plus d'un paramètre.

Solutionwriteln et write - Correction.

7. Compilateur

Nous avons vu que les deux outils les plus utilisés en programmation D sont l'éditeur de texte et le compilateur. Les programmes D sont écrits dans des éditeurs de textes.

Le concept de compilation et le rôle du compilateur doivent aussi être compris lors de l'utilisation de langages compilés tels que le D.

7-1. Code machine

Le cerveau de l'ordinateur est son microprocesseur (ou le CPU : Central Processing Unit, ce qui veut dire "unité centrale de traitement"). Dire au CPU ce qu'il doit faire est appeler "coder" et les instructions qui sont utilisé pour faire cela sont appelées "code machine".

La plupart des architectures de CPU utilisent du code machine (des instructions) spécifique. Ces instructions sont déterminées par des contraintes matérielles pendant la conception de l'architecture. Au plus bas niveau, ces instructions sont implémentées comme des signaux électriques. Parce que la facilité de codage n'est pas la première préoccupation à ce niveau, écrire directement des programmes sous forme de code machine est une tâche très difficile.

Ces instructions sont des nombres spéciaux, qui représentent des opérations diverses prises en charge par le CPU. Par exemple, pour un CPU 8-bit fictif, le nombre 4 pourrait représenter l'opération de chargement, le nombre 5 l'opération de stocker et le nombre 6 l'opération d'incrémenter. En supposant que les 3 bits de gauche sont le numéro de opération et que les 5 bits de droite sont la valeur utilisée dans cette opération, un simple programme en code machine pour ce CPU pourrait ressembler à ceci :

Opération

Valeur

Signification

100

11110

LOAD

11110

101

10100

STORE

10100

110

10100

INCREMENT

10100

'000

00000

PAUSE

 

Le code machine est si proche du matériel qu'il n'est pas adapté pour représenter des idées de plus haut niveau comme une carte de jeu ou une inscription d'étudiant.

7-2. Langage de programmation

Les langages de programmation sont conçus comme des moyens efficaces de programmer un CPU, capables de représenter des concepts de plus haut niveau. Ils n'ont pas a s'occuper des contraintes matérielles ; leurs buts principaux sont la facilité d'utilisation et leur expressivité. Ils sont plus faciles à comprendre pour des humains et plus proches des langages naturels :

 
Sélectionnez
if (une_carte_a_ete_jouee()) {
    afficher_la_carte();
}

Cependant, les langages de programmation suivent des règles bien plus strictes et formelles que n'importe quel langage parlé.

7-2-1. Langages compilés

Dans certains langages de programmation, les instructions du programme doivent être compilées avant de devenir un programme que l'on pourra exécuter. De tels langages produisent des programmes rapides à l'exécution mais le processus de développement implique deux étapes principales : écrire le programme et le compiler.

En général, les langages compilés aident à repérer les erreurs de programmation avant même que le programme commence son exécution.

D est un langage compilé.

7-2-2. Langages interprétés

Certains langages de programmation ne nécessitent pas de compilation. De tels langages sont appelés langages interprétés. Les programmes peuvent être exécutés directement à partir du programme écrit à la main. Quelques exemples de langages interprétés : Python, Ruby, Perl. Puisqu'il n'y a pas d'étape de compilation, le développement est plus facile en utilisant ces langages. D'un autre côté, comme les instructions du programme doivent être analysées pour être interprétées à chaque fois que le programme est exécuté, les programmes écrits dans ces langages sont plus lents que leurs équivalents écrits en langages compilés.

En général, pour un langage interprété, beaucoup de types d'erreurs ne peuvent pas être découvertes avant l'exécution du programme.

7-2-3. Compilateur

Le rôle d'un compilateur est la traduction : il traduit le programme écrit dans un langage de programmation en code machine. C'est la traduction depuis le langage du programmeur vers le langage du CPU. Cette traduction est appelée compilation. Chaque compilateur comprend un langage de programmation particulier et est présenté comme un compilateur de ce langage, comme dans « un compilateur D ».

7-2-4. Erreur de compilation

Comme le compilateur compile un programme selon les règles du langage, il arrête la compilation dès qu'il rencontre une instruction illégale. Les instructions illégales sont celles qui sont hors des spécifications du langage. Des problèmes comme des parenthèses non fermées, un point-virgule manquant, un mot-clé mal orthographié, etc. sont des causes d'erreurs de compilation.

Le compilateur peut aussi émettre un avertissement lors de la compilation s'il rencontre un bout de code douteux qui peut poser problème mais qui n'est pas forcément une erreur. Cependant, les avertissements indiquent presque toujours une erreur ou un mauvais style, il est donc monnaie courante de considérer la plupart des avertissements (sinon tous) comme des erreurs.

8. Types fondamentaux

Nous avons vu que le cerveau d'un ordinateur est le CPU. La plupart des tâches d'un programme sont effectuées par le CPU et le reste est dispatché aux autres parties de l'ordinateur.

La plus petite unité de données dans un ordinateur est appelé un bit, qui peut avoir la valeur 0 ou 1.

Comme un type de données qui ne peut stocker que des 0 ou des 1 n'aurait qu'une utilité très limitée, le CPU définit des type de données plus grands qui sont des combinaisons de plus d'un bit. Par exemple, un octet est composé de 8 bits. Le type le plus efficace d'un CPU fait de ce CPU un CPU N bits, comme dans CPU 32 bits, CPU 64 bits, etc.

Les types de données que le CPU définit ne sont encore pas suffisants : ils ne peuvent représenter des concepts de plus haut niveau comme le nom d'un étudiant ou une carte à jouer. D propose beaucoup de type de données utiles, mais même ces types ne sont pas suffisants pour représenter beaucoup de concepts de plus haut niveau. De tels concepts doivent être définis par le programmeur au moyen de structures et de classes, que l'on verra dans des chapitres ultérieurs.

Les types fondamentaux du D sont très similaires aux types fondamentaux de beaucoup d'autres langages ; ils sont présentés dans le tableau qui suit (les termes qui apparaissent dans ce tableau sont expliqués en dessous) :

Type fondamental du D

Définition

Valeur initiale

bool

type booléen

false

byte

8 bits signé

0

ubyte

8 bits non signé

0

short

16 bits signé

0

ushort

16 bits non signé

0

int

32 bits signé

0

uint

32 bits non signé

0

long

64 bits signé

0L

ulong

64 bits non signé

0L

float

32 bits à virgule flottante

float.nan

double

64 bits à virgule flottante

double.nan

real

soit le type à virgule flottante le plus large que le matériel prend en charge, soit double : le plus large des deux

real.nan

ifloat

float à valeur imaginaire

float.nan * 1.0i

idouble

double à valeur imaginaire

double.nan * 1.0i

ireal

real à valeur imaginaire

real.nan * 1.0i

cfloat

nombre complexe construit à partir de deux float

float.nan + float.nan * 1.0i

cdouble

nombre complexe construit à partir de deux double

double.nan + double.nan * 1.0i

creal

nombre complexe construit à partir de deux real

real.nan + real.nan * 1.0i

char

unité de stockage UTF-8

0xFF

wchar

unité de stockage UTF-16

0xFFFF

dchar

unité de stockage UTF-32 et point de code Unicode

0x0000FFFF

En plus de ces types, le mot-clé void représente "aucun type". Les mots-clés cent et ucent sont réservés pour un usage futur pour représenter les valeurs 128-bit signées et non signées.

Sauf s'il y a une raison spécifique de ne pas le faire, vous pouvez utiliser int pour représenter les valeurs entières. Pour représenter des valeurs fractionnaires, utilisez plutôt double.

Les termes suivants apparaissent dans le tableau :

  • Booléen : le type des expressions logiques, avec la valeur true pour représenter quelque chose de vrai et false pour quelque chose de faux.
  • Type signé : un type qui peut avoir des valeurs positives ou négatives. Par exemple, byte peut stocker des valeurs entre -128 et 127. Le nom de ces types viennent du signe négatif.
  • Type non signé : un type qui ne peut stocker que des valeurs positives. Par exemple, ubyte peut stocker des valeurs entre 0 et 255. Le u au début du nom de ces types vient de unsigned (qui veut dire "non signé").
  • Virgule flottante : un type qui peut représenter un nombre à virgule comme 1,25 (NdT : on parle de valeur à virgule flottante ou de valeur flottante). La précision des calculs à virgule flottante est directement reliée au nombre de bits du type : plus le nombre de bits est grand, plus le résultat est précis. Seuls les types à virgule flottante peuvent représenter des nombres à virgule. Les types entiers comme int ne peuvent représenter que des valeurs entières comme 1 et 2.
  • Type complexe : un type qui peut représenter les nombres complexes des mathématiques.
  • Type imaginaire : un type qui représente seulement la partie imaginaire d'un nombre complexe. Le i qui apparaît dans la colonne des valeurs initiales est le nombre i tel que kitxmlcodeinlinelatexdvpi^2=-1finkitxmlcodeinlinelatexdvp en mathématiques.
  • nan : initiales de "Not A Number" ("n'est pas un nombre"), représentant une valeur flottante non valide.

8-1. Attributs des types

Les types D ont des propriétés auxquelles on peut accéder par des attributs. On accède à ces propriétés avec un point après le nom du type. Par exemple, on accède à la taille de int avec int.sizeof. Nous verrons seulement 4 de ces attributs dans ce chapitre :

  • .stringof est le nom du type
  • .sizeof est la longueur du type en termes d'octet. (Pour déterminer le nombre de bits, cette valeurs doit être multipliée par 8, le nombre de bits dans un octet.)
  • .min (pour "minimum"). C'est la plus petite valeur que le type peut avoir.
  • .max (pour "maximum"). C'est la plus grande valeur que le type peut avoir.

Voici un programme qui affiche ces propriétés pour int :

 
Sélectionnez
import std.stdio;
 
void main()
{
    writeln("Type              ~ : ", int.stringof);
    writeln("Longueur en octets~ : ", int.sizeof);
    writeln("Valeur minimale   ~ : ", int.min);
    writeln("Valeur maximale   ~ : ", int.max);
}

Sur ma machine, ce programme donnerait le résultat suivant:

 
Sélectionnez
Type           : int
Length in bytes: 4
Minimum value  : -2147483648
Maximum value  : 2147483647
Initial value  : 0

8-1-1. size_t

Vous allez également rencontrer le type size_t. size_t n'est pas un type à part entière mais est un alias d'un type non signé existant. Son non vient de "size type" (type taille). C'est le meilleur type pour représenter des idées comme la taille ou pour compter. size_t doit être assez large pour représenter le nombre d'octets de la mémoire qu'un programme peut potentiellement utiliser. Sa taille est donc dépendante du système : uint sur un système 32 bits, ulong sur un système 64 bits, etc.

Vous pouvez utiliser l'attribut .stringof pour savoir de quel type size_t est un alias sur votre système :

 
Sélectionnez
import std.stdio;
 
void main()
{
    writeln(size_t.stringof);
}

Sur mon système (NdT: et sur le mien aussi), cela donne :

 
Sélectionnez
ulong

8-1-2. Exercice

Afficher les propriétés d'autres types.

vous ne pouvez pas utiliser les types réservés cent et ucent, et void n'a pas les attributs .min, .max et .init.

De plus, la propriété .min a été dépréciée pour les types à virgule flottante. (Vous pouvez voir les différentes propriétés des types fondamentaux dans la Spécification des propriétés ). Si vous utilisez un type à virgule flottante dans cet exercice, le compilateur vous préviendra que .min n'est pas valide pour ce type. À la place, comme nous le verrons plus tard dans le chapitres sur les types à virgule flottante, il faut utiliser le négative de la propriété .max , comme par exemple -double.max .

SolutionTypes Fondamentaux - Correction .

9. Affectation et ordre d'évaluation

Les deux premières difficultés auxquelles sont confrontés les étudiants quand ils apprennent à programmer concernent l'opération d'affectation et l'ordre d'évaluation.

9-1. Opération d'affectation

Vous verrez des lignes similaires à celle qui suit dans presque tous les programmes écrits dans presque n'importe quel langage de programmation :

 
Sélectionnez
a = 10;

Le sens de cette ligne est "changer la valeur de a par 10". De façon similaire, la ligne suivante veut dire "changer la valeur de b par 20" :

 
Sélectionnez
b = 20;

D'après cette information, que peut-on dire de cette ligne ?

 
Sélectionnez
a = b;

Malheureusement, la ligne n'est pas une égalité mathématique classique comme nous en avons tous l'habitude. L'expression ci-dessus ne veut pas dire "a est égal à b" ! En appliquant la même logique que pour les lignes précédentes, l'expression ci-dessus doit vouloir dire "changer la valeur de a par b". "Changer la valeur de a par b" veut dire "changer la valeur de a par la valeur de b".

Le connu symbole "=" des mathématiques a un sens complètement différent en programmation : changer la valeur de gauche par celle de droite.

9-2. Ordre d'évaluation

Les opérations d'un programme sont appliquées étape par étape dans un ordre spécifique. Nous pouvons voir les 3 expressions précédentes dans un programme dans cet ordre :

 
Sélectionnez
a = 10;
b = 20;
a = b;

Le sens de ces trois lignes ensemble est : "changer la valeur de a par 10", ensuite changer la valeur de b par 20, ensuite changer la valeur de a par celle de b". En conséquence, après que ces trois opérations sont effectuées, les valeurs de a et de b sont toutes les deux égales à 20.

9-3. 3 Exercice

Observer que les trois opérations suivantes échangent les valeurs de a et de b. Si leurs valeurs de départ sont respectivement 1 et 2, après les opérations les valeurs deviennent 2 et 1.

 
Sélectionnez
c = a;
a = b;
b = c;

SolutionAffectation et ordre d'évaluation - Correction.

10. Variables

Les concepts concrets qui sont représentés dans un programme sont appelés variable. Une valeur comme la température de l'air ou un objet plus compliqué comme un moteur de voiture peuvent être des variables d'un programme.

Toute variable a un certain type et une certaine valeur. La plupart des variables ont également un nom mais certaines variables sont anonymes.

Pour un exemple de variable, on peut penser à l'idée du nombre d'étudiants dans une école. Comme le nombre d'étudiants est un nombre entier, int est un type convenable et nombreEtudiants serait un nom suffisamment explicite.

Selon les règles de syntaxe du langage D, une variable est introduite par son type suivi de son nom. Introduire une variable dans le programme s'appelle sa définition. Une fois qu'une variable est définie, son nom se met à représenter sa valeur.

 
Sélectionnez
import std.stdio;
 
void main()
{
 // La définition de la variable ; cette définition
 // indique que le type de nombreEtudiants est int :
 int nombreEtudiants;
 
 // Le nom de la variable devient sa valeur :
 writeln("Il y a ", nombreEtudiants, " étudiants.");
}

Voici ce qu'affiche ce programme :

 
Sélectionnez
Il y a 0 étudiants.

Comme on le voit dans cet affichage, la valeur de nombreEtudiants est 0. Cela correspond au tableau des types fondamentaux du chapitre précédent : La valeur initiale de int est 0.

Notez que le nom de la variable, "nombreEtudiants", n'est jamais affiché. En d'autres termes, la sortie du programme n'est pas "Il y a nombreEtudiants étudiants.".

Les valeurs des variables sont changés par l'opérateur =. Cet opérateur affecte des nouvelles valeurs aux variables, il est donc nommé l'opérateur d'affectation :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int nombreEtudiants;
   writeln("Il y a ", nombreEtudiants, " étudiants.");
 
   // Affecter la valeur 200 à la variable nombreEtudiants :
   nombreEtudiants = 200;
   writeln("Il y a maintenant ", nombreEtudiants, " étudiants.");
}

Sortie :

 
Sélectionnez
Il y a 0 étudiants.
Il y a maintenant 200 étudiants.

Quand la valeur d'une variable est connue au moment de la définition d'une variable, la variable peut être définie et affectée en même temps. C'est une pratique importante, elle rend impossible d'utiliser une variable avant de lui affecter la valeur voulue :

 
Sélectionnez
import std.stdio;
 
void main()
{
   // Définition et affectation en même temps:
   int nombreEtudiants = 100;
 
   writeln("Il y a ", nombreEtudiants, " étudiants.");
}

Sortie :

 
Sélectionnez
Il y a 100 étudiants.

10-1. Exercice

Définir deux variables pour afficher "J'ai échangé 20 Euros à un taux de 2.11". Vous pouvez utiliser double pour la valeur flottante.

SolutionVariable - Correction.

11. Flux d'entrée et de sortie standards

Jusqu'ici, l'affichage de nos programmes apparaissait dans la console. Bien qu'elle soit la destination dans la plupart des cas, en réalité les caractères sont envoyés dans les flux de sortie des programmes.

La sortie standard est basée sur les caractères : tous les objets à afficher sont d'abord convertis en leur représentation sous forme de caractères et sont ensuite envoyés à la sortie un par un en tant que caractères. Par exemple, la valeur entière 100 que nous avons affichée dans le chapitre précédent n'est pas envoyée à la sortie en tant que valeur 100, mais comme trois caractères : '1', '0', et '0'.

De façon similaire, ce que nous percevons normalement comme le clavier est en fait le flux d'entrée standard d'un programme et est aussi basé sur les caractères. L'information vient toujours sous forme de caractères à convertir en données. Par exemple la valeur 42 vient en fait de l'entrée standard sous forme de caractères : '4' et '2'.

Ces conversions se font automatiquement.

Ce concept de caractères qui se suivent est appelé un flux de caractères. Comme l'entrée standard et la sortie standard du D correspondent à cette description, ce sont des flux de caractères.

Les noms des flux d'entrée standard et de sortie standard en D sont respectivement stdin et stdout.

Les opérations sur ces flux nécessitent normalement le nom du flux, un point, et l'opération, comme dans stream.operation(). Cependant, comme stdin et stdout sont très couramment utilisés, par commodité, les opérations standard les concernant peuvent être appelées sans qu'il y ait besoin du nom du flux et du point, comme dans operation().

writeln, que l'on a utilisé dans les chapitres précédents, est en effet la forme abrégée de stdout.writeln. De même, write est la forme abrégée de stdout.write. Du coup, le programme hello world peut aussi être écrit comme cela :

 
Sélectionnez
import std.stdio;
 
void main()
{
   stdout.writeln("Hello world!");
}

11-1. Exercice

Observez que write (ou writeln) fonctionne de la même manière que stdout.write (ou stdout.writeln).

SolutionEntrée standard et flux de sortie - Correction.

12. Expressions logiques

Le travail qu'un programme réalise est accompli par des expressions. Toute partie d'un programme qui produit une valeur ou un effet de bord est appelée une expression. La définition d'expression est très large car même une constante comme 42 et une chaîne comme "bonjour" sont des expressions parce qu'elles produisent respectivement les valeurs constantes 42 et "bonjour".

ne confondez pas produire une valeur avec définir une variable. Les valeurs n'ont pas besoin d'être associées à des variables.

Les appels de fonctions comme writeln sont également des expressions, parce qu'ils ont des effets de bords. Dans le cas de writeln, l'effet se produit sur le flux de sortie et se traduit par l'ajout de caractères dans ce flux. Un autre exemple tiré des programmes que nous avons écrits serait l'opération d'affectation, qui affecte la variable à sa gauche.

Parce qu'elles produisent des valeurs, les expressions peuvent faire partie d'autre expressions. Ceci nous permet de former des expressions plus complexes à partir d'expressions plus simples. Par exemple, en supposant qu'il y a une fonction nommée TemperatureActuelle() produisant la valeur de la température actuelle de l'air, la valeur qu'elle produit peut être directement utilisée dans une expression impliquant writeln.

 
Sélectionnez
writeln("Il fait actuellement ", TemperatureActuelle(),
      " degrés.");

Cette ligne consiste en 4 expressions :

  • "Il fait actuellement "
  • TemperatureActuelle()
  • "degrés."
  • l'expression writeln qui utilise les trois autres expressions.

Dans ce chapitre nous allons voir un genre particulier d'expressions qui sont utilisées dans les instructions conditionnelles.

Avant d'aller plus loin, je voudrais faire un petit rappel sur l'opérateur d'affectation, en mettant cette fois l'accent sur les deux expressions qui apparaissent à sa gauche et à sa droite : l'opérateur d'affectation (=) affecte la valeur de l'expression de droite à l'expression de gauche (p. ex. : à une variable).

 
Sélectionnez
Temperature = 23;   // La valeur de température devient 23

12-1. Expressions logiques

Les expressions logiques sont les expressions qui sont utilisées en arithmétique booléenne. Les expressions logiques sont ce qui fait qu'un programme prend des décisions comme « si la réponse est oui, j'enregistrerai ce fichier ».

Les expressions logiques peuvent prendre une valeur parmi seulement deux possibles : false (faux) qui indique que l'expression est fausse, et true (vrai) qui indique que l'expression est vraie.

J'utiliserai des expressions writeln dans les exemples qui suivent. Si une ligne se finit par true, cela voudra dire que ce qui est affiché sur la ligne est vrai. De même, false voudra dire que ce qui est sur la ligne est faux. Par exemple, si la sortie d'un programme est la suivante :

 
Sélectionnez
Il y a du café : true

Cela voudra dire qu'« il y a du café ». De même,

 
Sélectionnez
Il y a du café : false

voudra dire qu'« il n'y a pas de café ». Notez que le fait que « il y a » apparaît sur la gauche de la ligne ne veut pas dire que le café existe. J'utilise la construction « Il y a... : false » pour dire « il n'y a pas » ou « c'est faux ».

Les expressions logiques sont utilisées de façon intensive dans les instructions conditionnelles, les boucles, les paramètres de fonctions, etc. Il est essentiel de comprendre comment elles fonctionnent. Par chance, les expressions logiques sont très faciles à expliquer et utiliser.

Les opérateurs logiques qui sont utilisés dans les expressions logiques sont les suivants :

  • L'opérateur == répond à la question « est égal à ? ». Il compare les deux expressions à sa droite et à sa gauche et produit true si elles sont égales et false si elle ne sont pas égales. Par définition, la valeur que == produit est une expression logique.
    Par exemple, supposons que nous avons les deux variables suivantes :

     
    Sélectionnez
    int joursDansUneSemaine = 7;
    int moisDansUneAnnee = 12;

  • Les expressions suivantes sont deux expressions logiques qui utilisent ces valeurs :

     
    Sélectionnez
    joursDansUneSemaine == 7      // true
    moisDansUneAnnee == 11        // false
  • L'opérateur != répond à la question « n'est pas égal à ? ». Il compare les deux expressions à ses côtés et produit l'opposé de ==
    .

     
    Sélectionnez
    joursDansUneSemaine != 7      // false
    moisDansUneAnnee != 11        // true
  • l'opérateur || veut dire « ou » et produit true si l'une ou l'autre des expressions qui sont à ses cotés est vraie.
    Si la valeur de l'expression de gauche est true, il produit la valeur true sans même regarder l'autre expression. Si l'expression du côté gauche est fausse, alors l'opérateur produit la valeur de l'expression de droite. Cet opérateur est similaire à la conjonction « ou » du français dans le sens où si l'une, l'autre ou les deux expressions sont vraies, alors il produit true.
    Toutes les valeurs possibles des deux côtés de cet opérateur et leur résultat sont tels que présentés dans cette table de vérité :

    Expression de gauche

    Expression de droite

    Résultat

    false

    false

    false

    false

    true

    true

    true

    false (non évalué)

    true

    true

    true (non évalué)

    true

     
    Sélectionnez
    import std.stdio;
     
    void main()
    {
        // false veut dire «~ non~ », true veut dire «~ oui~ »
     
        bool ilExisteDuCafé = false;
        bool ilExisteDuThé = true;
     
        writeln("Il y a une boisson chaude~ : ",
                ilExisteDuCafé || ilExisteDuThé);
    }

    Du fait qu'une des deux expressions est vraie, l'expression logique de ce programme produit true.

    • L'opérateur && signifie « et », et produit true si les deux expressions sont vraies.
      Si la valeur de l'expression de gauche est fausse, il produit false sans même regarder l'expression qui est à sa droite. Si l'expression de gauche est vraie, alors il produit la valeur de l'expression de droite. Cet opérateur est similaire à la conjonction « et » du français : si la valeur de gauche et la valeur de droite sont toutes les deux true, alors il produit true.

      Expression de gauche

      Expression de droite

      Résultat

      false

      false (non évalué)

      false

      false

      true (non évalué)

      false

      true

      false

      false

      true

      true

      true

       
      Sélectionnez
      writeln("Je vais boire du café: ",
            jeVeuxBoireDuCafé && ilExisteDuCafé);

      Le fait que les opérateurs || et && peuvent ne pas évaluer l'expression de droite est appelé « comportement raccourci ». Le seul autre opérateur qui a ce comportement est l'opérateur ternaire ?:, qui sera vu dans un chapitre prochain. Tous les autres opérateurs évaluent et utilisent toujours toutes leurs expressions.

  • L'opérateur ^ répond à la question « l'un ou l'autre ? ». Cet opérateur produit true si seule une expression est vraie, mais pas les deux.

    Expression de gauche

    Expression de droite

    Résultat

    false

    false

    false

    false

    true

    true

    true

    false

    true

    true

    true

    false

  • Par exemple, la logique qui représente mon choix de jouer aux échecs si un seul de mes deux amis arrive peut être codé ainsi :

     
    Sélectionnez
    writeln("Je jouerai aux échecs
    : ", jimEstApparu ^ bobEstApparu);
  • L'opérateur < répond à la question « est plus petit que ? » (ou « vient avant dans l'ordre de tri ? »).

     
    Sélectionnez
    writeln("Nous gagnons
    : ", leurScore < notreScore);
  • L'opérateur > répond à la question «  est plus quand que ? » (ou « vient après dans l'ordre de tri ? »).

     
    Sélectionnez
    writeln("Ils gagnent
    : ", leurScore > notreScore);
  • L'opérateur <= répond à la question « est inférieur ou égal ? » (ou « vient avant ou au même endroit que ? »). Cet opérateur est l'opposé de l'opérateur >.

     
    Sélectionnez
    writeln("Nous n'avons pas été vaincus~ : ", leurScore <= notreScore);]
    • L'opérateur >= répond à la question « est supérieur ou égal à ? » (ou « vient après ou au même endroit que ? »). Cet opérateur est l'opposé de l'opérateur <.

       
      Sélectionnez
      writeln("Nous n'avons pas gagné
      : ", leurScore >= notreScore);
    • L'opérateur ! veut dire « l'opposé de ». Différent des autres opérateurs logiques, il prend une seule expression et produit true si cette expression est fausse, et false si cette expression est vraie.
 
Sélectionnez
writeln("Je marcherai
: ", !ilExisteUnVélo);

12-1-1. Grouper les expressions

L'ordre dans lequel les expressions sont évaluées peut être indiqué en utilisant des parenthèses pour les grouper. Quand des expressions entre parenthèses apparaissent dans des expressions plus complexes, les expressions entres parenthèses sont évaluées avant qu'elles puissent être utilisées dans les expressions dans lesquelles elles apparaissent. Par exemple, l'expression « s'il y a du café ou du thé, et aussi des cookies ou des pains au lait, alors je suis content » peut être codé de cette manière :

 
Sélectionnez
writeln("Je suis content~ : ",
   (ilExisteDuCafé || ilExisteDuThé) && (ilExisteDesCookies || ilExisteDesPains));

Si les sous-expressions n'étaient pas entre parenthèses, l'expression serait évaluée selon les règles de priorité opératoire du D (qui ont été héritées du C). Comme dans ces règles && a une plus grande priorité que ||, écrire l'expression sans parenthèses ne serait pas évalué comme voulu :

 
Sélectionnez
writeln("Je suis content~ : ",
   ilExisteDuCafé || ilExisteDuThé && ilExisteDesCookies || ilExisteDesPains);

L'opérateur && serait évalué en premier et l'expression entière aurait un sens équivalent à l'expression suivante :

 
Sélectionnez
writeln("Je suis content~ : ",
   ilExisteDuCafé || (ilExisteDuThé && ilExisteDesCookies) || ilExisteDesPains);

Cela a un sens complètement différent : « s'il y a du café, ou du thé et des cookies, ou du pain ; alors je suis content ».

12-1-2. Lire un booléen en entrée

Toutes les valeurs booléennes ci-dessus sont automatiquement affichées comme false et true. Ce n'est pas le cas dans la direction opposée : les chaînes "false" et "true" ne sont pas automatiquement lues comme valeurs false et true. Pour cette raison, l'entrée doit être d'abord lue en tant que chaîne et convertie vers une valeur booléenne.

Comme l'un des exercices suivants vous demande d'entrer "false" et "true" depuis l'entrée standard, j'ai été obligé d'utiliser des fonctionnalités du D que je ne vous ai pas encore expliquées. Je vais devoir définir une fonction qui convertit la chaîne en entrée en valeur booléenne. Cette fonction effectuera cette tâche en appelant to, qui est défini dans le module std.conv. (Vous pourrez voir des erreurs ConvException si vous entrez autre chose que "false" ou "true".)

J'espère que tout le code qui est dans les fonctions principales des programmes suivants est clair. read_bool() est la fonction qui contient de nouvelles fonctionnalités. Bien que j'aie inséré des commentaires pour expliquer ce qu'elle fait, vous pouvez ignorer cette fonction pour le moment. Malgré tout, elle est nécessaire pour que le programme marche et compile correctement.

12-1-3. Exercices

  • Nous avons vu ci-dessus que les opérateurs < et > sont utilisés pour déterminer si une valeur est inférieure ou supérieure à une autre valeur ; mais il n'y a pas d'opérateur qui réponde à la question « est entre ? » pour déterminer si une valeur est entre deux autres valeurs.
  1. Supposons qu'un programmeur a écrit le code pour déterminer si une valeur est entre 10 et 20. Observez que le programme ne peut pas être compilé tel quel :

     
    Sélectionnez
    import std.stdio;
     
    void main()
    {
        int valeur = 15;
     
        writeln("est entre~ : ",
                10 < valeur < 20);      //  ERREUR de compilation
    }
  2. Essayez d'utiliser des parenthèses autour de l'expression entière :

     
    Sélectionnez
    writeln("Est entre~ : ",
          (10 < valeur < 20));      //  ERREUR de compilation
  3. Observez qu'il ne peut toujours pas être compilé.
  • Alors qu'il cherchait une solution à ce problème, le même programmeur découvre que l'utilisation des parenthèses permet maintenant la compilation du code :

     
    Sélectionnez
    writeln("Est entre~ : ",
          (10 < valeur) < 20);      // &#8592; compile mais FAUX
  • Observez que le programme marche maintenant comme attendu et affiche "true". Malheureusement, la sortie est trompeuse parce que le programme a un bogue. Pour voir les effets de ce bogue, remplacez 15 avec une valeur supérieure à 20 :

     
    Sélectionnez
    int valeur = 21;
  • Observez que le programme affiche toujours "true" même si 21 n'est pas plus petit que 20.
    Astuce : se souvenir que le type d'une expression logique est bool. Vérifier qu'une valeur booléenne est inférieure à 20 ne devrait pas avoir de sens.

  • Changez l'expression du programme selon cette logique et observez que ça affiche maintenant "true" comme attendu. Vérifiez également que l'expression logique marche correctement pour d'autres valeurs. Par exemple, quand la valeur est 50 ou 1, le programme devrait afficher "false" ; et quand c'est 12, le programme devrait afficher "true".

  • si la distance jusqu'à la plage est inférieure à 10 et il y a un vélo pour tout le monde ;
    si nous somme moins de 6, et nous avons une voiture, et l'un de nous a le permis de conduire.
    Tel quel, le programme suivant écrit toujours "true". Écrivez une expression logique qui affichera "true" quand l'une des conditions ci-dessus est vraie. (Lors de l'essai du programme, entrez "false" ou "true" pour les questions qui commencent par « Y a-t-il ».). N'oubliez pas d'inclure la fonction read_bool() quand vous testerez le programme suivant :
  • L'expression logique qui répond à la question « est entre ? » doit plutôt être codée comme ceci : « est plus grand que la petite valeur et plus petit que la grande valeur ? »
  • Supposons que nous pouvons aller à la plage lorsque l'une des conditions suivantes est satisfaite :
 
Sélectionnez
import std.stdio;
import std.conv;
import std.string;
 
void main()
{
    write("Combien sommes-nous~ ? ");
    int nombrePersonnes;
    readf(" %s", &nombrePersonnes);
 
    write("Combien y a-t-il de vélos~ ? ");
    int nombreVélos;
    readf(" %s", &nombreVélos);
 
    write("À quelle distance est la mer~ ? ");
    int distance;
    readf(" %s", &distance);
 
    bool existeUneVoiture = read_bool("Y a-t-il une voiture~ ? ");
    bool existeUnPermis =
      read_bool("Y a-t-il un permis de conduire~ ? ");
 
    /*
      Remplacez la valeur 'true' ci-dessous par une expression logique qui
      produit la valeur true quand l'une des conditions
      listées dans la question est satisfaite~ :
   */
    writeln("Nous allons à la mer~ : ", true);
}
 
/*
Cette fonction utilise des fonctionnalités qui seront
expliquées dans des chapitres suivant.
*/
bool read_bool(string message)
{
    // Afficher le message
    write(message, "(false ou true) ");
 
    // Lire une ligne comme une chaîne
    string input;
    while (input.longueur == 0) {
      input = chomp(readln());
    }
 
    // Produire une valeur 'bool' depuis cette chaîne
    bool résultat = to!bool(input);
 
    // Retourner le résultat
    return résultat;
  1. Entrez des valeurs diverses et vérifiez que l'expression logique que vous avez écrite fonctionne correctement.

SolutionsExpressions logiques - Correction.

13. Lire depuis l'entrée standard

Toute donnée lue par le programme doit d'abord être stockée dans une variable. Par exemple, un programme lisant le nombre d'étudiants depuis l'entrée doit d'abord enregistrer cette information dans une variable. Le type de cette variable spécifique peut être int.

Comme on l'a vu dans le chapitre précédent, nous n'avons pas besoin de taper stdout quand nous affichons sur la sortie, parce que c'est sous-entendu. De plus, ce qui est à afficher est spécifié comme argument. write(nombreEtudiants) est donc suffisant pour afficher la valeur de nombreEtudiants. Pour résumer :

 

flux :

stdout

 
 

opération :

write

 
 

donnée :

la valeur de la variable nombreEtudiants

 
 

cible :

communément, la console

 

L'inverse de write est readf ; elle lit depuis l'entrée standard. Le "f" dans son nom vient de "formaté" puisque ce qu'elle lit doit toujours suivre un format précis.

Nous avons également vu dans le chapitre précédent que le flux d'entrée standard est stdin.

Dans le cas de la lecture, il manque encore une pièce du puzzle : où stocker les données. Pour résumer :

 

flux :

stdin

 
 

opération :

readf

 
 

données :

une information

 
 

cible :

 ?

 

Le lieu où l'on stocke les données est spécifié par l'adresse d'une variable. L'adresse d'une variable est l'endroit exact dans la mémoire de l'ordinateur où sa valeur est stockée.

En D, le caractère & qui est tapé avant le nom est l'adresse de ce que le nom représente. Par exemple, l'adresse de ce que stocke nombreEtudiants est &nombreEtudiants. Ici, &nombreEtudiants peut être lu comme : « l'adresse de nombreEtudiants » et est la pièce manquante qui va remplacer le point d'interrogation ci-dessus :

 

flux :

stdin

 
 

opération :

readf

 
 

données :

une information

 
 

cible :

l'endroit de la variable nombreEtudiants

 

Taper un & devant un nom signifie pointer vers ce que le nom représente. Ce concept est le fondement des références et des pointeurs que nous verrons dans les chapitres suivants.

Je laisserai une particularité à propos de l'utilisation de readf pour plus tard ; pour l'instant, acceptons comme une règle que le premier argument de readf doit être "%s" :

 
Sélectionnez
readf("%s", &nombreEtudiants);

Comme expliqué plus bas, dans la plupart des cas, il devrait y avoir une espace : " %s".

"%s" indique que les données devraient automatiquement être converties d'une manière qui convient pour le type de la variable. Par exemple, quand les caractères '4' et '2' sont lus et enregistrés dans une variable de type int, ils sont convertis vers la valeur entière 42.

Le programme ci-dessous demande à l'utilisateur d'entrer le nombre d'étudiants. Vous devez appuyer sur la touche "Entrée" après avoir tapé les données.

 
Sélectionnez
import std.stdio;
void main()
{
    write("Combien y a-t-il d'étudiants ici&#160;? ");
   /*
    * La définition de la variable qui va être utilisé pour
    * enregistrer l'information qui est lue depuis l'entrée.
    */
    int nombreEtudiants;
    // Stockage de l'entrée dans cette variable.
    readf("%s", &nombreEtudiants);
    writeln("J'ai obtenu : il y a ", nombreEtudiants, " étudiants.");
}

13-1. Sauter les caractères blancs

Même la touche Entrée sur laquelle nous appuyons après avoir tapé les données est stockée comme un code spécial et est placée dans le flux stdin. C'est utile pour les programme pour détecter si l'information a été entrée sur une seule ligne ou plusieurs lignes.

Même si c'est utile de temps en temps, de tels codes ne sont en général pas important pour le programme et doivent être retirés depuis l'entrée. Autrement, ils bloquent l'entrée et empêchent de lire d'autres données.

Pour voir ce problème dans un programme, lisons également le nombre d'enseignants depuis l'entrée :

 
Sélectionnez
import std.stdio;
void main()
{
    write("Combien y a-t-il d'étudiants ici ? ");
    int nombreEtudiants;
    readf("%s", &nombreEtudiants);
    write("Combien y a-t-il d'enseignants ici ? ");
    int nombreEnseignants;
    readf("%s", &nombreEnseignants);
    writeln("J'ai obtenu: Il y a ", nombreEtudiants, " étudiants",
            " et ", nombreEnseignants, " enseignants.");
}

Malheureusement, le programme reste bloqué quand il doit lire le second entier :

 
Sélectionnez
Combien y a-t-il d'étudiants ici ? 100Combien y a-t-il d'enseignants ici ? 20&#8592; Le programme reste bloqué ici

Même si l'utilisateur entre un nombre d'enseignants comme 20, le ou les codes spéciaux qui représentent la touche entrée sur laquelle on a appuyé quand on a entré 100 sont toujours dans le flux d'entrée et le bloquent. Les caractères qui sont apparus dans le flux d'entrée sont à peu près ceux-là :

 
Sélectionnez
100<Code d'entrée>20<code d'entrée>

Le code d'entrée qui bloque l'entrée est marqué en rouge.

La solution à utiliser est l'espace avant %s pour indiquer que le code d'entrée qui apparaît avant la lecture du nombre d'enseignants n'est pas important : " %s". Sans cela, les espaces qui sont dans la chaîne de formatage sont utilisés pour lire et ignorer 0 ou plus de caractères invisibles qui bloqueraient l'entrée. De tels caractères comprennent le caractère espace, le ou les codes qui représentent la touche Entrée, le caractère tabulation, etc. et ils sont appelés les caractères blancs (whitespace).

Comme règle générale, vous pouvez utiliser " %s" pour n'importe quelle donnée lue depuis l'entrée. Le programme précédent marche comme attendu avec les changements suivants :

 
Sélectionnez
// ...
    readf(" %s", &nombreEtudiants);
// ...
    readf(" %s", &nombreEnseignants);
// ...

La sortie :

 
Sélectionnez
Combien y a-t-il d'étudiants ici ? 100
Combien y a-t-il d'enseignants ici ? 20
J'ai obtenu : Il y a 100 étudiants et 20 enseignants.

13-2. Informations additionnelles

Les lignes qui commencent par // sont utiles pour les lignes de commentaires simples. Pour écrire plusieurs lignes comme un seul commentaire, il faut entourer les lignes par /* et */.

Pour commenter du code et même d'autres commentaires, utilisez /+ et +/ :

 
Sélectionnez
/+
       // un commentaire simple d'une ligne
       /*
          Un commentaire
          sur plusieurs lignes
       */
       Un bloc de commentaire qui comprends d'autres commentaires
    +/

La plupart des caractères blancs dans le code source n'ont pas d'importance. Écrire des expressions longues sur plusieurs lignes ou ajouter des espaces en plus pour rendre le code plus lisible est une bonne pratique. Malgré tout, tant que la syntaxe est respectée, les programmes peuvent être écrits sans aucun caractère blanc additionnel :

 
Sélectionnez
import std.stdio;void main(){writeln("Difficile à lire !");}

Il est difficile de lire un code source qui présente si peu de caractères blancs.

13-3. Exercice

Entrez un caractère non numérique quand le programme attend des valeurs entières et observez que le programme ne fonctionne pas correctement.

SolutionLire depuis l'entrée - Correction.

14. Instruction if

Nous avons appris que ce que fait le programme est effectué par les expressions. Toutes les expressions de tous les programmes que nous avons vu jusqu'à maintenant ont commencé avec la fonction main et étaient exécutés jusqu'à la fin de main.

Les instructions, d'un autre côté, sont des éléments qui affectent l'exécution des expressions. Les instructions ne produisent pas de valeurs et n'ont pas d'effets de bords en elles-mêmes. Elles déterminent si et dans quel ordre les expressions sont exécutées. Les instructions utilisent parfois des expressions logiques pour prendre de telles décisions.

d'autres langages de programmation peuvent avoir des définitions différentes pour les expressions et les instructions, alors que d'autres peuvent ne pas faire de distinction du tout.

14-1. Le bloc if et sa portée

L'instruction if détermine si une ou plusieurs expressions doivent être exécutées. Elle prend cette décision en évaluant une expression logique. Elle a le même sens que le mot français "si" dans la phrase "s'il y a du café alors je boirai du café".

if prend une expression logique entre parenthèses. Si la valeur de cette expression logique est true, alors il exécute les expressions qui sont dans les accolades qui suivent. À l'inverse, si l'expression logique est false, les expressions entre accolades ne seront pas exécutées.

L'étendue entre accolades est appelée bloc.

Voici la syntaxe de l'instruction if :

 
Sélectionnez
if (une_expression_logique)
{
   // ... La ou les expressions à exécuter si l'expression logique est vraie
}

Par exemple, la construction qui représente "s'il y a du café alors boire le café et laver la tasse" peut être écrite comme suit :

 
Sélectionnez
import std.stdio;
 
void main()
{
   bool ilyaDuCafé = true;
 
   if (ilyaDuCafé) {
      writeln("Boire le café");
      writeln("Laver la tasse");
   }
}

Si la valeur de ilyaDuCafé était false, alors les expressions qui sont dans le bloc seraient ignorées et le programme n'afficherait rien.

Notez également qu'ouvrir les accolades sur la même ligne que le mot clé if est un style de codage répandu.

14-2. Le bloc else et sa portée

Il y a parfois des opérations à exécuter quand l'expression logique de l'instruction if est fausse. Par exemple, il y a toujours une opération à exécuter dans une décision du type « s'il y a du café, je boirai du café, sinon je boirai du thé ».

Les opérations à exécuter dans le cas false sont placées dans un bloc après le mot clé else :

 
Sélectionnez
if (une_expression_logique)
{
   // ... expression(s) à exécuter si vrai
}
else
{
   // ... expression(s) à exécuter si faux
}

Par exemple, en supposant qu'il y a toujours du thé :

 
Sélectionnez
if(ilyaDuCafé) {
   writeln("Boire du café");
} else {
   writeln("Boire du thé");
}

Dans cet exemple, soit la première, soit la deuxième chaîne de caractère sera affichée selon la valeur de ilyaDuCafé.

Notez également comment les accolades sont placées autour de else ; c'est aussi un style de codage répandu.

else lui-même n'est pas une instruction mais une clause optionnelle de l'instruction if ; elle ne peut pas être utilisée toute seule.

14-3. Utilisez toujours les accolades pour entourer les blocs

Ce n'est pas recommandé, mais il est possible d'omettre les accolades s'il n'y a qu'une instruction dans un bloc. Comme les blocs if et else qui précèdent n'ont qu'une instruction, ce code peut être écrit comme suit :

 
Sélectionnez
if (ilyaDuCafé)
  writeln("Boire du café");
 
else
  writeln("Boire du thé");

Les programmeurs les plus expérimentés utilisent des accolades même pour une seule instruction (un des exercices de ce chapitre est à propos de leur omission). J'insiste sur cette règle de toujours utiliser des accolades parce que je vais maintenant vous montrer le seul cas où il est préférable d'omettre les accolades.

14-4. La chaîne "if, else if, else"

Une des forces des instructions et des expressions est la possibilité de les utiliser de manière plus complexe. En plus des expressions, les blocs peuvent contenir d'autres instructions. Par exemple, un bloc else peut contenir une instruction if. Combiner les instructions et les expressions nous permet de réaliser des programmes qui se comportent selon ce pour quoi ils sont écrits.

Ce qui suit est un code plus complexe écrit selon la supposition qu'aller à un bon troquet en vélo est mieux que marcher vers un mauvais troquet :

 
Sélectionnez
if (ilyaDuCafé) {
   writeln("Boire du café à la maison");
 
} else {
 
   if (ilyaUnVélo) {
      writeln("Aller au bon endroit");
 
   } else {
      writeln("marcher vers le mauvais endroit");
   }
}

Le code ci-dessus représente la phrase : « s'il y a du café, boire à la maison ; sinon, s'il y a un vélo, aller à la bonne place ; sinon marcher vers le mauvais endroit ».

Compliquons cette décision : au lieu de devoir marcher au mauvais endroit, essayer d'abord le voisin :

 
Sélectionnez
if (ilyaDuCafé) {
   writeln("Boire du café à la maison");
 
} else {
 
   if (ilyaUnVélo) {
      writeln("Aller au bon endroit");
 
   } else {
 
      if (voisinEstChezLui) {
         writeln("Prendre le café chez le voisin");
 
      } else {
         writeln("Marcher vers le mauvais endroit");
      }
   }
}

Des décisions comme "si ce cas, sinon si cet autre cas, sinon si cet autre autre cas, etc." sont communes dans les programmes. Malheureusement, quand la règle de toujours utiliser des accolades est suivie aveuglément, le code fini par avoir trop d'espacement horizontal et vertical : en ignorant les lignes vides, les 3 instructions if et les 4 expressions writeln ci-dessus occupent un total de 13 lignes.

Pour écrire de telles constructions d'une façon plus compacte, quand un bloc else contient seulement une instruction if, les accolades de ce bloc else sont omises.

Je laisse le code suivant mal écrit en tant qu'étape intermédiaire avant de le montrer sous une meilleure forme. Aucun code ne devrait être écrit comme cela. (Je le laisse également non coloré pour indiquer qu'il est inacceptable écrit de cette manière).

Ce qui suit est ce à quoi le code ressemble après avoir supprimé les accolades des deux blocs else qui ne contiennent qu'une instruction if :

 
Sélectionnez
if (ilyaDuCafé) {
   writeln("Boire du café à la maison");
 
 } else
 
   if (ilyaUnVélo) {
         writeln("Aller en vélo au bon endroit");
 
   } else
 
         if (voisinEstChezLui) {
             writeln("Prendre le café chez le voisin");
 
         } else {
             writeln("Marcher jusqu'au mauvais endroit");
         }

Si maintenant nous déplaçons ces instructions if sur la même ligne que le else qui les précède et que nous nettoyons le code, nous finissons par avoir quelque chose de plus lisible :

 
Sélectionnez
if (ilyaDuCafé) {
    writeln("Boire du café à la maison");
 
} else if (ilyaUnVélo) {
    writeln("Aller en vélo au bon endroit");
 
} else if (voisinEstChezLui) {
    writeln("Prendre le café chez le voisin");
 
} else {
    writeln("Marcher jusqu'au mauvais endroit");
}

Supprimer les accolades permet au code d'être plus compact et de regrouper les expressions pour une meilleure lisibilité. Les expressions logiques, l'ordre dans lequel elles sont évaluées, et les opérations qui sont exécutées quand elle sont vraies sont maintenant plus faciles à voir d'un coup d'œil.

Cette structure de programmation courante est appelée la chaîne "if, else if, else".

14-5. Exercices

  • Puisque l'expression logique ci-dessous est vraie, nous pourrions nous attendre à ce que le programme boive de la limonade et lave le verre.
 
Sélectionnez
import std.stdio;
 
void main()
{
    bool ilyaDeLaLimonade = true;
 
    if (ilyaDeLaLimonade) {
        writeln("Boire de la limonade");
        writeln("Laver le verre");
 
    } else
        writeln("Manger de la tarte");
        writeln("Laver l'assiette");
}
  1. Mais si vous lancez ce programme, vous verrez qu'il lave aussi l'assiette :

     
    Sélectionnez
    Boire de la limonade
    Laver le verre
    Laver l'assiette
  2. Pourquoi
  • Écrivez un programme qui joue à un jeu avec l'utilisateur (évidemment avec de la confiance). L'utilisateur lance un dé et entre sa valeur. L'utilisateur ou le programme gagne selon la valeur du dé :

     

    Valeur du dé

    Sortie du programme

     
     

    1

    Vous gagnez

     
     

    2

    Vous gagnez

     
     

    3

    Vous gagnez

     
     

    4

    Je gagne

     
     

    5

    Je gagne

     
     

    6

    Je gagne

     
     

    N'importe quelle autre valeur

    ERREUR : Valeur non valide

     
  • Bonus : faire en sorte que ce programme mentionne aussi la valeur quand celle-ci est invalide. Par exemple :
  • Changez le jeu pour que l'utilisateur entre une valeur entre 1 et 1000. Maintenant, l'utilisateur gagne quand la valeur est dans l'intervalle 1-500 et l'ordinateur gagne quand la valeur est dans l'intervalle 501-1000. Est-ce que le programme précédent peut être facilement modifié pour fonctionner de cette manière.

SolutionsL'instruction if - Correction.

15. Boucle while (Tant que)

La boucle while est similaire à l'instruction if et fonctionne essentiellement comme une instruction if répétée. Tout comme if, while prend aussi une expression logique et évalue le bloc quand l'expression logique est vraie. La différence est que l'instruction while évalue l'expression et exécute le bloc encore, tant que l'expression est vraie, pas qu'une fois. Répéter un bloc de code de cette manière est appelé « boucler ».

Voici la syntaxe de l'instruction while :

 
Sélectionnez
while (une_expression_logique)
{
   // ... expression(s) à exécuter tant que c'est vrai
}

Par exemple, le code qui représente « manger des cookies tant qu'il y a un cookie » peut être codé comme ceci :

 
Sélectionnez
import std.stdio;
 
void main()
{
    bool ilyaUnCookie = true;
 
    while (ilyaUnCookie) {
      writeln("Prendre un cookie");
      writeln("Le manger");
    }
}

Ce programme continuera à répéter la boucle indéfiniment parce que la valeur de ilyaUnCookie n'est jamais changée et est toujours true.

while est utile quand la valeur d'une expression change pendant l'exécution du programme. Pour voir ceci, écrivons un programme qui demande un nombre à l'utilisateur tant que ce nombre est positif ou nul. Souvenez-vous que la valeur initiale de int est 0 :

 
Sélectionnez
import std.stdio;
 
void main()
{
    int nombre;
 
    while (nombre >= 0) {
      write("Veuillez entrer un nombre: ");
      readf(" %s", &nombre);
 
      writeln("Merci d'avoir entré ", nombre);
    }
 
    writeln("Sorti de la boucle");
}

Le programme remercie d'avoir donné un nombre et ne quitte la boucle que quand le nombre est strictement négatif.

15-1. L'instruction continue

Cette instruction commence l'itération suivante de la boucle directement, au lieu d'exécuter le reste des expressions du bloc.

Modifions le programme ci-dessus pour le compliquer un peu : au lieu de remercier pour n'importe quel nombre, n'acceptons pas 13. Le programme suivant ne remercie pas pour 13 parce que dans ce cas l'instruction continue fait que le programme va au début de la boucle et évalue à nouveau l'expression logique.

 
Sélectionnez
import std.stdio;
 
void main()
{
    int nombre;
 
    while (nombre >= 0) {
      write("Veuillez entrer un nombre: ");
      readf(" %s", &nombre);
 
      if (nombre == 13) {
            writeln("Désolé, nous ne pouvons pas accepter celui-là...");
            continue;
      }
 
      writeln("Merci d'avoir entré ", nombre);
    }
 
    writeln("Sorti de la boucle");
}

Nous pouvons définir le comportement de ce programme comme « prendre des nombres tant qu'ils sont supérieurs ou égaux à 0 mais ignorer 13 ».

15-2. L'instruction break

Parfois, il devient évident qu'il n'y a plus besoin de rester dans la boucle. break permet au programme de sortir de la boucle directement. Le programme suivant sort de la boucle dès qu'il trouve un nombre spécial :

 
Sélectionnez
import std.stdio;
 
void main()
{
    int nombre;
 
    while (nombre >= 0) {
      write("Veuillez entrer un nombre : ");
      readf(" %s", &nombre);
 
      if (nombre == 42) {
         writeln("TROUVÉ !");
         break;
      }
 
      writeln("Merci d'avoir entré ", nombre);
    }
 
    writeln("Sorti de la boucle");
}

Nous pouvons résumer ce comportement comme : «Prendre des nombres tant qu'il sont positifs ou nuls jusqu'à ce que le nombre soit 42 ».

15-3. Boucle infinie

Parfois, l'expression logique est intentionnellement la constante true. L'instruction break est une manière commune de sortir de telles boucles infinies.

Le programme suivant affiche un menu dans une boucle infinie ; le seul moyen de sortir de la boucle est l'instruction break :

 
Sélectionnez
import std.stdio;
 
void main()
{
    /* Boucle infinie, parce que l'expression logique est toujours vraie */
    while (true) {
      write("0:Quitter, 1:Turc, 2:Français - Votre choix ? ");
 
      int choix;
      readf(" %s", &choix);
 
      if (choix == 0) {
         writeln("À bientôt...");
         break;   // La seule sortie de cette boucle
 
      } else if (choix == 1) {
         writeln("Merhaba!");
 
      } else if (choix == 2) {
         writeln("Bonjour !");
 
      } else {
         writeln("Je ne connais pas ce choix... :/");
      }
    }
}

les exceptions peuvent également terminer une boucle infinie. Nous verrons les exceptions dans un chapitre suivant.

15-4. Exercices

  • Le programme suivant est conçu pour rester dans la boucle tant que l'entrée est 3, mais il y a un bogue : il ne demande jamais d'entrer quoi que ce soit :
 
Sélectionnez
import std.stdio;
 
void main()
{
    int nombre;
 
    while (nombre == 3) {
        write("Nombre ? ");
        readf(" %s", &nombre);
    }
}
  1. Corrigez le bogue. Le programme devrait rester dans la boucle tant que l'entrée est 3.
  2. Une fois que le programme obtient un nombre valide de la part de Anna, il doit demander des nombres à Bill jusqu'à ce qu'il devine le nombre qu'Anna a donné.
  • Faire en sorte que l'ordinateur aide Anna et Bill à jouer à un jeu. D'abord, l'ordinateur demande un nombre à Anna entre 1 et 10. Le programme ne doit pas accepter d'autre nombre, il doit redemander jusqu'à ce qu'un nombre correct soit donné.

les nombres qu'Anna entre restent évidemment affichés à l'écran et peuvent être vus par Bill :o). Ignorons ce détail et écrivez le programme comme un exercice qui sert à pratiquer l'instruction while.

SolutionLa boucle while - Correction.

16. Nombres entiers et opérations arithmétiques

Nous avons vu que les instructions if et while permettent aux programmes de prendre des décisions en utilisant le type bool sous forme d'expressions logiques. Dans ce chapitre, nous allons voir les opérations arithmétiques sur les types entiers du D. Ces fonctionnalités nous permettrons d'écrire des programmes beaucoup plus utiles.

Même si les opérations arithmétiques font partie de notre vie quotidienne et sont en fait simples, il y a des concepts très importants dont un programmeur doit être conscient pour produire des programmes corrects : taille en bits d'un type, débordements, soupassements, et troncages.

Avant d'aller plus loin, je voudrais résumer les opérations arithmétiques dans le tableau suivant comme référence :

Opérateur

Effet

Exemple

++

incrémente

++variable

--

décrémente

--variable

+

fait la somme de deux valeurs

premier + second

-

soustrait second à premier

premier - second

*

fait le produit de deux valeurs

premier * second

/

divise premierpar second

premier / second

%

donne le reste de la division de premier par second

premier % second

^^

élève premier à la puissance second (multiplie premier par lui-même second fois)

premier ^^ second

La plupart de ces opérateurs ont leurs homologues qui ont un signe = collé à eux : +=, -=, *=, %=, ^^=. La différence avec ces opérateurs est qu'ils affectent le résultat à la variable à gauche de l'opérateur :

 
Sélectionnez
variable += 10;

Cette expression ajoute la valeur de variable et 10 et enregistre le résultat dans variable. À la fin, la valeur de variable est augmentée de 10. C'est l'équivalent de l'expression :

 
Sélectionnez
variable = variable + 10;

Je souhaiterai aussi résumer ici trois concepts importants avant de les développer plus.

  • Dépassement : toutes les valeurs ne peuvent pas entrer dans un type, dans certain cas il y a dépassement (ou débordement). Par exemple, une variable de type ubyte ne peut contenir que des valeurs entre 0 et 255 ; quand on lui affecte 260, la valeur déborde et la variable contient 4.
  • Soupassement : de façon similaire, des valeurs ne peuvent pas être plus petites que la valeur minimum qu'un type peut contenir.
  • Troncage : les types entiers ne peuvent pas avoir de valeurs avec parties fractionnaires. Par exemple, la valeurs de l'expression entière 1/2 n'est pas 1,5 mais 1.

16-1. Information supplémentaire

Nous rencontrons des opérations arithmétiques quotidiennement sans trop de surprises : si une baguette est à 1€, deux baguettes sont à 2€ ; si quatre sandwichs sont à 15€, un sandwich est à 3,75€, etc.

Malheureusement, les choses ne sont pas aussi simple avec les opérations arithmétiques des ordinateurs. Si on ne comprend pas comment les valeurs sont stockées dans un ordinateur, on peut être surpris de voir que la dette d'une compagnie est réduite à 1,7 milliards de dollars quand elle emprunte 3 nouveaux milliards en plus de sa dette existante de 3 milliards de dollars !
Ou quand, alors qu'une boîte de glace satisfait 4 enfants, une opération arithmétique peut prétendre que deux boîtes de glace suffiraient à 11 enfants !

Les programmeurs doivent comprendre comment les entiers sont stockés dans l'ordinateur.

16-1-1. Types entiers

Les types entier sont les types qui ne peuvent garder que des valeurs entières comme -2, 0, 10, etc. Ces types ne peuvent pas avoir de parties fractionnaires comme dans le nombre 2,5. Les types entiers que nous avons vu dans le chapitre sur les types fondamentauxTypes fondamentaux sont :

Type

Nombre de bits

Valeur initiale

byte

8

0

ubyte

8

0

short

16

0

ushort

16

0

Int

32

0

uint

32

0

long

64

0L

ulong

64

0L

Le u au début du type veut dire « unsigned » (non signé) et indique que de tels types ne peuvent pas avoir de valeurs en dessous de zéro.

16-1-2. Nombre de bits d'un type

Dans les systèmes informatiques d'aujourd'hui, la plus petite unité d'information est appelée un bit. Au niveau physique, un bit est représenté par des signaux électriques à certains endroits des circuits d'un ordinateur. Un bit peut être dans un ou deux états qui correspondent à la présence et à l'absence de signaux électriques au point qui définit un bit particulier. Ces deux états sont définis arbitrairement aux valeurs 0 et 1. De ce fait, un bit peut avoir une de ces deux valeurs.

Comme il n'y a pas 36 idées pouvant être représenté par seulement deux états, bit n'est pas un type très utile. Il ne peut être utile que pour des idées avec deux états comme « pile ou face »ou une lumière qui peut être allumée ou éteinte.

Si on considère deux bits à la fois, le nombre total d'informations qui peuvent être représentées est multiplié. Comme chaque bit vaut 0 ou 1, il y a un nombre total de 4 états possibles. En supposant que le chiffre de gauche représente le premier bit et que le chiffre de droite représente le deuxième bit, ces états sont 00, 01, 10 et 11. Ajoutons encore un bit pour voir mieux cet effet : 3 bits peuvent être dans 8 états différents : 000, 001, 010, 011, 100, 101, 110, 111. Comme on peut le voir, chaque bit ajouté double le nombre total d'états qui peuvent être représentés.

Les valeurs auxquelles ces 8 états correspondent sont définies par conventions. Le tableau suivant montre ces valeurs pour les représentations signées et non signées de 3 bits :

Etat binaire

Valeur non signée

Valeur signée

000

0

0

001

1

1

010

2

2

011

3

3

100

4

-4

101

5

-3

110

6

-2

111

7

-1

On peut écrire le tableau suivant en ajoutant plus de bits :

Bits

nombre de Valeur distinctes

Type D

Valeur minimale

Valeur maximale

1

2

     

2

4

     

3

8

     

4

16

     

5

32

     

6

64

     

7

128

     

8

256

byte

-128

127

ubyte

0

255

...

16

65 536

short

-32768

32767

ushort

0

65535

...

32

4 294 967 296

int

-2147483648

2147483647

uint

0

4294967295

...

64

18 446 744 073 709 551 616

long

-9223372036854775808

9223372036854775807

ulong

0

18446744073709551615

...

J'ai sauté beaucoup de lignes dans le tableau, et indiqué les versions signées et non signées des type D qui ont le même nombre de bits sur la même ligne (par exemple int et uint sont tous les deux sur la ligne des 32 bits).

16-1-3. Choisir un type

Comme un type 3 bits ne peut avoir que 8 valeurs distinctes, il ne peut représenter que des idées comme une face d'un dé ou le nombre de jours dans une semaine. (Ce n'est qu'un exemple, il n'y a pas de type 3 bits en D).

D'un autre côté, même si uint est un type très large, il ne peut pas représenter l'idée d'un numéro qui identifierait chaque personne vivante puisque sa valeur maximum est inférieure à la population mondiale de 7 milliards. long et ulong seraient plus qu'assez pour représenter beaucoup d'idées.

En règle générale, tant qu'il n'y a aucune raison spécifique de ne pas le faire, vous pouvez utiliser int pour les valeurs entières.

16-1-4. Dépassement

Le fait que les types ne peuvent avoir qu'un nombre limité de valeurs peut causer des effets inattendus. Par exemple, même si ajouter deux uint valant 3 milliards chacun devrait donner 6 milliards, comme la somme est plus grande que la valeur maximum qu'une variable uint peut stocker (environ 4 milliards), cette somme déborde. Sans aucun avertissement, seule la différence entre 6 et 4 milliards est stockée (un peu plus précisément, 6 moins 4,3 milliards).

16-1-5. Troncage

Comme les entiers ne peuvent pas avoir de valeur avec des parties fractionnaires, ils perdent la partie après la virgule. Par exemple, en supposant qu'une boîte de glace satisfait 4 enfants, même si 11 enfants auraient besoin de 2,75 boîtes, 2,75 ne peut être que stocké comme 2 dans un type entier.

Nous verrons des techniques basiques pour aider à réduire les effets de débordements, soupassements et troncages plus loin dans le chapitre.

16-1-6. .min et .max

Nous utiliserons les propriétés min et max plus bas, que nous avons vues dans le chapitre sur les Types Fondamentaux. Ces propriétés donnent les valeurs minimale et maximale qu'un type entier peut avoir.

16-1-7. Incrémentation: ++

Cet opérateur est utilisé avec une seule variable (plus généralement, avec une seule expression) et est écrite avant le nom de la variable.

Il incrémente la valeur de cette variable de 1 :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int nombre = 10;
   ++nombre;
   writeln("Nouvelle valeur : ", nombre);
}

Sortie :

 
Sélectionnez
Nouvelle valeur : 11

L'opérateur d'incrémentation est équivalent à l'utilisation de l'opérateur ajouter-et-affecter avec la valeur 1 :

 
Sélectionnez
nombre += 1;      // pareil que ++nombre

Si le résultat de l'opérateur d'incrémentation est plus grand que la valeur maximum du type de la variable, le résultat déborde et devient la valeur minimale. On peut voir cet effet en incrémentant une variable qui a la valeur int.max :

 
Sélectionnez
import std.stdio;
 
void main()
{
    writeln("valeur int maximum   : ", int.min);
    writeln("valeur int maximum   : ", int.max);
 
    int nombre = int.max;
    writeln("avant l'incrémentation : ", nombre);
    ++nombre;
    writeln("après l'incrémentation : ", nombre);
}

La valeur devient int.min après l'incrémentation :

Sortie :

 
Sélectionnez
valeur int minimum   : -2147483648
valeur int maximum   : 2147483647
avant l'incrémentation : 2147483647
après l'incrémentation : -2147483648

C'est une observation très importante, parce que la valeur change du maximum au minimum après une incrémentation et sans aucun avertissement ! Ce phénomène est appelé débordement (overflow). Nous verrons des effets similaires avec d'autres opérations.

16-1-8. Décrémentation : --

Cet opérateur est similaire à l'opérateur d'incrémentation ; la différence est que la valeur est diminuée de 1 :

 
Sélectionnez
--nombre;   // La valeur est diminuée de 1

L'opération de décrémentation est équivalente à l'utilisation de l'opérateur soustraire-puis-affecter avec la valeur 1 :

 
Sélectionnez
nombre -= 1;      // pareil que --nombre

De manière similaire à l'opérateur ++, si la valeur de la variable est la valeur minimum, elle devient la valeur maximum. Ce phénomène est appelé soupassement (underflow).

16-1-9. Addition : +

Cet opérateur est utilisé avec deux expressions et ajoute leurs valeurs :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int premier = 12;
   int second = 100;
 
   writeln("Résultat : ", premier + second);
   writeln("Avec une expression constante : ", 1000 + second);
}

Sortie :

 
Sélectionnez
Résultat : 112
Avec une expression constante : 1100

De même, si le résultat est plus grand que la somme des deux expressions, il déborde et devient inférieur aux deux expressions :

 
Sélectionnez
import std.stdio;
 
void main()
{
   // 3 milliards chacun
   uint premier = 3000000000;
   uint second = 3000000000;
 
   writeln("valeur minimum de uint : ", uint.max);
   writeln("               premier : ", premier);
   writeln("                second : ", second);
   writeln("                 somme : ", premier + second);
   writeln("DÉBORDEMENT ! Le résultat n'est pas 6 milliards !");
}

Sortie :

 
Sélectionnez
valeur minimum de uint : 4294967295
               premier : 3000000000
                second : 3000000000
                 somme : 1705032704
DÉBORDEMENT ! Le résultat n'est pas 6 milliards !

16-1-10. Soustraction : -

Cet opérateur est utilisé avec deux expressions et donne la différence entre la première et la seconde :

 
Sélectionnez
import std.stdio;
 
void main()
{
    int nombre_1 = 10;
    int nombre_2 = 20;
 
    writeln(nombre_1 - nombre_2);
    writeln(nombre_2 - nombre_1);
}

Sortie :

 
Sélectionnez
-10
10

Si le résultat d'une soustraction est inférieur à zéro, ce qu'on obtient en stockant ce résultat dans un type non signé est encore une fois surprenant. Réécrivons le programme en utilisant le type uint :

 
Sélectionnez
import std.stdio;
 
void main()
{
   uint nombre_1 = 10;
   uint nombre_2 = 20;
 
   writeln("PROBLÈME ! uint ne peut pas stocker de valeur négatives :");
   writeln(nombre_1 - nombre_2);
   writeln(nombre_2 - nombre_1);
}

Sortie :

 
Sélectionnez
PROBLÈME ! uint ne peut pas stocker de valeur négatives :
4294967286
10

On peut recommander d'utiliser des types signés pour représenter des choses qui pourraient être soustraites. Tant qu'il n'y a pas de raison spécifique de ne pas le faire, vous pouvez choisir int.

16-1-11. Multiplication : *

Cet opérateur multiplie les valeurs de deux expressions :

 
Sélectionnez
import std.stdio;
 
void main()
{
   uint nombre_1 = 6;
   uint nombre_2 = 7;
 
   writeln(nombre_1 * nombre_2);
}

Sortie :

 
Sélectionnez
42

Le résultat est encore une fois sujet au débordement.

16-1-12. Division : /

Cet opérateur divise la première expression par la seconde. Comme les types entiers ne peuvent pas avoir de partie fractionnaire, la partie fractionnaire est abandonnée. Cet effet est appelé troncage. De ce fait, le programme suivant affiche 3 et non 3.5.

 
Sélectionnez
import std.stdio;
 
void main()
{
   writeln(7 / 2);
}

Sortie :

 
Sélectionnez
3

Pour les calculs où les parties fractionnaires importent, les types à virgule flottante doivent être utilisés à la place des entiers. Nous verrons les types flottants dans le chapitre suivant.

16-1-13. Modulo : %

Cet opérateur divise la première expression par la seconde et donne le reste de cette division :

 
Sélectionnez
import std.stdio;
 
void main()
{
   writeln(10 % 6);
}

Sortie :

 
Sélectionnez
4

Une utilisation fréquente de cet opérateur est de déterminer si un nombre est pair ou impair. Comme le reste de la division d'un nombre par 2 est toujours 0 et le reste de la division d'un nombre impair par 2 est toujours 1, comparer la valeur à 0 est suffisant pour déterminer la parité d'un nombre :

 
Sélectionnez
if ((nombre % 2) == 0) {
   writeln("nombre pair");
 
} else {
   writeln("nombre impair");
}

16-1-14. Puissance : ^^

Cet opérateur élève la première expression à la puissance de la seconde. Par exemple, pour élever 3 à la puissance 4 :

 
Sélectionnez
import std.stdio;
 
void main()
{
   writeln(3 ^^ 4); // équivalent à 3*3*3*3
}

Sortie :

 
Sélectionnez
81

16-1-15. Opérations arithmétiques avec affectation

Tous les opérateurs qui prennent deux expressions ont un équivalent d'affectation. Ces opérateurs affectent le résultat à l'expression qui est à sa gauche :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int nombre = 10;
 
   nombre += 20;   // pareil que nombre = nombre + 20 ; maintenant 30
   nombre -= 5;    // pareil que nombre = nombre - 5 ;  maintenant 25
   nombre *= 2;    // pareil que nombre = nombre * 2 ;  maintenant 50
   nombre /= 3;    // pareil que nombre = nombre / 3 ;  maintenant 16
   nombre %= 7;    // pareil que nombre = nombre % 7 ;  maintenant  2
   nombre ^^= 6;   // pareil que nombre = nombre ^^ 6 ; maintenant 64
 
   writeln(nombre);
}

Sortie :

 
Sélectionnez
64

16-1-16. Négation : -

Cet opérateur change le signe de la valeur de l'expression (négatif devient positif et inversement) :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int nombre_1 = 1;
   int nombre_2 = -2;
 
   writeln(-nombre_1);
   writeln(-nombre_2);
}

Sortie :

 
Sélectionnez
-1
2

Le type du résultat de cette opération est le même que le type de l'expression. Comme les types non signés ne peuvent pas stocker de valeurs négatives, utiliser cet opérateur sur des types non signés peut mener à des résultats surprenants :

 
Sélectionnez
uint nombre = 1;
writeln("négation : ", -nombre);

le type de -nombre est uint également, et celui-ci ne peut pas représenter des valeurs négatives :

Sortie :

 
Sélectionnez
negation: 4294967295

16-1-17. Signe plus : +

Cet opérateur n'a pas d'effet et n'existe que pour avoir une certaine symétrie avec l'opérateur de négation. Les valeurs positives restent positives et les valeurs négatives restent négatives :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int nombre_1 = 1;
   int nombre_2 = -2;
 
   writeln(+nombre_1);
   writeln(+nombre_2);
}

Sortie :

 
Sélectionnez
1
-2

16-1-18. Post-incrémentation : ++

Sauf s'il y a une très bonne raison de le faire, utilisez toujours l'opérateur d'incrémentation usuel (qui est également appelé opérateur de pré-incrémentation).

Contrairement à l'opérateur de pré-incrémentation, il est écrit après l'expression et incrémente également la valeur de l'expression de 1. La différence est que l'opérateur de post-incrémentation donne l'ancienne valeur de l'expression, et non la nouvelle. Pour voir cette différence, comparons-la avec l'opérateur de préincrémentation :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int incrémenté_normalement = 1;
   writeln(++incrémenté_normalement);  // affiche 2
   writeln(incrémenté_normalement);    // affiche 2
 
   int post_incrémenté = 1;
 
   // incrémenté, mais l'ancienne valeur est utilisée :
   writeln(post_incrémenté++);         // affiche 1
   writeln(post_incrémenté);           // affiche 2
}

Sortie :

 
Sélectionnez
2
2
1
2

L'instruction writeln(post_incrémenté++); ci-dessus est équivalente à ce code :

 
Sélectionnez
int ancienne_valeur = post_incrémenté;
++post_incrémenté;
writeln(ancienne_valeur);   // affiche 1

16-1-19. Post-décrémentation : --

sauf s'il y a une bonne raison, utilisez toujours l'opérateur de décrémentation usuel (aussi appelé opérateur de pré-décrémentation).

Se comporte de la même manière que l'opérateur de post-incrémentation mais décrémente.

16-1-20. Priorité opératoire

Les opérateurs que nous avons vus jusqu'alors ont toujours été utilisés tout seuls, avec seulement une ou deux expressions. Cependant, comme les expressions logiques, il est fréquent de combiner ces opérateurs pour former des expressions arithmétiques plus complexes :

 
Sélectionnez
int valeur     = 77;
int résultat = (((valeur + 8) * 3) / (valeur - 1)) % 5;

Comme pour les opérateurs logiques, les opérateurs arithmétiques obéissent aussi à des règles de priorité. Par exemple, l'opérateur * est prioritaire sur l'opérateur +. Pour cette raison, quand il n'y a pas de parenthèses (par exemple dans l'expression valeur + 8 * 3), l'opérateur * est évalué avant l'opérateur +. De ce fait, cette expression vaut valeur + 24, ce qui est différent de (valeur + 8) * 3.

Utiliser des parenthèses sert à la fois à assurer des résultats corrects et à communiquer le but d'un code aux programmeurs qui pourraient à l'avenir travailler sur le code.

16-1-21. Solution potentielle contre les débordements

Si le résultat d'une opération ne peut pas être contenu dans le type du résultat, alors il n'y a rien qui puisse être fait. Parfois, alors que le résultat final pourrait être contenu dans un certain type, les résultats intermédiaires pourraient déborder et mener à des résultats incorrects.

Par exemple, supposons que nous avons besoin de planter un pommier par 1000 m² d'une aire de 40 par 60 km. De combien de pommiers avons-nous besoin ?

Quand on résout ce problème sur papier, on voit que le résultat est kitxmlcodeinlinelatexdvp\frac{40000x60000}{1000}finkitxmlcodeinlinelatexdvp, ce qui vaut 2,4 millions de pommiers. Écrivons un programme qui effectue ce calcul :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int largeur   = 40000;
   int longueur = 60000;
   int airParArbre = 1000;
 
   int arbresNecessaires = largeur * longueur / airParArbre;
 
   writeln("Nombre d'arbres nécessaires : ", arbresNecessaires);
}

Sortie :

 
Sélectionnez
Nombre d'arbres nécessaires : -1894967

Pour ne pas mentionner le fait que ce n'est même pas proche du résultat, on obtient un nombre négatif ! Dans ce cas, le calcul intermédiaire largeur * longueur déborde et le calcul suivant / airParArbre donne un résultat incorrect.

Une façon de d'éviter le débordement dans cet exemple est de changer l'ordre des opérations :

 
Sélectionnez
int arbresNecessaires = largeur / airParArbre * longueur ;

Le résultat est maintenant correct :

 
Sélectionnez
Nombre d'arbres nécessaires : 2400000

La raison pour laquelle cette méthode fonctionne est le fait que chaque étape du calcul est contenu dans le type int.

Notez que ce n'est pas une solution complète parce que cette fois la valeur intermédiaire est sujette au troncage, qui pourrait affecter le résultat de façon significative dans certains ordres de calcul.

Une autre solution serait d'utiliser un type flottant à la place d'un type entier : float, double ou real.

16-1-22. Solution potentielle contre le troncage

Changer l'ordre des opérateurs peut aussi être une solution contre le troncage. Un exemple intéressant peut être vu en divisant et en multipliant une valeur avec le même nombre. On s'attendrait à ce que 10/9*9 donne 10, mais on obtient 9 :

 
Sélectionnez
import std.stdio;
 
void main()
{
   writeln(10 / 9 * 9);
}

Sortie :

 
Sélectionnez
9

Le résultat est correct quand le troncage est évité en changeant l'ordre des opérations :

 
Sélectionnez
writeln(10 * 9 / 9);

Sortie :

 
Sélectionnez
0

Encore une fois, ce n'est pas une solution complète: cette fois, le calcul intermédiaire pourrait être sujet au débordement. Utiliser un type flottant peut être une autre solution au troncage dans certains calculs.

16-1-23. Exercices

  • Écrire un programme qui demande deux entiers à l'utilisateur, affiche le quotient entier d'une division du premier et du second, et affiche aussi le reste. Par exemple, quand 7 et 3 sont entrés, le programme affiche l'équation suivante :
  1. Sortie :
 
Sélectionnez
7 = 3 * 2 + 1
  • Modifier le programme pour donner une sortie plus courte quand le reste est 0. Par exemple, quand 10 et 5 sont entrés, il ne devrait pas afficher "10 = 5 * 2 + 0" mais ceci :
 
Sélectionnez
10 = 5 * 2
  • Écrire une calculette simple qui supporte les 4 opérations arithmétiques basiques. L'utilisateur choisit l'opération à effectuer depuis un menu et le programme effectue l'opération sur les deux valeurs qui ont été entrées. Vous pouvez ignorer les débordements et les troncages dans ce programme.
  • Écrire un programme qui affiche les valeurs de 1 à 10, chacune sur une ligne propre, à l'exception de la valeur 7. N'utilisez pas des lignes répétés comme cela :

     
    Sélectionnez
    import std.stdio;
     
    void main()
    {
       // Ne faîtes pas cela !
       writeln(1);
       writeln(2);
       writeln(3);
       writeln(4);
       writeln(5);
       writeln(6);
       writeln(8);
       writeln(9);
       writeln(10);
    }
  • Imaginez plutôt une variable dont la valeur est incrémentée dans une boucle. Vous pourriez avoir besoin de l'opérateur != ici.

Solutions.

17. Types à virgule flottante

Dans le chapitre précédent, nous avons vu que malgré leur facilité d'utilisation, les opérations arithmétiques sur les entiers sont sujettes à des erreurs de programmations à cause des débordements, des soupassements et des troncages. Nous avons aussi vu que les entiers ne peuvent pas représenter des valeurs avec des parties fractionnaires comme 1,25.

Les types flottants sont spécialement conçus pour prendre en charge les parties fractionnaires. La "virgule" dans leur nom vient de la virgule qui sépare partie entière et partie fractionnaire et "flottant" fait référence à la manière avec laquelle ces types sont implémentés : la virgule flotte à droite ou à gauche de façon appropriée. (Ce détail n'est pas important pour l'utilisation de ces types).

De même que pour les entiers, nous devons voir des détails important dans ce chapitre. Avant tout, voici une liste de quelques aspects intéressants des types flottants :

  • Ajouter 0,001 un millier de fois n'est pas pareil qu'ajouter 1.
  • Utiliser les opérateur == et != avec des types flottants est une erreur dans la plupart des cas.
  • La valeur initiale d'un type flottant est .nan, et non 0. .nan ne peut pas être utilisé dans des expressions. Quand .nan est utilisé dans des opérations de comparaison, il n'est ni plus petit, ni plus grand que n'importe quelle valeur.
  • La valeur de débordement est .infinity et la valeur de soupassement est .infinity négatif.

Même si les types flottants sont plus utiles dans certains cas, ils ont des singularités que tous les programmeurs doivent connaître. Par rapport aux entiers, ils sont très bons pour éviter les troncages parce que leur but principal est de prendre en charge les valeurs fractionnaires. Comme n'importe quel autre type, basés sur un certain nombre de bits, ils sont également sujet aux dépassements et soupassements, mais comparés aux entiers, l'ensemble des valeurs qu'ils prennent en charge est vaste.

De plus, au lieu d'être silencieux dans le cas de dépassements et de soupassements, ils prennent les valeurs spéciales d'infini positif ou négatif.

Pour rappel, voici les types flottants :

Type

Nombre de bits

Valeur initiale

float

32

float.nan

double

64

double.nan

real

Au moins 64, peut-être plus (par exemple 80, selon ce que le matériel prend en charge)

real.nan

17-1. Attributs des types flottants

Les types flottants ont plus d'attributs que les autres types :

  • .stringof est le nom du type.
  • .sizeof est la longueur du type en octets (pour avoir le nombre de bits, il faut multiplier cette valeur par 8).
  • .max est la valeur maximale qu'un type peut stocker ; l'opposé de cette valeur est la valeur minimale que le type peut avoir.
  • .min_normal est la plus petite valeur normalisée que ce type peut avoir (le type peut stocker des valeurs plus petites que .min_normal mais la précision de ces valeurs est moins grande que la précision normale du type).
  • .dig (pour digits (chiffres)) indique le nombre de chiffres décimaux significatifs pour la précision du type.
  • .infinity est la valeur spéciale utilisée pour indiquer un dépassement ou un soupassement.

Notez que la valeur minimale d'un type flottant n'est pas .min mais l'opposé de .max. Par exemple, la valeur minimale de double est -double.max.

D'autres attributs des types flottants sont utilisés moins souvent. Vous pouvez tous les voir sur cette page (en anglais) : Properties for Floating Point Types.

Les propriétés des types flottants et leurs relations peuvent être vues sur un axe comme celui-ci :

Image non disponible

Les parties avec des tirets sont à l'échelle : le nombre de valeurs qui peuvent être représentées entre min_normal et 1 est égal au nombre de valeurs qui peuvent être représentées entre 1 et max. Cela veut dire que la précision de la partie fractionnaire des valeurs qui sont entre min_normal et 1 est très grande (c'est également vrai pour le côté négatif).

17-2. .nan

Nous avons déjà vu que .nan est la valeur par défaut des variables flottantes. .nan peut apparaître comme résultat d'une expression flottant n'ayant pas de sens. Par exemple, les expressions flottantes du programme suivant produisent toutes double.nan :

 
Sélectionnez
import std.stdio;
 
void main()
{
   double zero = 0;
   double infini = double.infinity;
 
   writeln("n'importe quelle expression avec nan : ", double.nan + 1);
   writeln("zero / zero                          : ", zero / zero);
   writeln("zero * infini                      : ", zero * infini);
   writeln("infini / infini                  : ", infini / infini);
   writeln("infini - infini                  : ", infini - infini);
}

17-3. Écrire des valeurs flottantes

Les valeurs flottantes peuvent simplement être écrites sans point décimal comme 123, ou avec un point décimal comme 12.3 (NdT : pour écrire un nombre à virgule, on n'utilise pas la virgule mais le point).

Les valeurs flottantes peuvent aussi être écrites avec la syntaxe flottante: 1.23e+4. La partie e+ dans cette syntaxe peut être lue comme « fois 10 puissance ». On lit 1.23e+4 comme ceci « 1.23 fois 10 puissance 4 », qui est pareil que « 1.23 fois 104 », qui est en fait pareil que 1.23×10000, ce qui vaut 12 300.

Si la valeur après e est négative, comme pour 5.67e-3, alors on lit « divisé par 10 puissance ». Ainsi, pour cet exemple, on lit « 5.67 divisé par 103 », ce qui est pareil que kitxmlcodeinlinelatexdvp\frac{5,67}{1000}finkitxmlcodeinlinelatexdvp, ce qui vaut 0.00567.

Les valeurs qui sont affichées par le programme suivant sont toutes dans le format flottant. Il affiche les propriétés des trois types :

 
Sélectionnez
import std.stdio;
 
void main()
{
   writeln("Type                       : ", float.stringof);
   writeln("Précision                  : ", float.dig);
   writeln("Valeur minimale normalisée : ", float.min_normal);
   writeln("Valeur maximale            : ", float.max);
   writeln("Valeur minimale            : ", -float.max);
   writeln();
   writeln("Type                       : ", double.stringof);
   writeln("Précision                  : ", double.dig);
   writeln("Valeur minimale normalisée : ", double.min_normal);
   writeln("Valeur maximale            : ", double.max);
   writeln("Valeur minimale            : ", -double.max);
   writeln();
   writeln("Type                       : ", real.stringof);
   writeln("Précision                  : ", real.dig);
   writeln("Valeur minimale normalisée : ", real.min_normal);
   writeln("Valeur maximale            : ", real.max);
   writeln("Valeur minimale            : ", -real.max);
}

La sortie du programme est celle-ci dans mon environnement. Comme real dépend du matériel (NdT : et du système qui tourne dessus), vous pouvez obtenir autre chose :

Sortie :

 
Sélectionnez
Type                       : float
Précision                  : 6
Valeur minimale normalisée : 1.17549e-38
Valeur maximale            : 3.40282e+38
Valeur minimale            : -3.40282e+38

Type                       : double
Précision                  : 15
Valeur minimale normalisée : 2.22507e-308
Valeur maximale            : 1.79769e+308
Valeur minimale            : -1.79769e+308

Type                       : real
Précision                  : 18
Valeur minimale normalisée : 3.3621e-4932
Valeur maximale            : 1.18973e+4932
Valeur minimale            : -1.18973e+4932

17-4. Observations

Comme on l'a vu dans le chapitre précédent ; la valeur maximum de ulong a 20 chiffres décimaux : 18 446 744 073 709 551 616. Cette valeur semble petite même comparée au plus petit des types flottants : float peut stocker des valeurs de l'ordre de 1038, par exemple 340 282 000 000 000 000 000 000 000 000 000 000 000.

La valeur maximale de real est de l'ordre de 104932, une valeur de plus de 4 900 chiffres décimaux !

Regardons la valeur minimale que double peut représenter avec une précision de 15 chiffres décimaux : 0.000...(il y a 300 zéros additionnels ici)...0000222507385850720.

17-5. Les débordements et les soupassements

Malgré leur capacité de représenter des valeurs très grandes, les types flottants peuvent aussi déborder ou soupasser. Les types flottants sont plus sûrs que les types entiers dans ce domaine parce que les dépassements et les soupassements ne sont pas ignorés. Les valeurs qui débordent deviennent .infinity et les valeurs qui soupassent deviennent -.infinity. Pour l'observer, augmentons la valeur de .max de 10%. Comme la valeur est déjà le maximum, ça débordera :

 
Sélectionnez
import std.stdio;
 
void main()
{
   real valeur = real.max;
 
   writeln("Avant        : ", valeur);
 
   // Ajouter 10% est équivalent à multiplier par 1.1
   valeur *= 1.1;
   writeln("10% ajoutés  : ", valeur);
 
   // Essayons de la diminuer en divisant par 2 :
   valeur /= 2;
   writeln("Divisé par 2 : ", valeur);
}

Une fois que la valeur déborde et devient real.infinity, elle y reste même après avoir été divisée par 2 :

Sortie :

 
Sélectionnez
Avant        : 1.18973e+4932
10% ajoutés  : inf
Divisé par 2 : inf

17-6. Précision

La précision est une idée omniprésente dans la vie de tous les jours, sans qu'on y prête toujours attention.. C'est le nombre de chiffres qui sont utilisés pour écrire une valeur. Par exemple, quand on dit que le tiers de 100 est 33, la précision est de 2 parce que 33 a deux chiffres. Quand la valeur est notée plus précisément comme 33.33, la précision est alors de 4 chiffres.

Le nombre de bits qu'a chaque type flottant n'affecte pas seulement sa valeur maximale, mais aussi sa précision. Plus il y a de bits, plus la précision est grande.

17-7. Il n'y a pas de troncage lors d'une division

Comme nous l'avons vu dans le chapitre précédent, les divisions entières ne conservent pas la partie fractionnaire du résultat :

 
Sélectionnez
int premier = 3;
int second = 2;
writeln(premier / second);

Sortie :

 
Sélectionnez
1

Les types flottants n'ont pas ce problème de troncage ; ils sont spécialement conçu pour préserver les parties fractionnaires :

 
Sélectionnez
double premier = 3;
double second = 2;
writeln(premier / second);

Sortie :

 
Sélectionnez
30/12/99

La précision de la partie fractionnaire dépend de la précision du type : real a la plus grande précision et float la plus petite.

17-8. Quel type utiliser

Sauf s'il y a une raison spécifique de ne pas le faire, vous pouvez utiliser double pour les valeurs flottantes. float a une précision faible mais parce qu'il est plus petit que les autres types, il peut être utile quand l'espace de stockage est limité.

D'un autre côté, comme la précision de real est plus grande que double sur le même matériel, il sera préférable pour des calculs de haute précision.

17-9. On ne peut pas représenter toutes les valeurs

On ne peut pas représenter certaines valeurs de la vie de tous les jours. Dans le système décimal que nous utilisons quotidiennement, les chiffres avant la virgule représentent les unités, les dizaines, les centaines, etc. et les chiffres après la virgule représente les dixièmes, les centièmes, les millièmes, etc.

Si une valeur est une combinaisons exacte de ces valeurs, elle peut être représentée précisément. Par exemple, du fait que 0.23 consiste en 2 dixièmes et 3 centièmes, cette valeur est représentée précisément. D'un autre côté, la valeur de kitxmlcodeinlinelatexdvp\frac{1}{3}finkitxmlcodeinlinelatexdvp ne peut pas être représenté précisément dans le système décimal, parce quelque soit le nombre de chiffres qui sont écrits, ce n'est jamais suffisant : 0,33333...

C'est très similaire pour les types flottants. Parce que ces types sont basés sur un certain nombre de bits, ils ne peuvent pas représenter toutes les valeurs.

La différence avec le système binaire que l'ordinateur utilise est que les chiffres avant la virgule sont les unités, les deuzaines, les quatraines, etc. et les chiffres après la virgules sont les moitiés, les quarts, les huitièmes, etc. Seules les valeurs qui sont des combinaisons exactes de ces chiffres peuvent être représentées précisément.

Par exemple, on ne peut représenter directement dans le système binaire utilisé par les ordinateurs une valeur telle que 0,1 (comme dans 10 centimes). Alors que cette valeur peut être représentée précisément dans le système décimal, sa représentation binaire ne se finit jamais et répète continuellement 4 chiffres : 0.0001100110011... (en utilisant l'écriture binaire, pas décimale). C'est toujours imprécis à un certain niveau, selon la précision du type flottant utilisé.

Le programme suivant montre ce problème. La valeur d'une variable est incrémentée de 0.001 un millier de fois dans une boucle. De façon étonnante, le résultat n'est pas 1 :

 
Sélectionnez
import std.stdio;
 
void main()
{
   float résultat = 0;
 
   // On s'attendrait à ce que résultat vaille 1 après avoir bouclé 1000 fois :
   while (résultat < 1) {
      résultat += 0.001;
   }
 
   // Vérifions
   if (résultat == 1) {
      writeln("Comme attendu: 1");
 
   } else {
      writeln("DIFFERENT: ", résultat);
   }
}

Sortie :

 
Sélectionnez
DIFFERENT: 1.00099

Parce que 0.001 ne peut pas être représenté précisément, cette imprécision affecte le résultat de multiples fois. La sortie suggère que la boucle a été répétée 1001 fois.

17-10. Comparer des valeurs flottantes

Nous avons vu les comparaisons suivantes pour les entiers : égal à (==), n'est pas égal à (!=), plus grand que (>), inférieur ou égal à (<=) et supérieur ou égal à (>=). Les types flottants ont beaucoup plus d'opérateurs de comparaison.

Comme la valeur spéciale .nan représente des valeurs flottantes invalides, il ne peut être comparé avec aucune autre valeur. Par exemple, il n'y a pas de sens à demander lequel de .nan ou de 1 est plus grand que l'autre.

Pour cette raison, les valeurs flottantes ne sont pas toutes ordonnées. Si deux valeurs sont non ordonnées, alors au moins une des valeurs est .nan

Le tableau suivant montre tous les opérateurs de comparaison sur les flottants. Tous les opérateurs sont binaires (ce qui veut dire qu'ils prennent deux opérandes) et sont utilisés comme dans gauche == droite). Les colonnes qui contiennent false et true sont les résultats des opérations de comparaison.

La dernière colonne indique si l'opération fait encore sens lorsque l'une des opérandes est .nan. Par exemple, même si le résultat de 1.2 < real.nan est false, ce résultat n'a pas de sens parce que l'un des opérandes est real.nan. Le résultat de la comparaison inverse real.nan < 1.2 est également fausse. L'abréviation « cg » veut dire « côté gauche », désignant l'expression qui est à gauche de chaque opérateur.

Opérateur

Signification

Si cg est plus grand

si cg est plus petit

S'ils sont égaux

Si l'un des deux est .nan

Sensé avec .nan

==

est égal à

false

false

true

false

oui

!=

n'est pas égal à

true

true

false

true

oui

>

est plus grand que

true

false

false

false

non

=

est supérieur ou égal à

true

false

true

false

non

<

est plut petit que

false

true

false

false

non

<=

est inférieur ou égal à

false

true

true

false

non

!<>=

n'est ni supérieur, ni inférieur, ni égal à

false

false

false

true

oui

<>

plus grand ou plus petit que

true

true

false

false

non

<>=

est inférieur, supérieur, ou égal à

true

true

true

false

non

!<=

n'est ni inférieur, ni égal à

true

false

false

true

oui

!<

n'est pas inférieur à

true

false

true

true

oui

!>=

n'est pas supérieur ou égal à

false

true

false

true

oui

!>

n'est pas plus grand que

false

true

true

true

oui

!<>

n'est ni inférieur, ni supérieur à

false

false

true

true

oui

Notez qu'il est sensé d'utiliser .nan avec n'importe quel opérateur qui contient « n'est pas » dans sa signification et le résultat est toujours vrai. .nan n'étant pas une valeur valide, le résultat de la plupart des comparaisons n'a pas de sens.

17-11. isnan() pour tester .nan

Parce que le résultat est toujours false pour l'égalité avec .nan selon le tableau précédent, il n'est pas possible d'utiliser l'opérateur == pour déterminer si la valeur d'une variable flottante est .nan :

 
Sélectionnez
if (variable == double.nan) {    //  FAUX
   // ...
}

Pour cette raison, la fonction isnan() du module std.math doit être utilisée :

 
Sélectionnez
import std.math;
// ...
 
   if (isnan(variable)) {      //  correct
      // ...
   }

De même, pour déterminer si une valeurs n'est pas .nan, !isnan() doit être utilisé parce que l'opérateur != donnerait toujours true.

17-12. Exercices

  1. Modifiez la calculatrice du chapitre précédent pour prendre en charge les valeurs flottantes. La nouvelle calculatrice doit fonctionner plus précisément avec cette modification. Quand vous essayerez la calculatrice, vous pourrez entrer des valeurs flottantes dans des formats divers comme 1000, 1.23 et 1.23e4
  2. Écrivez un programme qui lit 5 valeurs flottantes depuis l'entrée. Le programme doit afficher le double de ces valeurs et ensuite le cinquième de ces valeurs. Cet exercice est une introduction à l'idée des tableaux du chapitre suivant. Si vous écrivez ce programme avec ce que vous avez vu jusqu'à maintenant, vous comprendrez les tableaux et vous les approprierez plus facilement.

SolutionsTypes à virgule flottante - Correction.

18. Tableaux

Nous avons défini 5 variables dans un des exercices du dernier chapitre et les avons utilisés dans certains calculs. Les définitions de ces variables étaient les suivantes :

 
Sélectionnez
double valeur_1;
double valeur_2;
double valeur_3;
double valeur_4;
double valeur_5;

Cette méthode de définir des variables individuellement n'est pas du tout efficace dans les cas où il y a besoin d'encore plus de variables. Imaginez qu'on ait besoin d'un millier de valeurs ; il est pratiquement impossible de définir 1000 variables de valeur_1 à valeur_1000.

Les tableaux sont utiles dans de tels cas : les tableaux permettent la définition de beaucoup de valeurs ensemble. Les tableaux sont aussi la structure de données la plus fréquente quand de multiples valeurs sont utilisées ensemble en tant que collection.

Ce chapitre couvre seulement quelques unes des fonctionnalités des tableaux. Plus de fonctionnalités seront introduites dans un chapitre ultérieur.

18-1. Définition

La définition de tableaux est vraiment similaire à la définition de variables. La seule différence est que le nombre de variables qui sont définies en même temps est indiqué entre crochets. On peut distinguer les deux définitions suivantes :

 
Sélectionnez
int uneSimpleVariable;
int[10] tableauDeDixVariables ;

La première ligne ci-dessus est la définition d'une simple variable, tout à fait comme les variables que nous avons définies jusqu'alors. La deuxième ligne est la définition d'un tableau consistant en 10 variables.

De même, l'équivalent des cinq variables séparées de l'exercice peut être défini comme un tableau de cinq variables en utilisant la syntaxe suivante :

 
Sélectionnez
double[5] valeurs;

Cette définition peut être lue comme 5 valeurs double. Notez qu'un pluriel a été choisi pour le nom du tableau pour éviter de le confondre avec une variable simple.

En résumé, la définition d'un tableau se compose du type des variables, de leur nombre et du nom du tableau :

 
Sélectionnez
nom_du_type[nombre_de_variables] nom_du_tableau;

Le type des variables peut aussi être un type défini par le programmeur (nous verrons les types définis par le programmeur plus tard). Par exemple :

 
Sélectionnez
// Un tableau qui stocke l'information météorologique de
// toutes les villes. Ici, les valeurs booléennes veulent dire :
//   false : couvert
//   true  : ensoleillé
bool[nombreDeVilles] ConditionsMeteorologiques;
 
// Un tableau qui stocke le poids de cent boîtes
double[100] poidsDesBoites;
 
// Information à propos des étudiants d'une école
DonneeEtudiant[NombredDEtudiants] DonneesEtudiants;

18-2. Éléments et conteneurs

Les structures de données qui rassemblent des éléments d'un certain type sont appelés conteneurs. Selon cette définition, les tableaux sont des conteneurs. Par exemple, un tableau qui stocke les températures de l'air des jours de juillet peut rassembler 31 variables double et forme un conteneur d'éléments de type double.

Les variables d'un conteneur sont appelés éléments. Le nombre d'éléments d'un tableau est appelé la longueur d'un tableau.

18-3. Accéder aux éléments

Pour différencier les variables dans l'exercice du chapitre précédent, nous devions ajouter un tiret du bas et un nombre à leurs noms (par exemple, valeur_1). Ce n'est ni possible, ni nécessaire quand les variables sont définies ensemble comme un seul tableau avec un seul nom. On accède aux éléments en indiquant leur numéro entre crochets :

 
Sélectionnez
valeurs[0]

Cette expression peut être lue comme « l'élément numéro 0 du tableau de nom "valeurs" ». En d'autres termes, au lieu de taper valeur_1, on tape valeurs[0] avec les tableaux. Il y a deux points importants qu'il vaut la peine de relever :

  • Les numéros commencent à 0 : même si les humains comptent à partir de 1, les indices de tableaux commencent à 0. Les valeurs que nous avons numérotées 1, 2, 3, 4 et 5 sont numéroté 0, 1, 2, 3 et 4 dans le tableau. Cette spécificité est une cause de nombreuses erreurs de programmation.
  • Deux utilisations différentes des crochets : ne les confondez pas. Quand on définit un tableau, les crochets sont écrits après le type des éléments et indiquent le nombre d'éléments. Quand on accède aux éléments, les crochets sont écrits après le nom du tableau et indiquent le numéro de l'élément auquel on accède :
 
Sélectionnez
// Ceci est une définition. Elle définit un tableau qui consiste
// en 12 éléments. Ce tableau est utilisé pour stocker le nombre
// de jours de chaque mois.
int[12] joursDuMois;
 
// Ceci est un accès. On accède à l'élément qui
// correspond à décembre et on lui donne la valeur 31.
joursDuMois[11] = 31;
 
// Ceci est un autre accès. On accède à l'élément qui
// correspond à janvier, sa valeur est passé à writeln.
writeln("Janvier a ", joursDuMois[0], " jours.");

Rappel : les numéros des éléments de Janvier et Décembre sont respectivement 0 et 11, non 1 et 12.

18-4. Indice

Le numéro d'un élément est appelé son indice.

Un indice n'a besoin d'être une valeur constante ; la valeur d'une variable peut peut aussi être utilisée comme un indice, ce qui rend les tableaux encore plus utiles. Par exemple, le mois est déterminé par la valeur de la variable IndiceMois ci-dessous :

 
Sélectionnez
writeln("Ce mois a ", joursDuMois[IndiceMois], " jours.");

Quand la valeur de IndiceMois est 2, l'expression ci-dessus affiche la valeur de joursDuMois[2], le nombre de jours du mois de mars.

Seuls les indices entre 0 et la longueur du tableau moins 1 sont valides. Par exemple, les indices valides d'un tableau à 3 éléments sont 0, 1 et 2. Accéder à un tableau avec un mauvais indice entraîne l'arrêt du programme avec une erreur.

Les tableaux sont des conteneurs dans lesquels les éléments sont placés les uns à côté des autres dans la mémoire de l'ordinateur. Par exemple, les éléments du tableau qui stocke le nombre de jours dans chaque mois peuvent être vus comme ceci :

Image non disponible

L'élément d'indice 0 a la valeur 31 (nombre de jours en janvier) ; l'élément d'indice 1 a la valeur 28 (nombre de jours en février), etc.

18-5. Tableaux de taille fixe vs tableaux dynamiques

Quand la taille du tableau est indiquée lorsque le programme est écrit, ce tableau est de taille fixe. Quand la longueur peut changer pendant l'exécution du programme, ce tableau est dynamique.

Les tableaux que nous avons définis au dessus sont des tableaux à taille fixe parce que leur nombre d'élément est indiqué lors de l'écriture du programme (5 et 12). Les longueurs de ces tableaux ne peuvent pas être changées pendant l'exécution du programme. Pour changer leur longueur, le code source doit être modifié et le programme doit être recompilé.

Définir un tableau dynamique est plus simple que définir un tableau à taille fixe ; il suffit d'omettre la taille et on obtient un tableau dynamique :

 
Sélectionnez
int[] tableauDynamique;

La taille d'un tel tableau peut augmenter ou diminuer pendant l'exécution du programme.

18-6. .length pour récupérer ou changer la taille du tableau

Les tableaux ont également des attributs. Ici, nous ne verrons que .length, qui retourne le nombre d'éléments du tableau.

 
Sélectionnez
writeln("Le tableau a", tableau.length, " éléments.");

De plus, la taille des tableaux dynamique peut être modifiée en affectant une valeur à cet attribut :

 
Sélectionnez
int[] array;         // initialement vide
tableau.length = 5;  // maintenant, il y a 5 éléments

Voyons maintenant comment on pourrait réécrire l'exercice avec les cinq valeurs en utilisant un tableau :

 
Sélectionnez
import std.stdio;
 
void main()
{
   // Cette variable est utilisée en tant que compteur dans une boucle
   int compteur;
 
   // La définition d'un tableau à taille fixe de cinq
   // éléments de type double
   double[5] valeurs;
 
   // récupérer les valeurs dans une boucle
   while (compteur < valeurs.length) {
      write("Valeur ", compteur + 1, " : ");
      readf(" %s", &valeurs[compteur]);
      ++compteur;
   }
 
   writeln("Le double des valeurs :");
   compteur = 0;
   while (compteur < valeurs.length) {
      writeln(valeurs[compteur] * 2);
      ++compteur;
   }
 
   // La boucle qui calcule le cinquième des valeurs
   // serait écrite de façon similaire
}

Observations : la valeur de compteur détermine combien de fois les boucles sont répétées (itérées). Itérer la boucle tant que cette valeur est strictement inférieure à valeurs.length assure que la boucle est exécutée une fois par élément. Comme la valeur de cette variable est incrémentée à la fin de chaque itération, l'expression valeurs[compteur] fait référence à chaque élément du tableau un par un : valeurs[0], valeurs[1], etc.

Pour voir à quel point ce programme est meilleur que le précédent, imaginez que vous auriez besoin de 20 valeurs. Le programme ci-dessus nécessiterait une seule modification : remplacer 5 par 20 ; alors que le programme qui n'utilisait pas de tableau aurait eu besoin de 15 définitions de variables de plus, idem pour les lignes dans lesquelles elles sont utilisées.

18-7. Initialiser les éléments

Comme toute variable en D, les éléments des tableaux sont automatiquement initialisés. La valeur initiale des éléments dépend du type des éléments : 0 pour int, double.nan pour double, etc.

Tous les éléments du tableau valeurs ci-dessus sont initialisés à double.nan :

 
Sélectionnez
double[5] valeurs;   // les éléments valent tous double.nan

Évidemment, les valeurs des éléments peuvent être modifiées plus tard dans le programme. On a déjà vu cela ci-dessus quand on a affecté une valeur à un élément de tableau :

 
Sélectionnez
joursDuMois[11] = 31;

Et aussi quand on récupérait une valeur depuis l'entrée :

 
Sélectionnez
readf(" %s", &valeurs[compteur]);

Parfois, les valeurs souhaitées des éléments sont connues au moment où le tableau est défini. Dans de tels cas, les valeurs initiales des éléments peuvent être indiquées du côté droit de l'opérateur d'affectation, entre crochets. Voyons cela dans un programme qui demande le numéro du mois à l'utilisateur et qui affiche le nombre de jours de ce mois :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int[12] joursDuMois =
      [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
 
   write("Veuillez saisir le numéro du mois : ");
   int numeroMois;
   readf(" %s", &numeroMois);
 
   int indice = numeroMois - 1;
   writeln("Le mois ", numeroMois, " a ",
            joursDuMois[indice], " jours.");
}

Comme vous pouvez le voir, le tableau joursDuMois est défini et initialisé au même moment. Notez aussi que le numéro du mois, qui est entre 1 et 12, est converti en un indice valide entre 0 et 11. Toute valeur entrée en dehors de l'intervalle 1-12 entraînerait l'arrêt du programme avec une erreur.

Quand on initialise des tableaux, il est possible d'utiliser une seule valeur sur le côté droit. Dans ce cas, tous les éléments du tableau sont initialisés à cette valeur :

 
Sélectionnez
int[10] tousUn = 1;    // Tous les éléments valent 1

18-8. Opérations basiques sur les tableaux

Les tableaux proposent des opérations pratiques qui s'appliquent à tous leurs éléments.

  • Copier des tableaux à taille fixe : l'opérateur d'affectation copie tous les éléments du tableau de droite dans le tableau de gauche :
 
Sélectionnez
    int[5] source = [ 10, 20, 30, 40, 50 ];
    int[5] destination;
     
    destination = source;

la signification de l'opération d'affectation est complètement différente pour les tableaux dynamiques. Nous verrons cela dans un chapitre ultérieur.

  • Ajouter des éléments aux tableaux : l'opérateur ~= ajoute un nouvel élément ou un nouveau tableau à la fin du tableau dynamique :
 
Sélectionnez
    int[] tableau;               // vide
    tableau ~= 7;                // un élément
    tableau ~= 360;              // deux éléments
    tableau ~= [ 30, 40 ];       // 4 éléments
  • Il n'est pas possible d'ajouter des éléments à un tableau à taille fixe :
 
Sélectionnez
    int[10] tableau;
    tableau ~= 7;                //  ERREUR de compilation
  • Combiner des tableaux : l'opérateur ~ crée un nouveau tableau en combinant deux tableaux. Son équivalent ~= combine deux tableaux et affecte le résultat au tableau de gauche :
 
Sélectionnez
    import std.stdio;
     
    void main()
    {
       int[10] premier = 1;
       int[10] second = 2;
       int[] resultat;
     
       resultat = premier ~ second;
       writeln(resultat.length);   // affiche 20
     
       resultat ~= premier;
       writeln(resultat.length);   // affiche 30
    }

L'opérateur ~= ne peut pas être utilisé quand le tableau de gauche est de taille fixe :

 
Sélectionnez
int[20] resultat;
// ...
resultat ~= premier;          //  ERREUR de compilation

Si le tableau de gauche n'a pas exactement la même taille que le tableau résultat, le programme se termine avec une erreur pendant l'affectation :

 
Sélectionnez
int[10] premier = 1;
int[10] second = 2;
int[21] resultat;

resultat = premier ~ second;

Sortie :

 
Sélectionnez
object.Error: Array lengths don't match for copy: 21 != 20

Si on traduit : « les longueurs ne correspondent pas pour la copie de tableau. »

18-9. Trier les éléments

std.algorithm.sort trie les éléments de plages à accès directs. Dans le cas des entiers, les éléments sont triés du plus petit au plus grand. Pour utiliser sort(), il est nécessaire d'importer le module std.algorithm :

 
Sélectionnez
import std.stdio;
import std.algorithm;
 
void main()
{
    int[] tableau = [ 4, 3, 1, 5, 2 ];
    sort(tableau);
    writeln(tableau);
}

La sortie :

 
Sélectionnez
[1, 2, 3, 4, 5]

18-9-1. Inverser les éléments

std.algorithm.reverse inverse les éléments sur place (le premier élément devient le dernier, etc.):

 
Sélectionnez
import std.stdio;
import std.algorithm;
 
void main()
{
    int[] array = [ 4, 3, 1, 5, 2 ];
    reverse(array);
    writeln(array);
}

La sortie :

 
Sélectionnez
[2, 5, 1, 3, 4]

18-9-2. Exercices

  1. Écrivez un programme qui demande à l'utilisateur combien de valeurs vont être entrées et ensuite les lit toutes. Le programme doit trier ces éléments en utilisant .sort et .reverse.
  2. Écrivez un programme qui lit des nombres depuis l'entrée, et affiche les nombres pairs et impairs séparément mais dans l'ordre. La valeur spéciale -1 termine la liste des nombres et ne fait pas partie de la liste.
    Par exemple, quand les nombres suivants sont entrés :

     
    Sélectionnez
    1 4 7 2 3 8 11 -1
  3. Le programme affiche ceci :

     
    Sélectionnez
    1 3 7 11 2 4 8

    vous pouvez vouloir enregistrer les éléments dans des tableaux séparés. Vous pouvez déterminer si un nombre est pair ou impair en utilisant l'opérateur % (modulo).

  4. Ce qui suit est un programme qui ne marche pas comme attendu. Le programme est écrit pour lire cinq nombre depuis l'entrée et placer les carrés de ces nombres dans un tableau. Le programme essaie ensuite d'afficher les carrés dans la sortie. Au lieu de ça, le programme se termine avec une erreur.
    Corrigez les bogues de ce programme et faites le marcher comme ce qui est attendu :
 
Sélectionnez
    import std.stdio;
     
    void main()
    {
        int[5] carres;
     
        writeln("Veuillez entrer 5 nombres");
     
        int i = 0;
        while (i <= 5) {
              int nombre;
              write("Nombre ", i + 1, ": ");
              readf(" %s", &nombre);
     
              carres[i] = nombre * nombre;
              ++i;
        }
     
        writeln("=== Les carrés des nombres ===");
        while (i <= carres.length)
        {
              write(carres[i], " ");
              ++i;
        }
     
        writeln();
    }

Les solutionsTableaux - Correction.

19. Caractères

Les caractères sont des briques élémentaires des chaînes. Chaque symbole d'un système d'écriture est appelé caractère : les lettres des alphabets, les chiffres, les signes de ponctuation, l'espace, etc. Les briques élémentaires des caractères elles-mêmes sont appelées caractères également, ce qui entraîne certaines ambiguïtés.

Les tableaux de caractères constituent les chaînes. Nous avons vu les tableaux dans le chapitre précédent ; les chaînes de caractères seront vues dans deux chapitres.

Comme n'importe quelle autre donnée, les caractères sont aussi représentés par des valeurs entières composées de bits. Par exemple, la valeur entière d'un 'a' minuscule est 97 et la valeur entière du chiffre '1' est 49. Ces valeurs ont été choisies principalement par convention quand la table ASCII a été écrite.

Dans la plupart des langages, les caractères sont représentés par le type char, qui ne peut stocker que 256 valeurs distinctes. Si vous êtes déjà familier-ère avec le type char dans d'autres langages, vous savez déjà probablement que ce n'est pas assez pour supporter les glyphes de beaucoup de systèmes d'écritures. Avant de parler des trois types de caractères du D, faisons un peu d'histoire sur les caractères dans les systèmes d'information.

19-1. Histoire

19-1-1. Table ASCII

La table ASCII a été écrite à une époque où le matériel informatique était très limité comparé aux systèmes modernes. Basée sur 7 bits, la table ASCII peut représenter 128 codes différents. C'est suffisant pour représenter des caractères comme les versions minuscules et majuscules des 26 lettres de l'alphabet latin, les chiffres, les signes de ponctuations couramment utilisés, et quelques caractères de contrôle pour le terminal.

Par exemple, les codes ASCII des caractères de la chaîne "hello" sont les suivants : 104, 101, 108, 108, 111.

Chaque code ci-dessus représente une lettre de "hello". Par exemple, il y a deux codes 108 pour les deux lettres 'l'. (Note : l'ordre réel de ces caractères dépend de la plateforme et même du document duquel ces valeurs font partie. Les codes ci-dessus sont dans l'ordre dans lequel ils apparaissent dans la chaîne.)

Les codes de la table ASCII ont ensuite été écrits sur 8 bits pour donner la table ASCII Étendue. La table ASCII Étendue contient 256 valeurs distinctes.

19-1-2. Pages de codes IBM

IBM Corporation a défini un ensemble de tables, chacune d'elle affectant les codes de 128 à 256 de la table ASCII étendue à un ou plusieurs systèmes d'écriture. Ces tables de codes ont permis de prendre en charge les lettres de beaucoup plus d'alphabets. Par exemple, les lettres spéciales de l'alphabet turc font partie de la page de codes IBM 857.

Bien qu'elles soient plus utiles qu'ASCII, les pages de codes ont des problèmes et des limitations : pour afficher le texte correctement, la page de codes utilisée pour l'écrire doit être connue. En effet, le même code correspond à un autre caractère dans la plupart des autres tables. Par exemple, le code qui représente 'Ğ' dans la table 857 correspond à 'ª' dans la table 437.

Un autre problème est la limitation du nombre d'alphabet qui peuvent être utilisés au sein d'un même document. De plus, les alphabets qui ont plus de 128 caractères non ASCII ne peuvent pas être pris en charges par une table IBM.

19-1-3. Pages de codes ISO/IEC 8859

Ces pages de codes sont le résultat d'efforts de standardisation internationaux. Elles sont similaires aux pages de codes IBM dans leur manière d'associer des caractères aux codes. Par exemple, les lettres spécifiques à l'alphabet turc apparaissent dans la page de codes 8859-9. Ces tables ont les mêmes problèmes et limitations que les tables d'IBM. Par exemple, le digramme néerlandais ij n'apparaît dans aucune de ces tables.

19-1-4. Unicode

Unicode résout tous les problèmes et les limitations des solutions précédentes. Unicode s'étend à plus de 100 milliers de caractères et symboles de systèmes d'écritures de beaucoup de langages humains, présents et passés (des nouveaux caractères sont constamment passés en revue en vue d'être ajoutés à la table). Chacun de ces caractères a un code unique. Les documents qui sont codés en Unicode peuvent utiliser tous les caractères des différents systèmes d'écriture en même temps sans aucune ambiguïté ni limitation.

19-2. Codages Unicode

Unicode associe un unique code à chaque caractère. Comme il y a plus de caractères Unicode que ce que peut contenir une valeur 8 bits, certains caractères doivent être représentés par au moins deux valeurs 8-bits. Par exemple, le code Unicode de 'Ğ' (286) est plus grand que la valeur maximale d'un type 8 bits (255).

Je vais utiliser 1 et 30 comme les valeurs des deux octets qui représentent Ğ de façon arbitraire. Ils ne sont valides dans aucun codage Unicode mais utiles pour introduire ces codages Unicode. Les valeurs correctes de ces valeurs sont hors de la portée de ce chapitre.

La manière dont les caractères sont représentés électroniquement est appelé codage. Nous avons vus ci-dessus comment la chaîne "hello" est représentée en ASCII. Nous allons maintenant voir les 3 codages Unicode qui correspondent aux types de caractères D.

  • UTF-32 : ce codage utilise 32 bits (4 octets) pour chaque caractère Unicode. Le codage UTF-32 de la chaîne "hello" est similaire à son codage ASCII, mais chaque caractère est représenté par 4 octets : 0, 0, 0, 104 ; 0, 0, 0, 101 ; 0, 0, 0, 108 ; 0, 0, 0, 108 ; 0, 0, 0, 111.
    Autre exemple : le codage UTF-32 de "aĞ" est 0, 0, 0,97 ; 0,0, 1, 30.
    Note : les valeurs réelles de ces octets sont différentes et l'ordre réel de ces octets peut être différent.
    'a' et 'Ğ' sont représentés par 1 et 2 octets significatifs respectivement, et les valeurs des 5 autres octets sont toutes zéro. Ces zéros peuvent être vus comme des octets de remplissage qui font que chaque caractère Unicode occupe 4 octets.
    Pour les documents écrits avec l'alphabet Latin de base, ce codage utilise toujours 4 fois plus d'octets que le codage ASCII. Quand, dans un document donné, la plupart des caractères a des équivalents ASCII, les trois octets de remplissage pour chacun de ces caractères rendent ce codage moins efficace que les autres codages.
    D'un autre côté, il y a des avantages à ce que chaque caractère soit toujours représenté par le même nombre d'octets.
  • UTF-16 : Ce codage utilise 16 bits (2 octets) pour représenter la plupart des caractères Unicode. Comme 16 bits peuvent représenter environ 65 milliers de valeurs uniques, les autres 35 mille caractères Unicode doivent être représentés par des octets supplémentaires.
    Par exemple, "aĞ" est codé avec 4 octets en UTF-16 : 0, 97 ; 1, 30.
    Note : les valeurs réelles de certains de ces octets sont différentes et l'ordre réel des octets peut être différent.
    Comparé à UTF-32, ce codage prend moins d'espace pour la plupart des documents, mais parce qu'il y a des caractères qui sont représentés par plus de deux octets. UTF-16 est plus compliqué à traiter.
  • UTF-8 : ce codage utilise un ou plusieurs octets pour chaque caractère. Si un caractère a un équivalent dans la table ASCII, il est représenté par un octet et par le même code que dans la table ASCII. Les autres caractères Unicode sont représentés par 2, 3 ou 4 octets. La plupart des caractères spéciaux des systèmes d'écritures européens font partie du groupe de caractères qui sont représentés par 2 octets.
    Pour la plupart des documents, UTF-8 est le codage qui prend le moins de place. Un autre bénéfice d'UTF-8 est que les documents qui ont déjà étés codés en ASCII correspondent directement à leurs codages UTF-8 respectifs. UTF-8 ne gaspille pas d'espace : chaque caractère est représenté par des octets significatifs uniquement. Par exemple, le codage UTF-8 de "aĞ" est : 97, 1, 30.
    Note : Les valeurs réelles de ces octets sont différentes et leur ordre peut être différent également.

19-3. Les types de caractères du D

Il y a trois types de caractères en D. Ces caractères correspondent aux trois codages Unicode mentionnés ci-avant. D'après ce que vous vous souvenez du chapitre sur les types fondamentauxTypes fondamentaux.

Type

Définition

Valeur initiale

char

Unité de stockage UTF-8

0xFF

wchar

Unité de stockage UTF-16

0xFFFF

dchar

Unité de stockage UTF-32 et point de code Unicode

0x0000FFFF

Comparé à d'autres langages de programmation, les caractères en D peuvent ne pas avoir le même nombre d'octets. Par exemple, parce que Ğ ne peut être représenté que par 2 octets au minimum dans Unicode, il ne rentre pas dans une variable de type char. En revanche, le type dchar, faisant 4 octets, peut stocker n'importe quel caractère Unicode.

Même si le D propose ces types utiles, le D ne supporte pas certaines fonctionnalités ésotériques d'Unicode. J'y reviens après.

19-4. Caractères littéraux

Les littéraux sont des valeurs constantes qui sont écrits dans le code source du programme. En D, les caractères littéraux sont écrits entres apostrophes :

 
Sélectionnez
char  letter_a = 'a';
wchar letter_e_acute = 'é';

Les guillemets ne sont pas valides pour les caractères parce qu'ils sont utilisés quand on écrit des chaînes, que nous verrons dans deux chapitres. 'a' est un caractère littéral et "a" est une chaîne littérale constituée d'un caractère.

Les variables de type char ne peuvent stocker que des lettres qui sont dans la table ASCII.

Il y a beaucoup de manières d'insérer des caractères dans le code :

  • Le plus naturellement, en les tapant sur le clavier.
  • En les copiant-collant depuis un autre programme ou un autre texte. Par exemple, vous pouvez copier-coller depuis un site Web, ou depuis un programme conçu pour afficher des caractères (on trouve de tels programmes dans la plupart des environnements Linux sous le nom de « Table de Caractères »).
  • En utilisant les noms raccourcis des caractères. La syntaxe, pour le faire, est : \&nom_du_caractere;. Par exemple, le nom du caractère Euro est euro et peut être écrit dans le programme comme ceci :

     
    Sélectionnez
    wchar currencySymbol = '\&euro;';
  • Voir la liste de tous les caractères nommés qui peuvent être écrits de cette manière.

  • En indiquant les caractères par leur numéro Unicode :

     
    Sélectionnez
        char a = 97;
        wchar Ğ = 286;
  • En spécifiant les codes des caractères de la table ASCII soit par \valeur_en_octal soit par \xvaleur_en_hexadécimal :
 
Sélectionnez
    char questionMarkOctal = '\77';
    char questionMarkHexadécimal = '\x3f';

Ces méthodes peuvent également être utilisées pour écrire des caractères dans les chaînes. Par exemple, les deux lignes qui suivent contiennent la même chaîne :

 
Sélectionnez
writeln("Résumé préparation: 10,25€");
writeln("\x52\&eacute;sum\u00e9 pr\u00e9paration: 10,25\&euro;");

19-5. Caractères de contrôle

Certains caractères ne font qu'affecter le format du texte, il n'ont pas de représentation visuelle propre. Par exemple, le caractère de nouvelle ligne, qui indique que la sortie devrait continuer sur une nouvelle ligne, n'a pas de représentation visuelle. De tels caractères sont appelés caractères de contrôle. Les caractères de contrôles sont écrit avec la syntaxe \caractère_de_contrôle.

Syntaxe

Nom

Effet

\n

Nouvelle ligne

Déplace l'affichage sur une nouvelle ligne

\r

retour chariot

Déplace l'affichage au début de la ligne actuelle

\t

tab

Déplace l'affichage à la prochaine tabulation

Par exemple, la fonction write, qui ne commence pas de nouvelle ligne automatiquement, le ferait pour chaque caractère \n. Chaque occurrence du caractère de contrôle \n à l'intérieur du littéral suivant représente le début d'une nouvelle ligne :

 
Sélectionnez
write("première ligne\ndeuxième ligne\ntroisième ligne\n");

Sortie :

 
Sélectionnez
première ligne
deuxième ligne
troisième ligne

19-6. Guillemet simple et antislash

Le guillemet simple lui-même ne peut pas être écrit à l'intérieur de guillemets simples parce que le compilateur prendrait le deuxième guillemet comme le caractère qui fermerait le premier : '''. Les deux premiers seraient les guillemets ouvrant et fermant, et le troisième serait tout seul, entraînant une erreur de compilation (NdT : de plus, n'écrire aucun caractère entre les deux guillemets simple est illégal).

De manière similaire, on ne peut pas écrire '\' pour désigner le caractère antislash. Comme l'antislash a une signification spéciale, le compilateur prendrait \' comme un caractère spécial. Le compilateur chercherait ensuite un guillemet fermant et ne le trouverait pas.

Pour éviter ces confusions, le guillemet simple et l'antislash sont échappés par un antislash :

 

caractère

Syntaxe

Nom

 
 

'

\'

guillemet simple

 
 

\

\\

antislash (contre-oblique)

 

19-7. Le module std.uni

Le module std.uni inclut des fonctions pour manipuler les caractères Unicode. Vous pouvez voir ces fonctions dans la documentation de ce module.

Les fonctions qui commencent par is répondent à certaines questions sur les caractères : le résultat est false ou true selon que la réponse est non ou oui (respectivement). Ces fonctions sont utiles dans des expressions logiques :

  • isLower : le caractère est-il en minuscule ?
  • isUpper : le caractère est-il en majuscule ?
  • isAlpha : le caractère est-il alphabétique au sens d'Unicode ? (une lettre)
  • isWhite : le caractère est-il blanc ? (espace, nouvelle ligne, tabulation, ...)

Les fonctions qui commencent par to renvoient de nouveaux caractères à partir de caractères existant :

  • toLower : renvoie le caractère minuscule correspondant au caractère donné.
  • toUpper : renvoie le caractère majuscule correspondant au caractère donné.

Voici un programme qui utilise toutes ces fonctions :

 
Sélectionnez
import std.stdio;
import std.uni;
 
void main()
{
   writeln("Est-ce que ğ est minuscule ? ", isLower('ğ'));
   writeln("Est-ce que Ş est minuscule ? ", isLower('Ş'));
 
   writeln("Est-ce que Ş est minuscule ? ", isUpper('İ'));
   writeln("Est-ce que ç est majuscule ? ", isUpper('ç'));
 
   writeln("Est-ce que z est alphanumérique ? ", isAlpha('z'));
   writeln("Est-ce que  est alphanumérique ? ", isAlpha('\&euro;'));
 
   writeln("Est-ce que la nouvelle ligne est un caractère blanc ? ", isWhite('\n'));
   writeln("Est-ce que le tiret du bas est un caractère blanc ? ", isWhite('_'));
 
   writeln("La minuscule de Ğ : ", toLower('Ğ'));
   writeln("La minuscule de İ : ", toLower('İ'));
 
   writeln("La majuscule de ş : ", toUpper('ş'));
   writeln("La majuscule de ı : ", toUpper('ı'));

Sortie :

 
Sélectionnez
Est-ce que ğ est minuscule ? true
Est-ce que Ş est minuscule ? false
Est-ce que İ est majuscule ? true
Est-ce que ç est majuscule ? false
Est-ce que z est alphanumérique ? true
Est-ce que € est alphanumérique ? false
Est-ce que la nouvelle ligne est un caractère blanc ? true
Est-ce que le tiret du bas est un caractère blanc ? false
La minuscule de Ğ : ğ
La minuscule de İ : i
La majuscule de ş : Ş
La majuscule de ı : I

19-7-1. Prise en charge limitée pour ı et i de l'alphabet turc

Les versions minuscules et majuscules des lettres ı et i sont « pointées » ou non de façon cohérente dans l'alphabet turc. La majorité des alphabets sont incohérents de ce point de vue : la majuscule du i « pointé » n'est pas « pointée ».

Parce que les systèmes informatiques ont commencé avec la table ASCII, la majuscule de i est I. Pour cette raison, ces deux lettres ont besoin d'une attention particulière. Le programme suivant montre ce problème :

 
Sélectionnez
import std.stdio;
import std.uni;
 
void main()
{
   writeln("La majuscule de i : ", toUpper('i'));
   writeln("La minuscule de I : ", toLower('I'));
}

Sortie :

 
Sélectionnez
La majuscule de i : I
La minuscule de I : i

19-7-2. Tous les alphabets ont une prise en charge limitée

Les caractères sont transformés en majuscule / minuscule selon leur code Unicode. Cette méthode est problématique pour beaucoup d'alphabets. Par exemple, les alphabets azéri et celte sont aussi affectés par le problème de la minuscule 'I' étant 'i'.

Il y a des problèmes similaires avec le tri. Les lettres de beaucoup d'alphabet, comme le ğ en turc, sont placées après le z. Même les caractères accentués comme á sont placés après le z, et ce, même pour l'alphabet latin basique.

19-8. Problème de lecture des caractères

La souplesse et la puissance des caractères Unicode en D peuvent être la source de résultats inattendus lors de la lecture d'un flux d'entrée. Cette contradiction est due aux multiples sens du terme « caractère ». Avant de développer plus ce point, considérons un programme qui a ce problème :

 
Sélectionnez
import std.stdio;
 
void main()
{
   char lettre;
   write("Veuillez entrer une lettre : ");
   readf(" %s", &lettre);
   writeln("La lettre suivante a été lue : ", lettre);
}

Si vous essayez ce programme dans un environnement qui n'utilise pas Unicode, vous pouvez voir que même les caractères non Unicode sont lus et affichés correctement.

Cependant, si vous démarrez ce programme dans un environnement Unicode, par exemple une console sous Linux, vous pouvez voir que le caractère affiché à la sortie n'est pas le même caractère que celui qui a été entré. Pour voir ceci, entrons un caractère non-ASCII dans une console qui utilise le codage UTF-8 (comme la plupart des consoles sous Linux) :

Sortie :

 
Sélectionnez
Veuillez entrer une lettre : ğ
La lettre suivante a été lue :     ← Pas de lettre à la sortie

La raison de ce problème est que les caractères non ASCII tels que le ğ sont représentés par deux codes, et lire un char depuis l'entrée ne lit que le premier des deux octets. Comme ce seul caractère n'est pas suffisant pour représenter le caractère Unicode entier, la console n'affiche pas le caractère incomplet.

Pour voir que les codes UTF-8 qui constituent le caractère sont lus char par char, lisons deux char et affichons-les l'un après l'autre :

 
Sélectionnez
import std.stdio;
 
void main()
{
   char premierCode;
   char secondCode;
 
   write("Veuillez entrer une lettre : ");
   readf(" %s", &premierCode);
   readf(" %s", &secondCode);
 
   writeln("La lettre suivante a été lue : ",
         premierCode, secondCode);
}

Le programme lit deux variables char depuis l'entrée et les affiche dans le même ordre. Quand ces codes sont envoyés à la console dans le même ordre, ils correspondent à un caractère UTF-8 entier sur la console et cette fois, le caractère Unicode est affiché correctement :

Sortie :

 
Sélectionnez
Veuillez entrer une lettre : ğ
La lettre suivante a été lue : ğ ← Le caractère Unicode qui
                                   consiste en deux codes char

Ces résultats sont dépendant aussi du fait que les entrées-sorties standard des programmes sont des flux de char.

Nous verrons plus tard dans le chapitre sur les chaînesChaînes de caractères qu'il est plus facile de lire des caractères comme des chaînes, plutôt que s'occuper individuellement des codes UTF-8.

19-9. Prise en charge de l'Unicode par le langage D

Unicode est un standard vaste et compliqué. D prend en charge un sous-ensemble utile de celui-ci.

Dans un document codé en Unicode, on distingue plusieurs idées :

  • Unité de stockage (code unit) : les valeurs qui constituent les codages UTF sont appelées unités de stockage. Selon le codage et les caractères eux-même, les caractères Unicode sont faits de une ou plusieurs unités de stockage. Par exemple, dans le codage UTF-8, la lettre 'a' est faite d'une seule unité de stockage et la lettre 'ğ' est faite de deux unités de stockage.
    Les types char, wchar, et dchar du D correspondent respectivement à une unité de stockage UTF-8, UTF-16, et UTF-32.
  • Point de code (code point) : chaque lettre, chiffre, symbole, etc. qu'Unicode définit est appelé un point de code. Par exemple, les codes Unicode de 'a' est de 'ğ' sont deux points de code distincts.
    Selon le codage, les points de code sont représentés par une ou plusieurs unités de stockage. Comme mentionné ci-avant, dans le codage UTF-8 'a' est représenté par une unité de stockage et 'ğ' est représenté par deux unités de stockage. En revanche, 'a' et 'ğ' sont représentés par une seule unité de stockage dans les codages UTF-16 et UTF-32.
    Le type D qui prend en charge les points de code est dchar. char est wchar ne peuvent être utilisés que pour les unités de stockage.
  • Caractère : n'importe quel symbole que le standard Unicode définit et que nous appelons « caractère » dans la vie de tous les jours est un caractère.
    La définition Unicode de caractère est flexible et ceci complique les choses. Certains caractères peuvent être formés de un ou plusieurs points de codes. Par exemple, la lettre ğ peut être désignée de deux manières :

    • comme simple point de code 'ğ' ;
    • comme les deux points de codes 'g' et '̆ '. (combining breve)

D ne prend pas nativement en charge le concept de points de codes combinés. En D, le point de code ğ est différent des deux points de codes consécutifs 'g' et '̆ '.

19-10. Résumé

  • Unicode supporte tous les caractères de tous les systèmes d'écritures.
  • Char est pour le codage UTF-8 ; même s'il ne convient pas pour représenter les caractères en général, il prend en charge la table ASCII.
  • wchar est pour l'encodage UTF-16 ; même s'il ne convient pas pour représenter les caractères en général, il peut prendre en charge une multitude d'alphabets.
  • dchar est pour le codage UTF-32 ; il peut également être utilisé pour les points de codes parce qu'il est 32 bits.

20. Tranches (slices) et autres fonctionnalités des tableaux

Nous avons vu dans le chapitre sur les tableauxTableaux comment les éléments sont groupés comme une collection dans un tableau. Ce chapitre a été volontairement bref, laissant à ce chapitre-ci la plupart des fonctionnalités des tableaux.

Avant d'aller plus loin, voici quelques définitions brèves de certains termes qui se trouvent être proches dans leur signification :

  • Tableau : l'idée générale d'un groupe d'éléments qui sont situés les uns à côté des autres et qui sont accédés par des indices.
  • Tableau de taille fixe (tableau statique) : tableau avec un nombre fixe d'éléments. Ce type de tableau stocke lui-même ses éléments.
  • Tableau dynamique : tableau qui peut perdre ou gagner des éléments. Ce type de tableau donne accès à des éléments qui sont stockés par l'environnement d'exécution du D.
  • Tranches (slices) : Autre nom des tableaux dynamiques.

Quand j'écrirai slice, je désignerai précisément une tranche. Quand j'écrirai tableau, je désignerai soit une tranche, soit un tableau à taille fixe, sans distinction.

20-1. Tranches

Les tranches sont la même chose que les tableaux dynamiques. Elles sont appelées tableaux dynamiques parce qu'elles sont utilisées comme des tableaux, et sont appelées tranches parce qu'elles donnent un accès à des portions d'autres tableaux. Elles permettent d'utiliser ces portions comme si elles étaient des tableaux à part entière.

Les tranches sont définies avec un intervalle correspondant aux indices du tableau duquel on veut « extraire » la tranche :

 
Sélectionnez
indice_de_debut .. un_apres_l_indice_de_fin

Dans la syntaxe permettant d'écrire un intervalle, l'indice de début fait partie de l'intervalle mais l'indice de fin est hors de l'intervalle :

 
Sélectionnez
/* ... */ = JoursDesMois[0 .. 3];   // 0, 1, et 2 sont inclus ; mais pas 3

les intervalles de nombres sont différents des intervalles de Phobos. Les intervalles de Phobos sont en relation avec les interfaces de classes et de structures. Nous verrons cela dans des chapitres ultérieurs.

Par exemple, on peut extraire des tranches du tableau JoursDesMois pour utiliser ses parties à travers quatre tableaux plus petits.

 
Sélectionnez
int[12] JoursDesMois =
      [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
 
int[] premierTrimestre   = JoursDesMois[0 .. 3];
int[] deuxiemeTrimestre  = JoursDesMois[3 .. 6];
int[] troisiemeTrimestre = JoursDesMois[6 .. 9];
int[] quatriemeTrimestre = JoursDesMois[9 .. 12];

Les quatre variables dans le code ci-dessus sont des tranches ; ils donnent accès aux 4 parties d'un tableau déjà existant. Un point important qui vaut la peine d'être noté est que ces tranches ne stockent pas leurs propres éléments. Elles donnent simplement accès aux éléments du vrai tableau. Modifier un élément d'une tranche modifie l'élément du tableau. Pour voir cela, modifions les premiers éléments de chaque tranche et affichons le tableau :

 
Sélectionnez
premierTrimestre[0]   = 1;
deuxiemeTrimestre[0]  = 2;
troisiemeTrimestre[0] = 3;
quatriemeTrimestre[0] = 4;
 
writeln(JoursDesMois);

Sortie :

 
Sélectionnez
[1, 28, 31, 2, 31, 30, 3, 31, 30, 4, 30, 31]

Chaque tranche modifie son premier élément, et l'élément correspondant dans le tableau est affecté.

Nous avons vu plus tôt que les indices valides des tableaux vont de 0 à la taille du tableau moins un. Par exemple, les indices valides d'un tableau à trois éléments sont 0, 1 et 2. De manière similaire, l'indice de fin dans la syntaxe de la tranche est l'indice de l'élément qui est juste après le dernier élément auquel la tranche donne accès. Par exemple, une tranche de tous les éléments d'un tableau à trois éléments serait tableau[0..3]

Évidemment, l'indice de début ne peut pas être plus grand que l'indice de fin :

 
Sélectionnez
int[3] tableau = [ 0, 1, 2 ];
int[] tranche = tableau[2 .. 1];   //  ERREUR lors de l'exécution

Il est correct d'avoir l'indice de début et l'indice de fin ayant la même valeur. Dans ce cas, la tranche est vide. En supposant qu'indice est valide :

 
Sélectionnez
int[] tranche = unTableau[indice .. indice];
writeln("La taille de la tranche : ", tranche.length);

Sortie :La taille de la tranche : 0

20-2. $, à la place de tableau.length

Quand on accède à un élément, $ est un raccourci pour désigner la taille du tableau :

 
Sélectionnez
writeln(array[array.length - 1]);   // le dernier élément
writeln(array[$ - 1]);              // la même chose

20-3. .dup pour copier

La propriété .dup (comme « dupliquer ») crée un nouveau tableau depuis les copies des éléments d'un tableau existant :

 
Sélectionnez
double[] array = [ 1.25, 3.75 ];
double[] theCopy = array.dup;

Par exemple, définissons un tableau qui contient le nombre de jours pour chaque mois d'une année bissextile. Une méthode est de faire la copie d'un tableau d'une année non bissextile et d'incrémenter l'élément qui correspond à février.

 
Sélectionnez
import std.stdio;
 
void main()
{
   int[12] JoursDesMois =
         [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
 
   int[] anneeBissextile = JoursDesMois.dup;
 
   ++anneeBissextile[1];   // incrémente le nombre de jours de février
 
   writeln("Année non bissextile : ", JoursDesMois);
   writeln("Année bissextile     : ", anneeBissextile);
}

Sortie :

 
Sélectionnez
Année non bissextile : [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
Année bissextile      : [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

20-4. Affectation

Jusqu'à maintenant, nous avons vu que l'opérateur d'affectation modifie les valeurs des variables. C'est la même chose avec les tableaux de taille fixe :

 
Sélectionnez
int[3] a = [ 1, 1, 1 ];
int[3] b = [ 2, 2, 2 ];
 
a = b;      // les éléments de 'a' deviennent 2
writeln(a);

Sortie : [2, 2, 2]

L'opération d'affectation a un sens complètement différent pour les tranches : après affectation, la tranche donne accès à des nouveaux éléments.

 
Sélectionnez
int[] impairs = [ 1, 3, 5, 7, 9, 11 ];
int[] pairs = [ 2, 4, 6, 8, 10 ];
 
int[] tranche;   // ne donne accès à aucun élément pour le moment
 
tranche = impairs[2 .. $ - 2];
writeln(tranche);
 
tranche = pairs[1 .. $ - 1];
writeln(tranche);

Ci-dessus, la tranche ne donne accès à aucun élément lors de sa définition. Ensuite, elle est utilisée pour donner accès à certains éléments de impairs et ensuite à certains éléments de pairs :

Sortie :

 
Sélectionnez
[5, 7]
[4, 6, 8]

20-5. Agrandir une tranche plus longue peut finir le partage

Comme la longueur d'un tableau à taille fixe ne peut pas être changée, l'idée de fin de partage ne concerne que les tranches.

Il est possible d'accéder aux mêmes éléments par plus d'une tranche. Par exemple, on accède aux deux premiers des huit éléments ci-dessous à travers trois tranches :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int[] tranche = [ 1, 3, 5, 7, 9, 11, 13, 15 ];
   int[] moitie = tranche[0 .. $ / 2];
   int[] quart = tranche[0 .. $ / 4];
 
   quart[1] = 0;   // modification via une tranche
 
   writeln(quart);
   writeln(moitie);
   writeln(tranche);
}

L'effet de la modification du second élément de quart se remarque sur toutes les tranches :

 
Sélectionnez
[1, 0]
[1, 0, 5, 7]
[1, 0, 5, 7, 9, 11, 13, 15]

Vu sous cet angle, les tranches donnent un accès partagé aux éléments. Ce partage pose la question de ce qu'il arrive quand un nouvel élément est ajouté à une des tranches. Comme les tranches donnent accès au même élément, il pourrait ne plus y avoir de place pour ajouter des éléments à une tranche sans rentrer en conflit avec les autres.

D répond à cette question en finissant le partage s'il n'y a pas de place pour le nouvel élément : la tranche qui n'a pas de place pour grandir abandonne le partage. Quand cela arrive, tous les éléments existants de cette tranche sont copiés à un nouvel endroit automatiquement et la tranche commence à donner accès à ces nouveaux éléments.

Pour voir ceci en action, ajoutons un élément à quart avant de modifier son second élément :

 
Sélectionnez
quart ~= 42;  // Cette tranche abandonne le partage parce qu'il
              // n'y a pas de place pour le nouvel élément.
 
quart[1] = 0; // Pour cette raison, la modification
              // n'affecte pas les autres tranches

La sortie du programme montre que la modification apportée à la tranche quart n'affecte pas les autres :

 
Sélectionnez
[1, 0, 42]
[1, 3, 5, 7]
[1, 3, 5, 7, 9, 11, 13, 15]

Augmenter la taille de la tranche de façon explicite lui fait également abandonner le partage :

 
Sélectionnez
++quart.length;       // Abandonne le partage

ou

 
Sélectionnez
quart.length += 5;    // Abandonne le partage

En revanche, raccourcir une tranche n'affecte pas le partage. Raccourcir la tranche veut simplement dire que celle-ci donne maintenant accès à moins d'éléments :

 
Sélectionnez
int[] a = [ 1, 11, 111 ];
int[] d = a;
 
d = d[1 .. $];   // raccourcir par la gauche
d[0] = 42;       // modifier l'élément depuis la tranche
 
writeln(a);      // afficher l'autre tranche

Comme on peut le voir sur la sortie, la modification depuis d est vue depuis a ; le partage est toujours là.

 
Sélectionnez
[1, 42, 111]

Réduire la longueur de différentes manières ne termine pas le partage non plus :

 
Sélectionnez
d = d[0 .. $ - 1];         // raccourcir par la droite
--d.length;                // idem
d.length = d.length - 1;   // idem

Le partage des éléments est toujours là.

20-6. capacity pour savoir si le partage va être terminé

Il y a des cas où les tranches continuent à partager des éléments même après qu'un élément leur ait été ajouté. Ceci arrive quand l'élément est ajouté à la tranche la plus longue et qu'il y a de la place après son dernier élément :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int[] tranche = [ 1, 3, 5, 7, 9, 11, 13, 15 ];
   int[] half = tranche[0 .. $ / 2];
   int[] quarter = tranche[0 .. $ / 4];
 
   tranche ~= 42;      // ajout à la tranche la plus longue...
   tranche[1] = 0;     // ... et modification d'un élément.
 
   writeln(quarter);
   writeln(half);
   writeln(tranche);
}

Comme on peut le remarquer sur la sortie, même si l'élément ajouté augmente la taille de la tranche, le partage n'a pas été terminé et la modification est reflétée sur toutes les tranches.

 
Sélectionnez
[1, 0]
[1, 0, 5, 7]
[1, 0, 5, 7, 9, 11, 13, 15, 42]

La propriété capacity des tranches détermine si le partage sera fini si un élément est ajouté à une certaine tranche. (capacity est en fait une fonction mais cette distinction n'a pas d'importance dans cette discussion.)

capacity a deux sens :

  • Quand sa valeur est 0, cela veut dire que ce n'est pas la tranche originale la plus grande. Dans ce cas, ajouter un nouvel élément déplacerait de toute façon les éléments de la tranche et le partage prendrait fin.
  • Quand sa valeur n'est pas nulle, cela veut dire que c'est la tranche originale la plus large. Dans ce cas, capacity dénote le nombre total d'éléments que la tranche peut contenir sans avoir besoin d'être copiée. Le nombre de nouveaux éléments qui peuvent être ajoutés à cette tranche avant qu'elle ait besoin d'être copiée peut être calculé en soustrayant la taille de la tranche à sa capacité. Il n'y a pas d'espace pour un nouvel élément si la taille de la tranche vaut sa capacité.

En conséquence, un programme qui a besoin de déterminer si le partage sera fini devrait utiliser un schéma dans ce style :

 
Sélectionnez
if (tranche.capacity == 0) {
   /* Ses éléments serait déplacés si un élément de plus
    * était ajouté à la tranche */
 
   // ...
 
} else {
   /* Cette tranche peut avoir de la place pour de nouveaux éléments avant
    * de nécessiter un déplacement. Calculons combien d'éléments on peut
    * ajouter avant déplacement de la tranche : */
   auto combienDeNouveauxElements = tranche.capacity - tranche.length;
 
   // ...
}

20-7. Opérations sur tous les éléments

Cette fonctionnalité concerne et les tranches, et les tableaux à taille fixe.

Les caractères [] écrits après le nom d'un tableau veulent dire : "tous ses éléments". Cette fonctionnalité simplifie le programme quand certaines opérations ont besoin d'être appliquées à tous les éléments d'un tableau.

Le compilateur D que j'ai (NdT:l'auteur) utilisé lors de l'écriture de ce chapitre (dmd 2.056) ne prend pas en charge cette caractéristique complètement. Pour cette raison, j'ai utilisé uniquement des tableaux à taille fixe dans quelques-uns des exemples qui suivent.

 
Sélectionnez
import std.stdio;
 
void main()
{
   double[3] a = [ 10, 20, 30 ];
   double[3] b = [  2,  3,  4 ];
 
   double[3] resultat = a[] + b[];
 
   writeln(resultat);
}

Sortie :

 
Sélectionnez
[12, 23, 34]

L'opération addition dans ce programme est appliquée aux éléments correspondant aux deux tableaux dans cet ordre : d'abord, les premiers éléments sont ajoutés, ensuite les seconds éléments sont ajoutés, etc. Une précondition naturelle est que les tailles des deux tableaux doivent être égales.

L'opérateur peut être l'un des opérateurs arithmétiques +, -, /, % et ^^ ; l'un des opérateurs binaires ^, & et | ou l'un des opérateurs unaires - et ~ qui sont écrits devant un tableau. Nous verrons certain de ces opérateurs dans des chapitres ultérieurs.

Les versions d'affectation de ces opérateurs peuvent aussi être utilisées : =, +=, -=, *=, /=, %=, ^^=, ^=, &= et |=.

Cette fonctionnalité n'est pas uniquement valable entre deux tableaux ; en plus d'un tableau, une expression qui est compatible avec les éléments peuvent aussi être utilisée. Par exemple, l'opération suivante divise tous les éléments d'un tableau par 4 :

 
Sélectionnez
double[3] a = [ 10, 20, 30 ];
a[] /= 4;
 
writeln(a);

Sortie : [2.5, 5, 7.5]

Pour affecter tous les éléments à une valeur spécifique :

 
Sélectionnez
a[] = 42;
writeln(a);

Sortie : [42, 42, 42]

Cette fonctionnalité nécessite une attention particulière quand elle est utilisée avec les tranches. Même s'il n'y a pas de différence apparente dans les valeurs des éléments, les deux expression suivantes ont un sens très différent :

 
Sélectionnez
tranche2 = tranche1;      //  tranche2 donne maintenant accès
                          //   aux mêmes éléments que tranche1
 
tranche3[] = tranche1;    //  les valeurs des éléments
                          //   de tranche3 changent

L'affectation à tranche2 lui fait partager les même éléments que tranche1. En revanche, comme tranche3[] fait référence à tous les éléments de tranche3, les valeurs de ses éléments deviennent les mêmes que celles des éléments de tranche1. L'effet de la présence ou de l'absence des crochets ne peut pas être ignorée.

On peut voir un exemple de cette différence dans le programme suivant :

 
Sélectionnez
import std.stdio;
 
void main()
{
   double[] tranche1 = [ 1, 1, 1 ];
   double[] tranche2 = [ 2, 2, 2 ];
   double[] tranche3 = [ 3, 3, 3 ];
 
   tranche2 = tranche1;      //  tranche2 commence à partager
                             //   les mêmes éléments que tranche1
 
   tranche3[] = tranche1;    //  Les valeurs des éléments de
                             //   tranche3 changent
 
    writeln("tranche1 avant : ", tranche1);
    writeln("tranche2 avant : ", tranche2);
    writeln("tranche3 avant : ", tranche3);
 
    tranche2[0] = 42;      //  La valeur d'un élément qu'elle
                           //   partage avec tranche1 change
 
    tranche3[0] = 43;      //  la valeur d'un élément auquel
                           //   tranche3 seule procure un accès change
 
   writeln("tranche1 après : ", tranche1);
   writeln("tranche2 après : ", tranche2);
   writeln("tranche3 après : ", tranche3);
}

La modification depuis tranche2 affecte tranche1 également :

 
Sélectionnez
tranche1 avant : [1, 1, 1]
tranche2 avant : [1, 1, 1]
tranche3 avant : [1, 1, 1]
tranche1 après : [42, 1, 1]
tranche2 après : [42, 1, 1]
tranche3 après : [43, 1, 1]

Le danger ici est que le bogue éventuel peut ne pas être remarqué jusqu'à ce que la valeur d'un élément partagé soit changée.

20-8. Tableaux multidimensionnels

Jusqu'à maintenant nous avons utilisé les tableaux avec des types fondamentaux comme int ou double uniquement. Le type d'élément peut en fait être n'importe quel autre type, notamment d'autres tableaux. Cela permet au programmeur de définir des conteneurs complexes comme des tableaux de tableaux. Les tableaux de tableaux sont appelés tableaux multidimensionnels.

Tous les éléments de tous les tableaux que nous avons définis jusqu'à présent ont été écrits dans le code source de gauche à droite. Pour aider à comprendre l'idée de tableau bidimensionnel, définissons cette fois-ci un tableau de haut en bas :

 
Sélectionnez
int[] array = [
            10,
            20,
            30,
            40
            ];

Comme vous devez vous en souvenir, la plupart des espaces dans le code source sont là pour rendre le code lisible et ne changent pas sa signification. Le tableau ci-dessus aurait pu être défini sur une seule ligne et aurait eu, le cas échéant, la même signification.

Remplaçons maintenant chaque élément de ce tableau par un autre tableau :

 
Sélectionnez
/* ... */ array = [
                     [ 10, 11, 12 ],
                     [ 20, 21, 22 ],
                     [ 30, 31, 32 ],
                     [ 40, 41, 42 ]
                  ];

On a remplacé des éléments de type entier avec des éléments de type int[]. Pour rendre le code conforme à la syntaxe permettant de définir les tableaux, on doit maintenant indiquer le type des éléments comme int[] à la place de int :

 
Sélectionnez
int[][] array = [
                   [ 10, 11, 12 ],
                   [ 20, 21, 22 ],
                   [ 30, 31, 32 ],
                   [ 40, 41, 42 ]
                ];

De tels tableaux sont appelés tableaux bidimensionnels parce qu'ils peuvent être vus comme ayant des lignes et des colonnes.

Les tableaux bidimensionnels sont utilisés comme n'importe quel autre tableau du moment que l'on se souvient que chaque élément est lui-même un tableau et qu'il est utilisé avec les opérations sur les tableaux :

 
Sélectionnez
array ~= [ 50, 51 ]; // ajoute un nouvel élément (une tranche)
array[0] ~= 13;      // ajoute un élément au premier élément

Le nouvel état du tableau :

 
Sélectionnez
[[10, 11, 12, 13], [20, 21, 22], [30, 31, 32], [40, 41, 42], [50, 51]]

Les éléments du tableau et le tableau lui-même peuvent être de taille fixe. Ce qui suit est un tableau tridimensionnel où toutes les dimensions sont de taille fixe :

 
Sélectionnez
int[2][3][4] tableau;   // 2 colonnes, 3 lignes, 4 pages

La définition ci-dessus peut être vue comme 4 pages de trois lignes de deux colonnes. Par exemple, un tel tableau peut être utilisé pour représenter un bâtiment à 4 étages dans un jeu d'aventure, chaque étage consistant en 2×3=6 pièces.

Par exemple, le nombre d'objets dans la première pièce du deuxième étage peut être incrémenté de cette manière :

 
Sélectionnez
// L'indice du deuxième étage est 1 et on accède à
// la première pièce de cet étage par [0][0]
++nombresObjets[1][0][0];

En plus de la syntaxe ci-dessus, l'expression new peut aussi être utilisée pour créer une tranche de tranches. L'exemple suivant utilise seulement deux dimensions :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int[][] s = new int[][](2, 3);
   writeln(s);
}

L'expression new ci-dessus crée deux tranches contenant 3 éléments chacune et retourne une tranche qui donne accès à ces tranches et éléments.

Sortie :

 
Sélectionnez
[[0, 0, 0], [0, 0, 0]]

20-9. Résumé

  • Les tableaux de taille fixe stockent leurs éléments ; les tranches donnent accès à des éléments qui ne leur appartiennent pas.
  • À l'intérieur des crochets, $ est l'équivalent de nom_tableau.length.
  • .dup crée un nouveau tableau qui est composé des copies des éléments d'un tableau existant.
  • Avec les tableaux à taille fixe, l'affectation change les valeurs des éléments ; avec les tranches, elle fait pointer la tranche vers d'autres éléments.
  • Les tranches qui grandissent peuvent arrêter de partager les éléments et commencer à donner accès à des éléments nouvellement copiés. .capacity détermine si ça sera le cas.
  • array[] fait référence à tous les éléments ; l'opération qui est appliquée à array[] est appliquée à chaque élément individuellement.
  • Les tableaux de tableaux sont appelés tableaux multidimensionnels.

20-10. Exercice

Itérez sur les éléments d'un tableau de doubles et divisez par deux ceux qui sont plus grands que 10. Par exemple, étant donné le tableau suivant :

 
Sélectionnez
double[] array = [ 1, 20, 2, 30, 7, 11 ];

On doit obtenir :

 
Sélectionnez
[1, 10, 2, 15, 7, 5.5]

Même s'il y a beaucoup de solutions à ce problème, essayez de n'utiliser que les fonctionnalités des tranches. Vous pouvez commencer avec une tranche qui donne accès à tous les éléments. Ensuite, vous pouvez réduire la tranche depuis le début et ne travailler que sur le premier élément.

L'expression suivante réduit la tranche depuis le début :

 
Sélectionnez
tranche = tranche[1 .. $];

La solutionTranches et autres fonctionnalités des tableaux - Correction.

21. Chaînes de caractères

Nous avons utilisé des chaînes de caractères dans beaucoup de programmes que nous avons vus jusqu'à maintenant. Les chaînes sont une combinaison de deux fonctionnalités que nous avons couvertes dans les 3 derniers chapitres : les caractères et les tableaux. Dans la définition la plus simple, les chaînes ne sont rien d'autre que des tableaux de caractères. Par exemple,

char[]

est un type de chaîne.

Cette simple définition peut être trompeuse. Comme nous l'avons vu dans le chapitre sur les caractèresCaractères, D a trois types de caractères distincts, dont certain peuvent avoir des résultats surprenants dans certaines opérations sur les chaînes.

21-1. readln et chomp, à la place de readf

Il y a des surprises même quand on lit des chaînes depuis la console.

Étant des tableaux de caractères, les chaînes peuvent contenir des caractères de contrôle comme '\n'. Lorsqu'on lit des chaînes de caractères depuis l'entrée, le caractère de contrôle qui correspond à la touche Entrée sur laquelle on appuie à la fin d'une saisie dans la console fait partie de la chaîne également. De plus, à cause de l'impossibilité de dire à readf() combien de caractères il faut lire, readf() continue à lire jusqu'à la fin de l'entrée. De ce fait, read() ne marche pas comme attendu quand on lit des chaînes :

 
Sélectionnez
import std.stdio;
 
void main()
{
   char[] nom;
 
   write("Quel est votre nom ? ");
   readf(" %s", &nom);
 
   writeln("Salut ", nom, "!");
}

La touche entrée sur laquelle l'utilisateur appuie après le nom ne termine pas l'entrée. readf() continue à attendre plus de caractères à ajouter à la chaîne :

 
Sélectionnez
Quel est votre nom ? Mert
   ← L'entrée n'est pas terminée alors même que la touche Entrée a été enfoncée.
   ← (Supposons que Entrée est enfoncée une nouvelle fois ici)

Une manière de terminer le flux d'entrée standard est d'appuyer sur Ctrl+D sous les systèmes de type Unix et Ctrl+Z sur les systèmes Windows. Si l'utilisateur finit l'entrée de cette manière, on voit que les caractères de nouvelle ligne on été lus également :

 
Sélectionnez
Salut Mert
    ← nouvelle ligne après le nom
!(encore un avant le point d'exclamation)

Le point d'exclamation apparaît après ces caractères au lieu d'être affiché juste après le nom.

readln() convient mieux à la lecture de chaînes. Abréviation de read line (lire ligne), readln() lit jusqu'à la fin de la ligne. Il est utilisé différemment parce que la chaîne de formatage " %s" et l'opérateur & ne sont pas nécessaires :

 
Sélectionnez
import std.stdio;
 
void main()
{
   char[] nom;
 
   write("Quel est votre nom ? ");
   readln(nom);
 
   writeln("Salut ", nom, " !");
}

readln() enregistre le caractère de nouvelle ligne également ; ceci, pour que le programme ait un moyen de déterminer si l'entrée contient une ligne complète ou si la fin de l'entrée a été atteinte :

 
Sélectionnez
Quel est votre nom ? Mert
Salut Mert
 ! ← Caractère de nouvelle ligne avant le point d'exclamation

De tels caractères de contrôle qui sont situés à la fin des chaînes peuvent être supprimés par std.string.chomp.

 
Sélectionnez
import std.stdio;
import std.string;
 
void main()
{
   char[] nom;
 
   write("Quel est votre nom ? ");
   readln(nom);
   nom = chomp(nom);
 
   writeln("Salut ", nom, " !");
}

L'expression chomp() ci-avant retourne une nouvelle chaîne qui ne contient pas les caractères de contrôle de la fin de la chaîne. Réaffecter la valeur de retour à nom donne le résultat attendu :

 
Sélectionnez
Quelle est votre nom ? Mert
Salut Mert !                   #  pas de nouvelle ligne

readln() peut être utilisé sans paramètre. Dans ce cas, readln() retourne la ligne qui vient d'être lue. Chaîner le résultat de readln() et chomp() permet une syntaxe plus lisible et plus courte :

 
Sélectionnez
string nom = chomp(readln());

Après avoir introduit le type string, j'utiliserai cette syntaxe.

21-2. Guillemets doubles, et non simples

Nous avons vu que les guillemets simples sont utilisés pour définir des caractères littéraux. Les chaînes littérales sont définies avec des guillemets doubles. 'a' est un caractère ; "a" est une chaîne qui ne contient qu'un caractère.

21-3. string, wstring, et dstring sont immuables (immutable)

Il y a trois types de chaînes qui correspondent aux trois types de caractères : char[], wchar[] et dchar[].

Il y a trois alias des versions immuables de ces types : string, wstring et dstring. Les caractères de ces variables qui sont définies avec ces alias ne peuvent pas être modifiés. Par exemple, les caractères d'un wchar[] peuvent être modifiés mais les caractères d'une wstring ne peuvent pas l'être (nous verrons l'immuabilité en D ultérieurement).

Par exemple, le code suivant, qui essaie de capitaliser la première lettre d'une string ne compile pas :

 
Sélectionnez
string nePeutEtreMutée = "salut";
nePeutEtreMutée[0] = 'S'; // ERREUR DE COMPILATION

On pourrait penser à définir la variable en tant que char[] au lieu de l'alias string mais ceci ne compilerait pas non plus :

 
Sélectionnez
char[] a_tranche = "hello";   // ERREUR DE COMPILATION

Cette fois, l'erreur de compilation est due à la combinaison de deux facteurs :

  1. Le type des chaînes littérales comme "hello" est string, et non char[], elles sont donc immuables.
  2. Le char[] sur le côté gauche est une tranche, qui donnerait accès, si le code était compilé, à tous les caractères du côté droit.

Comme char[] est mutable et que string ne l'est pas, il y a conflit. Le compilateur ne permet pas d'accéder aux caractères d'un tableaux immuable par une tranche « mutable ».

La solution ici est de faire une copie de la chaîne immuable avec la propriété .dup :

 
Sélectionnez
import std.stdio;
 
void main()
{
   char[] s = "salut".dup;
   s[0] = 'S';
   writeln(s);
}

Le programme peut maintenant être compilé et afficher la chaîne modifiée :

 
Sélectionnez
Salut

De façon similaire, char[] ne peut pas être utilisé là ou une string est nécessaire. Dans ce tels cas, la propriété .idup peut être utilisée pour produire une variable string immuable à partir d'une variable mutable char. Par exemple, si s est une variable du type char[], la ligne suivante ne peut être compilée :

 
Sélectionnez
string resultat = s ~ '.'; // ERREUR DE COMPILATION

Quand le type de s est char[], le type de l'expression à droite de l'affectation ci-dessus est char[] également. .idup est utilisé pour obtenir des chaînes immuables à partir de chaînes existantes :

 
Sélectionnez
string resultat = (s ~ '.').idup;   //  maintenant, compile

21-4. Confusion potentielle sur la taille des chaînes

Nous avons vu que certains caractères Unicode sont représentés par plus d'un octet. Par exemple, la lettre 'é' est représentée par deux octets. On remarque cela avec la propriété .length des chaînes :

 
Sélectionnez
writeln("résumé".length);

Même si « résumé » contient 6 lettres, la taille de la chaîne est le nombre de caractères qu'elle contient : 8

Le type des éléments de chaînes littérales comme "hello" est char et char représente une unité de stockage UTF-8. Cela peut engendrer un problème quand on essaie de remplacer une lettre à deux unités de stockage par une lettre avec une seule unité de stockage :

 
Sélectionnez
char[] s = "résumé".dup;
writeln("Avant : ", s);
s[1] = 'e';
s[5] = 'e';
writeln("Après : ", s);

Les deux caractères 'e' ne remplacent pas les deux lettre 'é' ; ils remplacent des unités de stockage uniques, ce qui conduit à un codage UTF-8 incorrect :

Sortie :

 
Sélectionnez
Avant : résumé
Après : re�sueé   ← INCORRECT

Quand on s'occupe des lettres, des symboles ou autre caractères unicodes directement comme dans le code ci-dessus, le type à utiliser est dchar :

 
Sélectionnez
dchar[] s = "résumé"d.dup;
writeln("Before: ", s);
s[1] = 'e';
s[5] = 'e';
writeln("After : ", s);

Sortie :

 
Sélectionnez
Avant : résumé
Après : resume

Notez les deux différences dans le nouveau code :

  1. Le type de chaîne est dchar[].
  2. Il y a un d à la fin du littéral "résumé"d, indiquant que c'est un tableau de dchars.

21-5. Chaînes littérales

Le caractère optionnel qui est indiqué après les chaînes littérales détermine le type d'élément de la chaîne :

 
Sélectionnez
import std.stdio;
 
void main()
{
   string s = "résumé"c;   // équivalent à "résumé"
   wstring w = "résumé"w;
   dstring d = "résumé"d;
 
   writeln(s.length);
   writeln(w.length);
   writeln(d.length);
}

Sortie :

 
Sélectionnez
8
6
6

Parce que toutes les lettres de "résumé" peuvent être représentés par un seul wchar ou dchar, les deux dernières longueurs sont égales au nombre de lettres.

21-6. Concaténations de chaînes

Comme les chaînes sont en fait des tableaux, toute les opérations sur les tableaux peuvent être appliquées sur les chaînes également. ~ concatène deux chaînes et ~= ajoute à une nouvelle chaîne :

 
Sélectionnez
import std.stdio;
import std.string;
 
void main()
{
   write("Quel est votre nom ? ");
   string nom = chomp(readln());
 
   // Concaténer :
   string salutation = "Salut " ~ nom;
 
   // Ajouter :
   salutation ~= " ! Bienvenue...";
 
   writeln(salutation);
}

Sortie :

 
Sélectionnez
Quel est votre nom ? Can
Salut Can ! Bienvenue...

21-7. Comparaison de chaînes

Unicode ne définit pas comment les caractères sont ordonnés autrement que par leurs codes Unicode. Pour cette raison, vous pouvez obtenir des résultats qui ne correspondent pas à vos attentes. Quand l'ordre alphabétique est important, vous pouvez utiliser une bibliothèque du type trileri qui prend en charge l'idée d'alphabet.

Jusqu'à maintenant, nous n'avons utilisé les opérateurs de comparaison <, >=, etc. qu'avec les entiers et les flottants. Les mêmes opérateurs peuvent être utilisés avec les chaînes également, mais avec une signification différente : les chaînes sont ordonnées de façon lexicographique. Cet ordre considère le code Unicode de chaque caractère comme étant la place de ce caractère dans un alphabet fictif géant :

 
Sélectionnez
import std.stdio;
import std.string;
 
void main()
{
   write("      Entrez une chaîne : ");
   string s1 = chomp(readln());
 
   write("Entrez une autre chaîne : ");
   string s2 = chomp(readln());
 
   if (s1 == s2) {
      writeln("Ce sont les mêmes !");
 
   } else {
      string avant;
      string apres;
 
      if (s1 < s2) {
            avant = s1;
            apres = s2;
 
      } else {
            avant = s2;
            apres = s1;
      }
 
      writeln("'", avant, "' vient avant '", apres, "'.");
   }
}

Du fait qu'Unicode reprend la table ASCII pour les lettres de base de l'alphabet latin, les chaînes qui ne contiennent que des caractères ASCII sont ordonnées correctement.

21-8. Majuscule et minuscule sont différentes

Du fait que chaque lettre a un code unique, chaque lettre est différente des autres. Par exemple, 'A' et 'a' sont des lettres différentes.

De plus, du fait de leur code ASCII, les lettres majuscules sont ordonnées avant les lettres minuscules. Par exemple, 'B' est avant 'a'. La fonction icmp() du module std.string peut être utilisée quand les chaînes doivent être comparées sans tenir compte des majuscules et des minuscules. Vous pouvez voir les fonctions de ce module dans sa documentation.

Du fait que les chaînes sont des tableaux (et donc des intervalles), les fonctions des modules std.array, std.algorithm, et std.range sont très utiles avec les chaînes également.

21-9. Exercices

  • Parcourez la documentation des modules std.string, std.array, std.algorithm et std.range.
  • Écrivez un programme qui utilise l'opérateur ~ : l'utilisateur entre le prénom et le nom en minuscule et le programme donne le nom complet qui contient la bonne capitalisation des noms. Par exemple, quand les chaînes sont « ebru » et « domates », le programme devrait afficher « Ebru Domates ».
  • Lisez une ligne depuis l'entrée et affichez la partie entre le premier et le dernier 'e' de la ligne. Par exemple, quand la ligne est « cette ligne a 5 mots », le programme devrait afficher « cette ligne ».
  1. Les fonctions indexOf() et lastIndexOf() sont utiles pour obtenir les deux indices pour créer une tranche.
    Comme indiqué dans leur documentation, les types de retour de indexOf() et lastIndexOf() ne sont pas int ni size_t, mais sizediff_t. Vous pouvez avoir besoin de définir des variables de ce type :

     
    Sélectionnez
    sizediff_t premier_e = indexOf(ligne, 'e');
  2. Il est possible de définir des variables de façon plus concise avec le mot-clé auto, que nous verrons dans un chapitre ultérieur :
 
Sélectionnez
auto premier_e = indexOf(ligne, 'e');

Les solutionsChaînes de caractères - Correction.

22. Rediriger les flux d'entrée et de sortie standards

Tous les programmes que nous avons vu jusqu'à maintenant interagissaient à travers stdin et stdout, respectivement le flux d''entrée standard et le flux de sortie standard. Par défaut, les fonctions d'entrée et de sortie comme readf et writeln opèrent sur ces flux.

Lors de l'utilisation de ces flux, nous avons supposé que l'entrée standard venait du clavier et que la sortie standard allait vers l'écran.

Nous commencerons à écrire des programmes qui géreront des fichiers dans le chapitre suivant. Nous verrons que comme les flux d'entrée et de sortie, les fichiers sont des flux de caractères également ; il sont donc utilisés pratiquement de la même manière que stdin et stdout.

Avant de voir comment on accède aux fichiers depuis les programmes, je voudrais vous montrer comment les entrées et sorties standard des programmes peuvent être redirigés vers des fichiers ou vers d'autres programmes. Les programmes peuvent enregistrer dans des fichiers au lieu d'afficher à l'écran, et lire depuis des fichiers à la place du clavier. Même si ces fonctionnalités ne concernent pas directement les langages de programmation, ce sont des outils utiles qui sont disponibles dans toutes les consoles modernes.

22-1. Rediriger la sortie standard vers un fichier avec >.

Quand on lance un programme depuis la console, écrire un caractère > et un nom de fichier à la fin de la ligne de commande redirige la sortie standard de ce programme vers le fichier spécifié. Tout ce que le programme envoie vers la sortie standard sera écrit dans ce fichier plutôt que sur l'écran.

Testons ceci avec un programme qui lit un nombre flottant depuis son entrée, le multiplie par deux et l'affiche dans sa sortie standard :

 
Sélectionnez
import std.stdio;
 
void main()
{
   double nombre;
   readf(" %s", &nombre);
 
   writeln(nombre * 2);
}

Si le nom du programme est par_deux, sa sortie sera écrite vers un fichier nommé par_deux_resultat.txt si on exécute la ligne suivante :

 
Sélectionnez
./par_deux > par_deux_resultat.txt

Par exemple, si on entre 1.2 dans la console, le résultat 2.4 apparaîtra dans par_deux_resultat.txt. Note : même si le programme n'affiche pas une ligne du style « Veuillez entrer un nombre », il s'attend quand même à ce qu'un nombre soit entré.

22-2. Rediriger l'entrée standard depuis un fichier avec <

De manière similaire, l'entrée standard peut être redirigée depuis un fichier par <. Dans ce cas, le programme lit depuis le fichier spécifié à la place du clavier.

Pour tester cela, utilisons un programme qui calcule le dixième d'un nombre :

 
Sélectionnez
import std.stdio;
 
void main()
{
   double nombre;
   readf(" %s", &nombre);
 
   writeln(nombre / 10);
}

En supposant que le fichier par_deux_resultat.txt existe toujours et contient 2.4, et que le nom du nouveau programme est un_dixieme, on peut rediriger l'entrée du nouveau programme de cette manière :

 
Sélectionnez
./un_dixieme < par_deux_resultat.txt

Cette fois, le programme lira depuis par_deux_resultat.txt et affichera le résultat 0.24 à la console.

22-3. Rediriger les deux flux standard

> et < peuvent être utilisés en même temps :

 
Sélectionnez
./un_dixieme < par_deux_resultat.txt > un_dixieme_resultat.txt

Cette fois, l'entrée standard serait lue depuis par_deux_resultat.txt et la sortie standard serait écrite vers un_dixieme_resultat.txt.

22-4. Combiner les programmes avec |

Notez que par_deux_resultat.txt est un intermédiaire entre les deux programmes ; par_deux écrit dedans et un_dixieme lit depuis ce fichier.

Le caractère | « achemine » (pipe) la sortie standard du programme qui est sur sa gauche vers l'entrée standard du programme qui est sur la droite sans nécessiter un fichier intermédiaire. Par exemple, quand les deux programmes ci-dessus sont « combinés » ensemble comme dans la ligne qui suit, ils calculent ensemble un cinquième du nombre en entrée :

 
Sélectionnez
./par_deux | ./un_dixieme

par_deux est lancé et lit un nombre depuis l'entrée. Le résultat de par_deux apparaît ensuite dans l'entrée standard de un_dixieme, qui à son tour calcule et affiche un dixième de ce résultat.

22-5. Exercice

Combinez plus de deux programmes :

 
Sélectionnez
./un | ./deux | ./trois

La solutionRediriger l'entrée standard et les flux de sortie - Correction.

23. Fichiers

Nous avons vu dans le chapitre précédent que les flux d'entrée et de sortie standard peuvent être redirigés vers et depuis des fichiers ou d'autres programmes avec les opérateurs >, < et | dans la console. Malgré leur puissance, ces outils ne sont pas adaptés dans toutes les situations parce que dans beaucoup de cas, les programmes ne peuvent pas effectuer leurs tâches en lisant leur entrée et en écrivant sur leur sortie.

Par exemple, un programme qui gère des enregistrements d'étudiants pourrait utiliser sa sortie standard pour afficher le menu du programme. Mais un tel programme aurait besoin d'écrire les enregistrements dans un fichier plutôt que dans stdout.

Dans ce chapitre, nous allons voir comment lire et écrire des fichiers du système de fichiers.

23-1. Concepts fondamentaux

Les fichiers sont représentés par la structure File du module std.stdio. Comme je n'ai pas encore présenté les structures, il faudra accepter leur syntaxe telle quelle.

Avant de rentrer dans le code, il faut voir les idées fondamentales à propos des fichiers.

23-1-1. Le producteur et le consommateur

Les fichiers qui sont créés sur une plateforme peuvent ne pas être utilisables tels quels sur d'autres plateformes. Simplement ouvrir un fichier et écrire des données dedans peut ne pas être suffisant pour que ces données soient disponibles au consommateur. Le producteur et le consommateur des données doivent d'abord se mettre d'accord sur le format des données qui sont dans le fichier. Par exemple, si le producteur a écrit l'identifiant et le nom des enregistrements des étudiants dans un certain ordre, le consommateur doit lire les données dans le même ordre.

De plus, les exemples de code ci-après n'écrivent pas de BOM au début du fichier. Ceci peut rendre vos fichiers incompatibles avec les systèmes qui nécessite une BOM (BOM veut dire Byte Ordrer Mark, indicateur d'ordre des octets).

23-1-2. Droits d'accès

Les systèmes de fichiers donnent accès aux fichiers aux programmes avec certains droits. Les droits d'accès sont aussi important pour l'intégrité des données que pour les performances.

Quand il s'agit de lire, autoriser plusieurs programmes à lire depuis le même fichier améliore les performances parce qu'aucun programme n'a à attendre que les autres finissent leur lecture. En revanche, quand il s'agit d'écrire, un seul programme devrait être autorisé à écrire dans un fichier donné, sinon les programmes pourraient remplacer les données des autres programmes.

23-1-3. Ouvrir un fichier

Les flux d'entrée et de sortie standard stdin et stdout sont déjà ouverts quand les programmes démarrent. Ils sont prêts à être utilisés.

En revanche, les fichiers doivent d'abord être ouverts en indiquant leur nom et les droits d'accès nécessaires.

23-1-4. Fermer un fichier

Tout fichier qui a été ouvert par un programme doit être fermé quand le programme a fini de l'utiliser. Dans la plupart des cas, les fichiers n'ont pas besoin d'être fermés explicitement, ils sont fermés automatiquement quand les objets File sont détruits automatiquement :

 
Sélectionnez
if (uneCondition) {
 
   // On suppose qu'un objet File a été créé et utilisé ici.
   // ...
 
} //  Le fichier sera fermé automatiquement ici en sortie
  //   de la structure if. Pas besoin de le fermer explicitement.

Dans certains cas, il peut être nécessaire qu'un objet File soit ré-ouvert pour accéder à un fichier différent ou au même fichier avec des droits d'accès différents. Dans de tels cas, le fichier doit être fermé et ensuite ré-ouvert :

 
Sélectionnez
fichier.close();
fichier.open("enregistrements_etudiants", "r");

23-1-5. Écrire et lire un fichier

Comme les fichiers sont des flux de caractères, les fonctions d'entrée et de sortie writeln, readf, etc. sont utilisées exactement de la même manière avec eux. La seule différence est que le nom de la variable File suivie d'un point doivent être ajoutés :

 
Sélectionnez
writeln("bonjour");         // écrit sur la sortie standard
stdout.writeln("bonjour");  // même chose que la ligne précédente
fichier.writeln("bonjour"); // écrit dans un fichier spécifique

23-1-6. eof() pour tester la fin du fichier

La méthode eof() détermine si la fin du fichier a été atteinte pendant la lecture du fichier. Quand c'est le cas, elle retourne true.

Par exemple, la boucle suivante sera exécutée jusqu'à la fin du fichier :

 
Sélectionnez
while (!fichier.eof()) {
   /* ... */
}

23-1-7. Le module std.file

Le module std.file contient des fonctions et des types qui sont utiles pour travailler avec le contenu des dossiers. Par exemple, la fonction exists peut être utilisée pour savoir si un fichier ou un dossier existe sur le système de fichier :

 
Sélectionnez
import std.file;
 
// ...
 
   if (exists(nomFichier)) {
      // Il y a un fichier ou un dossier qui porte ce nom
 
   } else {
      // Aucun fichier ou dossier portant ce nom
   }

23-2. La structure std.stdio.File

File utilise les mêmes caractères de mode que ceux qui sont utilisés par la fonction fopen du langage C.

Mode

Definition

r

Accès en lecture. Le fichier est ouvert et lu depuis le début.

r+

Accès en lecture et en écriture. Le fichier est ouvert pour être lu et écrit depuis le début.

w

Accès en écriture. Si le fichier n'existe pas, un fichier vide est créé. S'il existe déjà, son contenu est écrasé.

w+

Accès en lecture et en écriture. Si le fichier n'existe pas, un fichier vide est créé. S'il existe déjà, son contenu est écrasé.

a

Accès en ajout. Si le fichier n'existe pas, un fichier vide est créé. Si le fichier existe déjà, son contenu est conservé. Les données seront écrite à la fin du fichier.

a+

Accès en lecture et en ajout. Si le fichier n'existe pas, un fichier vide est créé. Si le fichier existe déjà, son contenu est conservé et le fichier est ouvert pour être lu au début et écrit à la fin.

Un caractère 'b' peut être ajouté à la chaîne de mode, comme dans "rb". Ceci peut avoir un effet sur les plateformes qui supportent le mode binaire, mais il est ignoré sur tous les systèmes POSIX.

La structure File est comprise dans le module std.stdio.

23-3. Écrire dans un fichier

Le fichier doit avoir été ouvert dans un des modes d'écriture :

 
Sélectionnez
void main()
{
   File fichier = File("enregistrements_etudiants", "w");
 
   fichier.writeln("Nom    : ", "Zafer");
   fichier.writeln("Numéro : ", 123);
   fichier.writeln("Classe : ", "1A");
}

Comme vous devez vous rappeler depuis le chapitre sur les chainesChaînes de caractères le type des littéraux tels que "enregistrements_etudiants" est string, qui consiste en une suite de caractères immuables. Pour cette raison, il n'est pas possible de construire des objets File à partir d'un nom mutable (par exemple char[]).

Le programme ci-dessus crée ou remplace le contenu d'un fichier nommé enregistrements_etudiants dans le dossier dans lequel il a été lancé (le dossier de travail du programme).

Les noms de fichiers peuvent contenir n'importe quel caractère autorisé par le système de fichier. Pour être portable, je n'utiliserai que des caractères ASCII.

23-4. Lire un fichier

Le fichier doit avoir été ouvert dans un des modes de lecture :

 
Sélectionnez
import std.stdio;
import std.string;
 
void main()
{
   File fichier = File("enregistrements_etudiants", "r");
 
   while (!fichier.eof()) {
      string ligne = chomp(fichier.readln());
      writeln("ligne lue -> |", ligne);
   }
}

Le programme ci-dessus lit toutes les lignes du fichier nommé enregistrements_etudiants et affiche ces lignes dans la sortie standard.

23-5. Exercice

Écrire un programme qui demande un nom de fichier à l'utilisateur, ouvre ce fichier, et écrit toutes les lignes non vides de ce fichier dans un autre fichier. Le nom du nouveau fichier peut être basé sur le nom du fichier original. Par exemple, si le nom du fichier original est foo.txt, le nouveau fichier peut être appelé foo.txt.out.

La solutionFichiers - Correction.

24. auto et typeof

24-1. auto

Quand on a défini les variables de type File dans le chapitre précédent, on a répété le nom du type des deux côtés de l'opérateur = :

 
Sélectionnez
File file = File("enregistrements_etudiants", "w");

Cela semble redondant. C'est également lourd et sujet à erreurs, surtout quand le nom du type est plus long.

 
Sélectionnez
NomDeTypeTresLong var = NomDeTypeTresLong(/* ... */);

Heureusement, le nom du type sur le côté gauche de l'affectation n'est pas nécessaire parce que le compilateur peut inférer le type de la variable depuis l'expression à droite de l'affectation. Pour que le compilateur infère le type, le mot-clé auto peut être utilisé :

 
Sélectionnez
auto var = NomDeTypeTresLong(/* ... */);

auto peut être utilisé avec n'importe quel type même si le type n'est pas écrit explicitement du côté droit :

 
Sélectionnez
auto duree = 42;
auto distance = 1.2;
auto accueil = "Hello";
auto vehicule = JoliVelo("bleu");

Même si auto est l'abréviation de automatique, il ne vient pas d'inférence automatique des types. Il vient de classe automatique de stockage, qui est une notion à propos de la durée de vie des variables. auto est utilisé quand aucun autre indicateur n'est approprié. Par exemple, la définition suivante n'utilise pas auto :

 
Sélectionnez
immutable i = 42;

Dans ce code, le compilateur infère le type de i en immutable int (nous verrons immutable dans un chapitre ultérieur).

24-2. typeof

typeof donne le type d'une expression (ce qui comprend les variables simples, les objets, les littéraux...).

Ce qui suit est un exemple de comment typeof peut être utilisé pour spécifier un type sans l'écrire explicitement :

 
Sélectionnez
int valeur = 100;       // déjà défini comme 'int'
 
typeof(valeur) valeur2; // signifie "type de valeur"
typeof(100) valeur3;    // signifie "type du littéral 100"

Les deux dernières définitions sont équivalentes à ce qui suit :

 
Sélectionnez
int valeur2;
int valeur3;

Il est évident que typeof n'est pas nécessaire dans des situations comme celles-ci, quand les types sont déjà connus. Ce mot-clé est surtout utile dans les définitions de modèles (templates), que nous verrons dans des chapitres ultérieurs.

24-3. Exercices

Comme nous n'avons vu précédemment, le type des littéraux tels que 100 est int (et non short, long, ou n'importe quel autre type). Écrire un programme pour déterminer le type des littéraux à virgule flottante comme 1.2.

typeof() et .stringof sont utiles pour ce programme.

La solutionauto et typeof - Correction.

25. Espace de nom (name space)

Tout nom est accessible depuis l'endroit où il a été défini jusqu'à l'endroit où l'on quitte sa portée, de même que dans toutes les portées que la sienne inclue. De ce point de vue, toute portée définie un espace de nom.

Les noms ne sont plus disponibles une fois que l'on a quitté leur portée :

 
Sélectionnez
void main()
{
   int dehors;
 
   if (uneCondition) {   //  les accolades commencent une nouvelle portée
      int dedans = 1;
      dehors = 2;        //  ça marche ; 'dehors' est défini à cet endroit
 
   } //  'dedans' n'est plus disponible eu delà de cet endroit
 
   dedans = 3;   //  ERREUR de compilation
                 //   'dedans' n'est pas disponible dans la portée extérieure.
}

Parce que dedans est défini dans la portée de la condition if, elle est disponible seulement dans cette portée. En revanche, outer est disponible et dans la portée globale et dans la portée intérieure.

Définir le même nom dans une portée intérieure n'est pas autorisé :

 
Sélectionnez
size_t taille = nombresImpairs.length;
 
if (uneCondition) {
   size_t taille = nombresPremiers.length; //  ERREUR de compilation
}

25-1. Définir les noms le plus proche possible de leur première utilisation

Comme nous l'avons fait dans tous les programmes jusqu'à maintenant, les variables doivent être définies avant leur première utilisation :

 
Sélectionnez
writeln(nombre);   //  ERREUR de compilation
                   //   nombre n'est pas encore connu
int nombre = 42;

Pour que ce code soit acceptable par le compilateur, nombre doit être défini avant d'être utilisé avec writeln. Même s'il n'y a pas de restriction sur combien de lignes avant il devrait être défini, une bonne pratique de programmation est de définir les variables le plus près possible de là où elles sont utilisées pour la première fois.

Voyons ceci dans un programme qui affiche la moyenne des nombres qu'il demande à l'utilisateur. Les programmeurs qui ont l'habitude d'utiliser d'autres langages de programmation peuvent être accoutumés à définir les variables au début des portées :

 
Sélectionnez
int nombre;                               //  ici
int[] nombres;                            //  ici
double valeurMoyenne;                     //  ici
 
write("Combien y a-t-il de nombres ? ");
 
readf(" %s", &nombre);
 
if (nombre >= 1) {
   nombres.length = nombre;
 
   // ... Supposons que le calcul se fasse ici ...
 
} else {
   writeln("ERREUR : Vous devez rentrer au moins un nombre !");
}

Comparez le code ci-avant avec celui qui suit et qui définit les variables plus tard, quand elles commencent à jouer un rôle dans le programme :

 
Sélectionnez
write("Combien y a-t-il de nombres ? ");
 
int nombre;                              //  ici
readf(" %s", &nombre);
 
if (nombre >= 1) {
   int[] nombres;                        //  ici
   nombres.length = nombre;
 
   double valeurMoyenne;                 //  ici
 
   // ... supposons qu'il y a des calculs ici ...
 
} else {
   writeln("ERREUR : vous devez entrer au moins un nombre !");
}

Même si définir toutes les variables au début peut sembler mieux au niveau de la structure, il y a plusieurs avantages à les définir le plus tard possible :

  • Vitesse : chaque définition de variable a un coût en vitesse dans le programme. Comme toutes les variables sont initialisées à 0, définir les variables au début entraînerait potentiellement une initialisation coûteuse et inutile si ces variables n'étaient pas utilisées plus tard.
  • Risque d'erreurs : chaque ligne qui est entre la définition et l'utilisation d'une variable entraîne un risque plus grand de faire des erreurs de programmation. Pour prendre un exemple, considérez une variable portant un nom aussi commun que « taille ». Il est possible d'utiliser cette variable par inadvertance pour une autre taille avant de l'utiliser réellement pour ce pour quoi elle a été définie. Au moment où la ligne de son utilisation prévue est atteinte, la variable peut avoir une valeur non attendue.
  • Lisibilité : quand le nombre de ligne dans une portée est devenu grand, il est probable que la définition d'une variable peut être trop loin dans le code source, forçant le programmeur à faire défiler le code pour trouver cette définition.
  • Maintenance du code : le code source est modifié et amélioré constamment : de nouvelles fonctionnalités sont ajoutées, d'anciennes fonctionnalités sont supprimées, les bogues sont corrigés, etc. Ces modifications rendent parfois nécessaire le déplacement d'un groupe de ligne dans une nouvelle fonction.
    Quand cela arrive, avoir toutes les variables définies près des lignes qui les utilisent rend plus facile le déplacement de celles-ci comme un groupe cohérent.
    Par exemple, dans le code précédent, toutes les lignes dans le bloc if peuvent être déplacées dans une nouvelle fonction.Au contraire, quand les variables sont toujours définies au début, si les lignes doivent être déplacées, les variables qui sont utilisées dans ces lignes doivent être identifiées une à une.

26. Boucles for

La boucle for a le même but que la boucle while. for permet d'écrire les définitions et expressions relatives à l'itération de la boucle sur la même ligne.

Même si for est utilisée beaucoup moins souvent que foreach en pratique, il est important de comprendre la boucle for en premier. Nous verrons foreach dans un chapitre ultérieur.

26-1. Sections de la boucle while

La boucle while évalue la condition et continue d'exécuter la boucle tant que la condition est vraie. Par exemple, une boucle pour afficher les nombres entre 1 et 10 peut vérifier la condition inférieur à 11 :

 
Sélectionnez
while (nombre < 11)

Itérer la boucle peut être fait en incrémentant un compteur à la fin de la boucle :

 
Sélectionnez
++nombre;

Pour être compilable en D, nombre doit avoir été défini avant sa première utilisation :

 
Sélectionnez
int nombre = 1;

Finalement, il y a ce qui est effectivement fait dans le corps de la boucle :

 
Sélectionnez
writeln(nombre);

Ces quatre sections peuvent être définies comme dans les commentaires ci-après :

 
Sélectionnez
int nombre = 1;         //  préparation
 
while (nombre < 11) {   //  vérification de la condition
   writeln(nombre);     //  travail de la boucle
   ++nombre;            //  itération
}

Les sections d'une boucle while sont exécutées dans l'ordre suivant pendant l'itération de la boucle while :

 
Sélectionnez
préparation

vérification de la condition
travail de la boucle
itération

vérification de la condition
travail de la boucle
itération

...

Une instruction break ou une exception levée peut également terminer la boucle.

26-2. Les sections d'une boucle for

for regroupe trois de ces sections sur une seule ligne. Elles sont écrites à l'intérieur des parenthèses de la boucle for, séparées par des points-virgules. Le corps de la boucle ne contient que ce que la boucle fait :

 
Sélectionnez
for (/* préparation */; /* condition */; /* itération */) {
   /* travail de la boucle */
}

Le même code est plus cohérent quand il est écrit avec une boucle for :

 
Sélectionnez
for (int nombre = 1; nombre < 11; ++nombre) {
   writeln(nombre);
}

L'avantage de la boucle for est plus évident quand le code de la boucle a un grand nombre d'instructions. L'expression qui incrémente la variable d'itération est visible sur la ligne for au lieu d'être au mélangée avec les autres instructions de la boucle.

Les sections de la boucle for sont exécutées dans le même ordre que la boucle while.

Les instructions break et continue fonctionnent exactement de la même façon dans la boucle for.

La seule différence entre les boucles while et for est l'espace de nom de la variable d'itération. Ceci est expliqué ci-dessous.

Bien que ce soit généralement le cas, la variable d'itération n'a pas à être un entier, et n'est pas forcément modifiée par une opération d'incrémentation. Par exemple, la boucle suivante est utilisée pour afficher les moitiés des valeurs flottantes précédentes :

 
Sélectionnez
for (double valeur = 1; valeur > 0.001; valeur /= 2) {
   writeln(valeur);
}

l'information énoncée au début de cette partie est techniquement incorrecte mais couvre la majorité des cas d'utilisations de la boucle for, surtout pour les programmeurs qui ont de l'expérience en C ou en C++. En réalité, la boucle for du D n'a pas trois sections séparées par des points-virgules. Elle a deux sections, dont la première contient la préparation et la condition de la boucle. Sans aller dans les détails de cette syntaxe, voici comment définir deux variables de types différents dans la section de préparation :

 
Sélectionnez
for ({ int i = 0; double d = 0.5; } i < 10; ++i) {
   writeln("i: ", i, ", d: ", d);
   d /= 2;
}

Notez que la section de préparation est la partie entre les accolades qui sont entre les parenthèses et qu'il n'y a pas de point-virgule entre la section de préparation et la section de condition.

26-3. Les sections peuvent être vides

Les trois sections de la boucle for peuvent être laissées vides :

  • Parfois une variable d'itération n'est pas nécessaire, par exemple parce qu'une variable déjà définie est utilisée.
  • Parfois la sortie de la boucle peut se faire au moyen d'une instruction break au lieu d'être basée sur la condition de boucle.
  • Parfois les expressions d'itération dépendent de certaines conditions qui seront vérifiées dans le corps de la boucle.

Quand toutes les sections sont vides, la boucle for correspond à une boucle infinie :

 
Sélectionnez
for ( ; ; ) {
   // ...
}

Une telle boucle peut être utilisée pour ne jamais finir ou finir avec une instruction break.

26-4. L'espace de nom de la variable d'itération

La seule différence entre la boucle for et la boucle while est l'espace de nom de la variable définie pendant la préparation de la boucle : la variable n'est accessible qu'à l'intérieur de la boucle, et non à l'extérieur :

 
Sélectionnez
for (int i = 0; i < 5; ++i) {
   // ...
}
 
writeln(i);   //  Erreur de compilation
              //   i n'est pas accessible ici

En revanche, quand la variable est définie dans l'espace de nom qui contient la boucle while, le nom est accessible même après la boucle :

 
Sélectionnez
int i = 0;
 
while (i < 5) {
   // ...
   ++i;
}
 
writeln(i);   //  Ça fonctionne ; i est accessible ici

Nous avons vu le conseil de définir les noms le plus près possible de leur première utilisation dans le chapitre précédent. De façon similaire, plus l'espace de nom d'une variable est petit, mieux c'est. De ce fait, quand la variable d'itération n'est pas utilisée hors de la boucle, for est mieux que while.

26-5. Exercices

  1. Afficher le tableau 9×9 suivant en utilisant deux boucles for, l'une à l'intérieur de l'autre :
    Image non disponible
  2. Utiliser une ou plusieurs boucles for pour afficher le caractère * de façon à produire des formes géométriques :
 

Image non disponible

 

Les solutionsLa boucle for - Correction.

27. L'opérateur ternaire ?:

L'opérateur ternaire ?: a un fonctionnement très similaire de celui de l'instruction if-else :

 
Sélectionnez
if (/* condition */) {
    /* ... expression(s) à exécuter si vrai */
 
} else {
    /* ... expression(s) à exécuter si faux */
}

L'instruction if exécute soit le bloc correspondant au cas où la condition est vraie, soit le bloc correspondant au cas où la condition est fausse. Comme vous devez vous en rappeler, étant une instruction, elle n'a pas de valeur ; if affecte principalement l'exécution des blocs de code.

En revanche, l'opérateur ?: est une expression. En plus de fonctionner d'une manière similaire à l'instruction if-else, il produit une valeur. Ce qui suit est un équivalent du code ci-dessus :

 
Sélectionnez
/* condition */ ? /* expression si vrai */ : /* expression si faux */

L'opérateur ?: utilise trois expressions, d'où son nom d'opérateur ternaire.

La valeur qui est produite par cet opérateur est soit la valeur de l'expression si vrai, soit celle de l'expression si faux. Du fait que c'est une expression, il peut être utilisé partout où les expressions peuvent être utilisées.

Les exemples suivant comparent l'opérateur ?: et l'instruction if-else. L'opérateur ternaire est plus concis dans les cas similaires à ces exemples.

  • Initialisation. Pour initialiser à 366 s'il s'agit d'une année bissextile, à 365 sinon :

     
    Sélectionnez
    int jours = estAnneeBissextile ? 366 : 365;
  • Avec if, une façon de faire est de définir la variable sans valeur initiale explicite et d'affecter la valeur voulue :

     
    Sélectionnez
    int jours;
     
    if (estAnneeBissextile) {
        jours = 366;
     
    } else {
        jours = 365;
    }
  • Une alternative avec if est d'initialiser la variable avec la valeur des années non bissextiles et de l'incrémenter s'il s'agit d'une année bissextile :

     
    Sélectionnez
    int jours = 365;
     
    if (estAnneeBissextile) {
         ++jours;
    }
  • Affichage. Afficher un message différemment selon une condition :

     
    Sélectionnez
    writeln("Le verre est à moitié ",
            estOptimiste ? "plein." : "vide.");
  • Avec if, la première et la dernière parties peuvent être affichées séparément :

     
    Sélectionnez
    write("Le verre est à moitié ");
     
    if (estOptimiste) {
        writeln("plein.");
     
    } else {
        writeln("vide.");
    }
  • Ou alors, le message entier peut être affiché séparément :

     
    Sélectionnez
    if (estOptimiste) {
        writeln("Le verre est à moitié plein.");
     
    } else {
        writeln("Le verre est à moitié vide.");
    }
  • Calcul. Augmenter le score d'un gagnant au backgammon de 2 points ou 1 point selon si le jeu s'est fini sur un gammon ou pas :

     
    Sélectionnez
    score += estGammon ? 2 : 1;
  • Transposition directe avec if :

     
    Sélectionnez
    if (estGammon) {
        score += 2;
     
    } else {
        score += 1;
    }
  • Ou alors, on peut d'abord incrémenter de 1 et incrémenter une seconde fois s'il y a eu gammon :
 
Sélectionnez
++score;
 
if (estGammon) {
    ++score;
}

Comme on peut le voir dans les exemples précédant, le code est plus concis et plus clair avec l'opérateur ternaire dans certaines situations.

27-1. Les types des expressions de sélection doivent correspondre

La valeur de l'opérateur ?: est soit la valeur de l'expression si vrai, soit la valeur de l'expression si faux. Les types de ces expressions n'ont pas besoin d'être les mêmes mais ils doivent correspondre. Par exemple, ils peuvent être tous deux des types entiers comme int et long mais ne peuvent pas être des types qui ne se correspondent pas, comme int et string.

Dans les exemples précédents, les valeurs sélectionnées selon la valeur de estAnneeBissextile étaient 366 et 365. Les deux valeurs sont du type int et se correspondent.

Pour voir une erreur de compilation causée par des valeurs des expressions ne correspondant pas, considérons la composition d'un message qui rapporte le nombre d'objets à expédier. Affichons « Une douzaine » quand la valeur est 12 : « Une douzaine d'objets seront expédiés ». Dans les autres cas, on affiche le nombre exact : « 3 objets seront expédiés ».

On pourrait penser que la partie variable du message peut être sélectionnée avec l'opérateur ?: :

 
Sélectionnez
writeln((nombre == 12) ? "Une douzaine d'" : nombre, //  ERREUR de compilation
        " objets seront expédiés.");

Les expression ne correspondent pas parce que le type de "Une douzaine d'" est string et le type de nombre est int.

Une solution est de convertir nombre en string. La fonction to!string du module std.conv donne une valeur de type string depuis le paramètre donné :

 
Sélectionnez
import std.conv;
// ...
    writeln((nombre == 12) ? "Une douzaine d'" : to!string(nombre),
            " objets seront expédiés.");

Maintenant que les expressions de sélection de l'opérateur ?: sont toutes deux du type string, le code compile et affiche le message attendu.

27-2. Exercice

Le programme doit lire une valeur nette de type int. Quand cette valeur est positive, on l'interprète comme un gain et quand elle est négative, on l'interprète comme une perte.

Le programme doit afficher un message qui contient « gagnés » ou « perdus », selon si la quantité est positive ou négative. Par exemple, « 100€ gagnés » ou « 70€ perdus ». Même s'il se pourrait que ce soit plus approprié, n'utilisez pas l'instruction if dans cet exercice.

La solutionL'opérateur ternaire ?: - Correction.

28. Littéraux

Les programmes effectuent leur tâche en manipulant les valeurs de variables et d'objets. Ils produisent de nouvelles valeurs et de nouveaux objets en les utilisant avec des fonctions et des opérateurs.

Certaines valeurs n'ont pas besoin d'être produites pendant l'exécution du programme ; elles sont écrites directement dans le code source. Par exemple, la valeur flottante 0.75 et la chaîne "Prix total :" ci-dessous ne sont pas calculées :

 
Sélectionnez
prixSoldé = prixDeBase * 0.75;
prixTotal += nombre * prixSoldé;
writeln("Prix total : ", prixTotal);

De telles valeurs écrites littéralement dans le code source sont appelées littéraux. Nous avons utilisé beaucoup de littéraux dans les programmes que nous avons écrits jusqu'à maintenant. Nous allons couvrir tous les types de littéraux et leur syntaxe.

28-1. Littéraux entiers

Les littéraux entier peuvent être écrits de quatre façons différentes : le système décimal que nous utilisons dans la vie de tous les jours ; les systèmes hexadécimal et binaire, qui sont plus adaptés dans certains calculs ; et le système octal, qui peut être nécessaire dans des cas très rares.

Pour rendre le code plus lisible ou pour n'importe quelle autre raison, il est possible d'insérer des caractères _ n'importe où parmi les caractères des littéraux entiers. Par exemple, pour séparer les nombres à intervalles de trois chiffres comme dans 1_234_567. Ces caractères sont optionnels ; ils sont ignorés par le compilateur.

  • Dans le système décimal : les littéraux sont indiqués par les chiffres décimaux exactement de la même manière que nous le faisons dans la vie de tous les jours, comme dans 12. Dans ce système, le premier chiffre ne peut pas être 0 parce que ce chiffre est réservé pour indiquer le système octal dans la plupart des autres langages. En D, les littéraux entiers ne peuvent pas commencer avec le chiffre 0, afin d'éviter des bogues causés par cette différence subtile. Ceci ne s'applique par à 0 lui-même : 0 est zéro.
  • Dans le système hexadécimal : les littéraux commencent par 0x ou 0X et incluent les chiffres du système hexadécimal : « 0123456789abcdef » et « ABCDEF » comme dans 0x12ab00fe.
  • Dans le système octal : les littéraux sont indiqués par le modèle octal du module conv et inclue les chiffres du système octal : « 01234567 », comme dans octal!576.
  • Dans le système binaire : les littéraux commencent par 0b ou 0B et incluent les chiffres du système binaire : 0 et 1, comme dans 0b01100011.

28-2. Les types des littéraux entiers

Exactement comme n'importe quelle autre valeur, tout littéral est typé. Les types des littéraux ne sont pas indiqués explicitement comme int, double, etc. Le compilateur infère le type depuis la valeur et la syntaxe du littéral lui-même.

Même si la plupart du temps les types des littéraux ne sont pas important, parfois les types peuvent ne pas correspondre aux expressions dans lesquelles ils sont utilisés. Dans de tels cas, le type doit être explicité.

Par défaut, les littéraux entiers sont inférés comme étant de type int. Quand la valeur est trop large pour être représenté par un int, le compilateur utilise le procédé suivant pour décider de quel type le littéral est :

  • Si la valeur du littéral ne rentre pas dans int et qu'il est spécifié dans le système décimal alors son type est long.
  • Si la valeur du littéral ne rentre pas dans int et qu'il est spécifié dans n'importe quel autre système alors le compilateur essaie uint, long et ulong, dans cet ordre, selon quel type pourra contenir la valeur.

Pour voir ce procédé en action, essayons le programme suivant qui se sert de de typeof et stringof :

 
Sélectionnez
import std.stdio;
 
void main()
{
   writeln("\n--- Ces nombres sont écrits en décimal ---");
 
   // rentre dans int, le type est donc int
   writeln(        2_147_483_647, "\t\t",
            typeof(2_147_483_647).stringof);
 
   // ne rentre pas dans int et est décimal, le type est donc long
   writeln(        2_147_483_648, "\t\t",
            typeof(2_147_483_648).stringof);
 
   writeln("\n--- Ces nombres ne sont PAS écrits en décimal ---");
 
   // rentre dans int, le type est donc int
   writeln(        0x7FFF_FFFF, "\t\t",
            typeof(0x7FFF_FFFF).stringof);
 
   // ne rentre pas dans un int et n'est pas décimal, le type est donc uint
   writeln(        0x8000_0000, "\t\t",
            typeof(0x8000_0000).stringof);
 
   // ne rentre pas dans un uint et n'est pas décimal, le type est donc long
   writeln(        0x1_0000_0000, "\t\t",
            typeof(0x1_0000_0000).stringof);
 
   // ne rentre pas dans un long et n'est pas décimal, le type est donc ulong
   writeln(        0x8000_0000_0000_0000, "\t\t",
            typeof(0x8000_0000_0000_0000).stringof);
}

La sortie :

 
Sélectionnez
--- Ces nombres sont écrits en décimal ---
2147483647     int
2147483648     long

--- Ces nombres ne sont PAS écrits en décimal ---
2147483647     int
2147483648     uint
4294967296     long
9223372036854775808     ulong

28-2-1. Le suffixe L

Indépendamment de la grandeur de la valeur, si elle finit par un L comme dans 10L, le type est long.

28-2-2. Le suffixe U

Indépendamment de la grandeur de la valeur, si elle finit par un U comme dans 10U, le type est unsigned. La minuscule 'u' peut également être utilisée.

Les indicateurs L et U peuvent être utilisés ensemble dans n'importe quel ordre. Par exemple, 7UL et 8LU sont toutes les deux ulong.

28-3. Les littéraux flottants

Les littéraux flottants peuvent être spécifiés soit dans le système décimal (exemple : 1.1234), soit dans le système hexadécimal (0x9a.bc). (NdT : le séparateur décimal est un point).

Dans le système décimal : un exposant peut être ajouté après le caractère e ou E, signifiant « fois 10 puissance ». Par exemple, 3.4e5 signifie « 3,4 fois 10 puissance 5 ». Un caractère + peut aussi être indiqué avant la valeur de l'exposant, mais cela n'a aucun effet. Par exemple, 5.6e2 et 5.6e+2 sont la même chose.

Le caractère - tapé avant la valeur de l'exposant change son sens, qui devient "divisé par 10 puissance". Par exemple, 7.8e-3 signifie « 7.8 divisé par 10 puissance 3 ».

Dans le système hexadécimal : la valeur commence par 0x ou 0X et les chiffres de part et d'autre du point sont ceux du système hexadécimal. Comme dans ce système, e et E sont des chiffres valides, l'exposant est indiqué avec p ou P.

Une autre différence est que l'exposant ne veut pas dire « 10 puissance » mais « 2 puissance ». Par exemple, la partie P4 dans 0xabc.defP4 veut dire « 2 puissance 4 ».

Les littéraux en virgule flottante ont presque toujours un point mais il peut être omis si un exposant est indiqué. Par exemple, 2e3 est un littéral flottant dont la valeur est 2000.

La valeur avant le point peut être omise si elle est nulle. Par exemple, .25 est un littéral qui a la valeur d'un quart.

Les tirets du bas optionnels (_) peuvent aussi être utilisés avec les littéraux flottant : 1_000.5.

28-3-1. Les types des littéraux flottant

Sauf si explicitement indiqué, le type d'un littéral flottant est double. Les indicateurs f et F signifient float, et l'indicateur L signifie real. Par exemple, 1.2 est double, 3.4f est float et 5.6L est real.

28-4. Les littéraux de caractères

Les littéraux de caractères sont indiqués avec des apostrophes (guillemets anglais simples) comme dans 'a', '\n', '\x21', etc.

Il y a différentes manières d'écrire un littéral de caractère.

Directement. Le caractère peut être écrit directement avec le clavier ou copié depuis un texte : « a », « ş », etc.

Avec une lettre spéciale. Le caractère est indiqué par un antislash suivi d'une lettre spéciale ; par exemple, le caractère antislash lui-même peut être désigné de cette manière : '\\'. Voici la liste des lettres spéciales :

Syntaxe

Définition

\'

guillemet simple

\"

guillemet double

\?

point d'interrogation

\\

antislash

\a

alerte (son de cloche dans certains terminaux)

\b

caractère de suppression

\f

nouvelle page

\n

nouvelle ligne

\r

retour chariot

\t

tabulation

\v

tabulation verticale

Par son code ASCII étendu. Les codes peuvent être indiqués soit dans le système hexadécimal, soit dans le système octal. Quand le système hexadécimal est utilisé, le littéral doit commencer par \x et doit utiliser deux chiffres pour le code ; quand le système octal est utilisé, le littéral doit commencer par \ et comporter de un à trois chiffres. Par exemple, les littéraux 'x21' et '\41' désignent tous les deux le point d'exclamation.

Par son code Unicode. Quand le littéral est indiqué avec un u suivi par 4 chiffres hexadécimaux, son type est wchar. Quand il est indiqué avec U suivi de 8 chiffres hexadécimaux, son type est dchar. Par exemple, '\u011e' et '\U0000011e' désignent tous les deux le caractère Ğ et ont respectivement les types wchar et dchar.

Par son nom d'entité (comme en HTML). Les caractères qui ont un nom peuvent être désignés par ce nom en utilisant la syntaxe '\&nom;' (voir le tableau des noms de caractères). Par exemple, '\&euro;' désigne le caractère €, '\&hearts;' le caractère ♥ et '\&copy;' le caractère ©.

28-5. Les littéraux de chaînes

Les littéraux de chaînes sont une combinaison de littéraux de caractères et peuvent être désignés de diverses manières.

28-5-1. Les littéraux de chaînes entourés de guillemets anglais doubles

La manière la plus commune d'écrire un littéral de chaîne est d'entrer les caractères entre guillemets anglais doubles, comme dans "salut". Les caractères individuels d'un littéral de chaîne suivent les règles des littéraux de caractères. Par exemple, le littéral "A4 ka\u011fıt: 3\&frac12;TL" est le même que "A4 kağıt: 3½TL".

28-5-2. Les littéraux de chaîne Wysiwyg

Quand les littéraux de chaînes sont écrits dans des guillemets obliques (accents graves, apostrophes inversées, backticks, backquotes), les caractères de la chaîne n'obéissent par aux règles spéciales de la syntaxe d'un littéral de caractère. Par exemple, le littéral `c:\nurten` peut être un nom de répertoire sur le système d'exploitation Windows. S'il était écrit avec des guillemets doubles, le '\n' désignerait le caractère de nouvelle ligne :

 
Sélectionnez
writeln(`c:\nurten`);
writeln("c:\nurten");

Sortie :

 
Sélectionnez
c:\nurten  ← wysiwyg (what you see is what you get, ce que vous voyez est ce que vous obtenez)
c:         ← Le littéral caractère est pris comme une nouvelle ligne
urten

Les littéraux de chaîne Wysiwyg (NdT : What You See Is What You Get : « ce que vous voyez est ce que vous obtenez ») peuvent également être écrits entre guillemets doubles en les préfixant avec le caractère r : r"c:\nurten" est aussi un littéral de chaîne wysiwyg.

28-5-3. Littéraux de chaînes hexadécimaux

Dans des situations où tous les caractères d'une chaînes doivent être indiqués dans le système hexadécimal, au lieu d'entrer \x avant chacun d'eux, un caractère x peut être ajouté avant le guillemet double ouvrant. Dans ce cas, chaque caractère du littéral de chaîne est pris comme étant écrit en hexadécimal. De plus, le littéral peut contenir des espaces et ceux-ci seront ignorés par le compilateur. Par exemple, "\x44\x64\x69\x6c\x69" et x"44 64 69 6c 69" désignent la même chaîne.

28-5-4. Littéraux de chaîne délimités

Le littéral de chaîne peut contenir des délimiteurs qui sont entrés à l'intérieur des guillemets anglais doubles. Ces délimiteurs ne sont pas considérés comme faisant partie de la valeur du littéral. Les littéraux de chaîne délimités commencent par un q avant le guillemet double ouvrant. Par exemple, la valeur de q".hello." est "hello", les points ne font pas partie de la valeur. S'il finit par une nouvelle ligne, le délimiteur peut avoir plus d'un caractère :

 
Sélectionnez
writeln(q"MON_DELIMITEUR
première ligne
seconde ligne
MON_DELIMITEUR");

MON_DELIMITEUR ne fait pas partie de la valeur :

 
Sélectionnez
première ligne
seconde ligne

28-5-5. Littéraux de chaîne « jetons »

Les littéraux de chaînes qui commencent par q et qui utilisent { et } comme délimiteurs peuvent contenir du code D correct :

 
Sélectionnez
auto str = q{int nombre = 42; ++nombre;};
writeln(str);

La sortie :

 
Sélectionnez
int nombre = 42; ++nombre;

Cette fonctionnalité est particulièrement utile pour aider les éditeurs de textes à colorer le code D dans les chaînes de caractères.

28-5-6. Type des littéraux de chaînes

Par défaut, le type d'un littéral de chaîne est immutable(char)[]. On peut ajouter un caractère c, w ou d pour spécifier explicitement le type de la chaîne comme étant immutable(char)[], immutable(wchar)[] ou immutable(dchar)[] respectivement. Par exemple, les caractères de la chaîne "hello"d sont de type immutable(dchar).

Nous avons vu dans le chapitre sur les chainesChaînes de caractères que ces trois types de chaînes ont respectivement pour alias string, wstring et dstring.

28-6. Les littéraux sont calculés lors de la compilation

Il est possible d'utiliser des expressions littérales. Par exemple, au lieu d'écrire le nombre total de secondes du mois de janvier comme 2678400 ou 2_678_400, il est possible de l'écrire par un calcul plus explicite comme 60 * 60 * 24 * 31. Les opérations de multiplication n'influent pas sur la vitesse d'exécution du programme car le programme obtenu après compilation est exactement le même que si 2678400 avait été écrit à la place de l'opération.

La même chose s'applique aux littéraux de chaînes. Par exemple, l'opération de concaténation dans "bonjour" ~ " le " ~ "monde" est exécutée lors de la compilation, pas pendant l'exécution. Le programme est compilé comme si le code ne contenait que le littéral de chaîne "bonjour le monde".

28-7. Exercices

  1. La ligne suivante donne une erreur de compilation :

     
    Sélectionnez
    int quantite = 10_000_000_000;    //  ERREUR de compilation
  2. Modifiez le programme de sorte que la ligne puisse être compilée et que quantite vaille dix milliards.

  3. Écrire un programme qui augmente la valeur d'une variable et qui l'affiche dans une boucle infinie. La valeur doit toujours être affichée sur la même ligne :

     
    Sélectionnez
    Nombre : 25774       ← toujours sur la même ligne
  4. Un caractère spécial autre que '\n' peut être utilisé ici.

Les solutionsLittéraux - Correction.

29. Sortie formatée

Ce chapitre traite des fonctionnalités du module std.format de Phobos, et non des fonctionnalités du langage D lui-même. Les indicateurs de formatage d'entrée et de sortie du D sont similaires à ceux du C. Avant d'aller plus loin, je voudrais résumer ici les indicateurs de format et les drapeaux :

Drapeaux

(peuvent être combinés)

-

aligner à gauche

+

afficher le signe

#

afficher d'une autre manière

0

compléter avec des zéros

espace

compléter avec des espaces

Indicateurs de format

 

s

par défaut

b

binaire

d

décimal

o

octal

x,X

hexadécimal

e,E

écriture scientifique

f,F

virgule flottante

g,G

comme e ou f

a,A

virgule flottante hexadécimal

Nous avons utilisé des fonction comme writeln avec de multiples paramètres si nécessaire. Les paramètres sont alors convertis vers leur représentation en chaîne de caractère et envoyés vers la sortie.

Parfois, ce n'est pas suffisant. La sortie peut devoir suivre un certain format. Jetons un œil sur le code suivant, utilisé pour afficher les éléments d'une facture :

 
Sélectionnez
éléments ~= 1.23;
éléments ~= 45.6;
 
for (int i = 0; i != éléments.length; ++i) {
   writeln("Élément ", i + 1, " : ", éléments[i]);
}

La sortie :

 
Sélectionnez
Élément 1 : 1.23
Élément 2 : 45.6

L'information est correcte. Cependant, il est parfois souhaitable d'avoir un format de sortie différent. Par exemple, on pourrait vouloir qu'il y ait toujours deux chiffres après la virgule et que les points soient alignés, comme dans la sortie suivante :

 
Sélectionnez
Élément 1 :      1.23
Élément 2 :     45.60

Dans de tels cas, la sortie formatée se révèle utile. Les fonctions de sortie que nous avons vues jusqu'à maintenant ont des équivalents contenant la lettre f, pour « formaté », dans leur nom : writef() et writefln(). Le premier paramètre de ces fonctions est une chaîne de formatage qui décrit comment les autres paramètres doivent être affichés.

Par exemple, la chaîne de formatage qui ferait afficher la sortie souhaitée ci-dessus à writefln() est la suivante :

 
Sélectionnez
writefln("Élément %d : %9.02f", i + 1, éléments[i]);

La chaîne de formatage contient des caractères normaux qui sont affichés tels quels, ainsi que des indicateurs de format spéciaux qui correspondent à chaque paramètre à afficher. Les indicateurs de format commencent par le caractère % et finissent par un caractère de formatage. La chaîne de formatage ci-dessus contient deux indicateurs de format : %d et %9.02f.

Chaque indicateur est associé aux paramètres qui suivent dans l'ordre. Par exemple, %d est associé avec i+1 et %9.02f est associé avec éléments[i]. Chaque indicateur donne le format du paramètre auquel il correspond (les indicateurs de format peuvent également avoir des paramètres numériques ; nous verrons cela plus loin dans le chapitre).

Tous les autres caractères de la chaîne de format qui ne font pas partie d'un indicateur de format sont affichés tels quels. Ils correspondent aux caractères surlignés dans la chaîne suivante : "Élément %d:%9.02f".

Les indicateurs de format comportent 6 parties, la plupart desquelles sont optionnelles. La partie nommée position sera expliqué un peu plus tard. Les cinq autres parties sont les suivantes (note : les espaces entre ces parties sont insérés ici pour des raisons de lisibilité, ils ne font pas partie des indicateurs) :

% drapeaux largeur précision caractère_de_formatage

Le caractère % au début et le caractère de formatage à la fin sont requis, les autres sont optionnels.

Du fait que % a une signification spécifique dans les chaînes de formatage, quand un % doit être affiché, il doit être écrit comme ceci : %%.

29-1. Caractères de formatage

  • b : le paramètre entier est affiché dans le système binaire.
  • o : le paramètre entier est affiché dans le système octal.
  • x et X : le paramètre entier est affiché dans le système hexadécimal ; avec des minuscules pour x et des majuscules pour X.
  • d : le paramètre entier est affiché dans le système décimal ; un signe négatif est également affiché s'il s'agit d'un type signé et que la valeur est strictement négative.

     
    Sélectionnez
    int valeur = 12;
     
    writefln("Binaire     : %b", valeur);
    writefln("Octal       : %o", valeur);
    writefln("Hexadécimal : %x", valeur);
    writefln("Décimal     : %d", valeur);
  • Sortie :
 
Sélectionnez
Binaire     : 1100
Octal       : 14
Hexadécimal : c
Décimal     : 12
  • e : le paramètre flottant est affiché selon les règles suivantes :

    • un seul chiffre avant le point ;
    • un point si la précision n'est pas nulle ;
    • des chiffres après le point, en nombre déterminé par la précision (6 par défaut) ;
    • le caractère e (signifiant « 10 puissance ») ;
    • le signe + ou -, selon si l'exposant est plus grand ou plus petit que zéro ;
    • l'exposant, constitué d'au moins deux chiffres.
  • E : pareil que e, sauf que le caractère E est affiché à la place de e.
  • f et F : le paramètre flottant est affiché dans le système décimal ; il y a au moins un chiffre avant le point et la précision par défaut est 6.
  • g : pareil que f si l'exposant est entre -5 et précision ; sinon pareil que e. précision n'indique pas le nombre de chiffres après le point, mais les chiffres significatifs de la valeur complète . S'il n'y a pas de chiffres significatifs après le point, le point n'est pas affiché. Les zéros les plus à droite après le point ne sont pas affichés.
  • G : pareil que pour g mais avec E ou F.
  • a : le paramètre en virgule flottante est affiché dans le format flottant hexadécimal :

    • les caractères 0x ;
    • un seul chiffre hexadécimal ;
    • le point si la précision n'est pas nulle ;
    • les chiffres après le point, dont le nombre est déterminé par la précision ; si la précision est indiquée, il y a autant de chiffres que nécessaire ;
    • le caractère p (signifiant « 2 puissance ») ;
    • le caractère + ou - selon si l'exposant est plus grand ou plus petit que zéro ;
    • l'exposant consistant en au moins un chiffre (l'exposant de la valeur 0 est 0).
  • A : pareil que a, mais avec 0X et P.

     
    Sélectionnez
    double valeur = 123.456789;
     
    writefln("avec e : %e", valeur);
    writefln("avec f : %f", valeur);
    writefln("avec g : %g", valeur);
    writefln("avec a : %a", valeur);
  • Sortie :
 
Sélectionnez
avec e : 1.234568e+02
avec f : 123.456789
avec g : 123.457
avec a : 0x1.edd3c07ee0b0bp+6
  • s : la valeur est affichée de la même manière que dans une sortie non formatée, selon le type de paramètre :

    • les valeurs booléennes comme true ou false ;
    • les valeurs entières comme avec %d ;
    • les valeurs en virgule flottante comme avec %g ;
    • les chaînes codées en UTF-8 ; la précision détermine le nombre maximal d'octets à utiliser (souvenez vous que dans le codage UTF-8, le nombre d'octets n'est pas le même que le nombre de caractères ; par exemple la chaîne "ağ" a deux caractères, consistant en un total de trois octets) ;
    • les structures et les classes comme le retour de la fonction toString() de leur type ; la précision détermine le nombre maximal d'octets à utiliser ;
    • les tableaux avec leurs éléments côte à côte.

       
      Sélectionnez
      bool b = true;
      int i = 365;
      double d = 9.87;
      string s = "formatée";
      auto o = File("fichier_test", "r");
      int[] a = [ 2, 4, 6, 8 ];
       
      writefln("booléen : %s", b);
      writefln("entier  : %s", i);
      writefln("double  : %s", d);
      writefln("chaîne  : %s", s);
      writefln("objet   : %s", o);
      writefln("tableau : %s", a);
    • Sortie :
 
Sélectionnez
booléen : true
entier  : 365
double  : 9.87
chaîne  : formatée
objet   : File(55738FA0)
tableau : [2, 4, 6, 8]

29-2. Largeur

Cette partie détermine la largeur du champ dans lequel le paramètre est affiché. Si la largeur est indiquée avec le caractère *, alors c'est le paramètre suivant qui indique cette largeur. Si la largeur est une valeur négative, le drapeau - est implicite.

 
Sélectionnez
int valeur = 100;
 
writefln("Dans un champ de 10 caractères : %10s", valeur);
writefln("Dans un champ de 5 caractères  : %5s", valeur);

Sortie :

 
Sélectionnez
Dans un champ de 10 caractères :        100
Dans un champ de 5 caractères  :   100

29-3. Précision

La précision est indiquée après le point dans l'indicateur de format. Pour les types en virgule flottante, elle détermine la précision de la représentation des valeurs. Si la précision est indiquée par le caractère *, la précision est obtenue à partir du paramètre suivant (ce paramètre doit être un int). Les précisions négatives sont ignorées.

 
Sélectionnez
double valeur = 1234.56789;
 
writefln("%.8g", valeur);
writefln("%.3g", valeur);
writefln("%.8f", valeur);
writefln("%.3f", valeur);

Sortie :

 
Sélectionnez
1234.5679
1.23e+03
1234.56789000
1234.568
 
Sélectionnez
auto nombre = 0.123456789;
writefln("Nombre : %.*g", 4, nombre);

Sortie :

 
Sélectionnez
Nombre : 0.1235

29-4. Drapeaux

On peut indiquer plus d'un drapeau à la fois.

  • - : la valeur est alignée à gauche dans son champ ; ce drapeau annule le drapeau 0

     
    Sélectionnez
    int valeur = 123;
     
    writefln("Normalement aligné à droite : |%10d|", valeur);
    writefln("Aligné à gauche             : |%-10d|", valeur);
  • Sortie :
 
Sélectionnez
Pas d'effet sur les valeurs négatives : -50
Valeur positive avec le drapeau +     : +50
Valeur positive sans le drapeau +     : 50
  • + : si la valeur est positive, elle est préfixée par le caractère + ; ce drapeau annule le drapeau espace.

     
    Sélectionnez
    writefln("Pas d'effet sur les valeurs négatives : %+d", -50);
    writefln("Valeur positive avec le drapeau +     : %+d", 50);
    writefln("Valeur positive sans le drapeau +     : %d", 50);
  • Sortie :
 
Sélectionnez
Pas d'effet sur les valeurs négatives : -50
Valeur positive avec le drapeau +     : +50
Valeur positive sans le drapeau +     : 50
  • # : affiche la valeur sous une autre forme, selon caractère_de_formatage.

    • o : le premier caractère de la valeur octale est toujours 0 ;
    • x et X : si la valeur n'est pas zéro, elle est préfixée avec 0x ou 0X ;
    • virgules flottantes : un point est affiché même s'il n'y a pas de chiffres significatifs après le point ;
    • g et G : même les zéros non significatifs après le point sont affichés.

       
      Sélectionnez
      writefln("L'octal commence par un 0        : %#o", 1000);
      writefln("L'hexadécimal commence par 0x    : %#x", 1000);
      writefln("Un point même si non nécessaire  : %#g", 1f);
      writefln("Les zéros à droite sont affichés : %#g", 1.2);
    • Sortie :
 
Sélectionnez
L'octal commence par un 0        : 01750
L'hexadécimal commence par 0x    : 0x3e8
Un point même si non nécessaire  : 1.00000
Les zéros à droite sont affichés : 1.20000
  • 0 : le champ est complété avec des zéros (sauf si la valeur est nan ou infinity) ; si la précision est également indiquée, ce drapeau est ignoré.

     
    Sélectionnez
    writefln("Dans un champ de 8 caractères : %08d", 42);
  • Sortie :
 
Sélectionnez
Dans un champs de 8 caractères : 00000042
  1. caractère espace : si la valeur est positive, un caractère espace est préfixé pour aligner les valeurs négatives et positives.

     
    Sélectionnez
    writefln("Pas d'effet sur les valeurs négatives : % d", -34);
    writefln("Valeur positive avec un espace        : % d", 56);
    writefln("Valeur positive sans espace           : %d", 56);
  2. Sortie :
 
Sélectionnez
Pas d'effet sur les valeurs négatives : -34
Valeur positive avec un espace        :  56
Valeur positive sans espace           : 56

29-5. Paramètres de position

Nous avons vu ci-dessus que les paramètres sont associés un à un avec les indicateurs dans la chaîne de formatage. Il est possible d'utiliser des numéros de position dans les indicateurs de format. Ceci permet d'associer des indicateurs avec des paramètres spécifiques. Les paramètres sont numérotés de façon croissante, en commençant par un 1. Les numéros de paramètres sont indiqués juste après le caractère %, suivi d'un $ :

 
Sélectionnez
%   position$   drapeaux   largeur   précision   caractère_de_formatage

Un avantage des paramètres de positions est d'utiliser le même paramètre à plusieurs endroits dans la même chaîne de formatage :

 
Sélectionnez
writefln("%1$d %1$x %1$o %1$b", 42);

La chaîne de formatage utilise le paramètre numéro 1 dans 4 indicateurs différents pour l'afficher en formats décimal, hexadécimal, octal et binaire :

 
Sélectionnez
42 2a 52 101010

Une autre application de ces paramètres de position est de prendre en charge plusieurs langues. Lorsqu'ils sont référés par des numéros de position, les paramètres peuvent être déplacés n'importe où avec une chaînes de formatage spécifique à une langue. Par exemple, le nombre d'étudiants d'une classe peut être affichée de cette manière :

 
Sélectionnez
writefln("Il y a %s étudiants dans la classe %s.", nombre, classe);

Sortie :

 
Sélectionnez
Il y a 20 étudiants dans la classe 1A.

Supposons que le programme doive aussi prendre en charge le turc. La chaîne de formatage doit alors être choisie en fonction de la langue choisie. La méthode suivante utilise l'opérateur ternaire :

 
Sélectionnez
auto format = (langue == "fr"
               ? "Il y a %s étudiants dans la classe %s."
               : "%s sınıfında %s öğrenci var.");
 
writefln(format, nombre, classe);

Malheureusement, quand les paramètres sont associés un à un, la classe et le nombre d'étudiants sont inversés dans le message turc ; la classe s'affiche là où le nombre devrait être et inversement :

 
Sélectionnez
20 sınıfında 1A öğrenci var.   ← Faux : signifie "classe 20", et "1A étudiants" !

Pour éviter cela, les paramètres peuvent être indiqués par leur position pour associer chaque indicateur avec le bon paramètre :

 
Sélectionnez
auto format = (langue == "fr"
               ? "Il y a %s étudiants dans la classe %s."
               : "%2$s sınıfında %1$s öğrenci var.");
 
writefln(format, nombre, classe);

Maintenant, les paramètres apparaissent dans le bon ordre, indépendamment de la langue choisie :

 
Sélectionnez
Il y a 20 étudiants dans la pièce 1A.

1A sınıfında 20 öğrenci var.

29-6. format

La sortie formatée est également utilisable avec la fonction format() du module std.string. format() fonctionne de la même manière que writef() mais retourne le résultat dans une chaîne au lieu de l'afficher :

 
Sélectionnez
import std.stdio;
import std.string;
 
void main()
{
   write("Quel est ton nom ? ");
   auto nom = chomp(readln());
 
   auto resultat = format("Bonjour %s!", nom);
}

Le programme pourra utiliser ce résultat plus tard.

29-7. Exercices

  1. Écrire un programme qui lit une valeur et qui l'affiche dans le système hexadécimal.
  2. Écrire un programme qui lit une valeur flottante et qui l'affiche comme un pourcentage avec deux chiffres après le point. Par exemple, si la valeur est 1.2345, il devrait afficher 1.23%.

Les solutionsSortie formatée - Correction.

30. Entrée formatée

Il est possible d'indiquer le format des données attendues à l'entrée. Le format indique aussi bien les données à lire que les caractères qui doivent être ignorés.

Les indicateurs de formatage en entrée du D sont similaires à ceux du langage C.

L'indicateur de formatage " %s", que nous avons déjà utilisé dans les chapitres précédents, lit une donnée selon le type de la variable. Par exemple, comme le type de la variable suivante est double, les caractères à l'entrée seront lus comme une valeur flottante :

 
Sélectionnez
double nombre;

readf(" %s", &nombre);

La chaîne de formatage peut contenir trois types d'information :

  • le caractère espace : indique zéro, un ou plusieurs caractères blancs dans l'entrée et indique que tous ces caractères devraient être lus et ignorés ;
  • indicateur de format : comme pour les indicateurs de format de sortie, les indicateurs de format d'entrée commencent par le caractère % et déterminent le format de la donnée à lire ;
  • n'importe quel autre caractère : indique que ces caractères sont attendus dans l'entrée tels quels, et qu'ils doivent être lus et ignorés.

La chaîne de formatage permet de sélectionner des informations spécifiques dans l'entrée et d'ignorer les autres.

Penchons-nous sur un exemple qui utilise les trois types d'information dans la chaîne de formatage. Supposons que l'on s'attende à voir apparaître le numéro d'étudiant et la classe dans l'entrée dans le format suivant :

 
Sélectionnez
numero:123 classe:90

Supposons également que les étiquettes numero: et classe: doivent être ignorées. La chaîne de formatage suivante sélectionnera les valeurs du numéro et de la classe et ignorera les autres caractères :

 
Sélectionnez
int numero;
int classe;
readf("numero:%s classe:%s", &numero, &classe);

Les caractères surlignés dans "numero:%s classe:%s" doivent apparaître tels quels dans l'entrée ; readf() les lit et les ignore.

Le caractère espace qui apparaît dans la chaîne de formatage fera que tous les caractères blancs qui apparaîtront exactement à cet endroit seront lus et ignorés.

Comme le caractère % a un sens spécial dans les chaînes de formatage, quand ce caractère lui-même doit être lu et ignoré, il doit être écrit deux fois : %%.

Dans le chapitre sur les chainesChaînes de caractères pour lire une ligne de donnée depuis l'entrée, nous avons utilisé chomp(readln()). On peut également faire ceci en plaçant un caractère \n à la fin de la chaîne de formatage :

 
Sélectionnez
import std.stdio;
 
void main()
{
    write("Prénom : ");
    string prenom;
    readf(" %s\n", &prenom);  //  \n à la fin
 
   write("Nom : ");
   string nom;
   readf(" %s\n", &nom);      //  \n à la fin
 
   write("Âge : ");
   int age;
   readf(" %s", &age);
 
   writefln("%s %s (%s)", prenom, nom, age);
}

Les caractères \n à la fin des chaînes de formatage quand on lit le nom et le prénom font que les caractères de nouvelle ligne sont lus depuis l'entrée et ignorés. Cependant, il peut toujours être nécessaire de supprimer les caractères blancs se trouvant éventuellement en fin de ligne avec chomp().

30-1. Caractères d'indicateur de format

  • : lit un entier dans le système décimal ;
  • : lit un entier dans le système octal ;
  • : lit un entier dans le système hexadécimal ;
  • : lit un nombre en virgule flottante ;
  • : lit selon le type de la variable ;
  • : lit un caractère. Cet indicateur permet aussi de lire des caractères blancs (ceux-ci ne sont pas ignorés).

Par exemple, si l'entrée contient « 23 23 23 », les valeurs seront lues différemment selon l'indicateur de format utilisé :

 
Sélectionnez
int nombre_d;
int nombre_o;
int nombre_x;
 
readf(" %d %o %x", &nombre_d, &nombre_o, &nombre_x);
 
writeln("Lu avec %d : ", nombre_d);
writeln("Lu avec %o : ", nombre_o);
writeln("Lu avec %x : ", nombre_x);

Même si l'entrée contient trois ensembles identiques de caractères « 23 », les valeurs des variables sont différentes :

 
Sélectionnez
Lu avec %d : 23
Lu avec %o : 19
Lu avec %x : 35

très brièvement, 23 vaut 2×8+3=19 dans le système octal et 2×16+3=35 dans le système hexadécimal.

30-2. Exercice

Supposons que l'entrée contient la date dans le format année.mois.jour. Écrivez un programme qui affiche le numéro du mois. Par exemple, si l'entrée est 2009.09.30, la sortie doit être 9.

La solutionEntrée formatée - Correction.

31. Boucle do-while

Dans le chapitre sur la boucle forBoucles for, nous avons vu les étapes d'exécution d'une boucle while :

 
Sélectionnez
préparation

vérification de la condition
ce qu'il y a à faire
itération

vérification de la condition
ce qu'il y a à faire
itération

...

La boucle do-while est très similaire à la boucle while. La différence est que la vérification de la condition est vérifiée à la fin de chaque itération de la boucle do-while, et donc que le corps de la boucle est exécuté au moins une fois :

 
Sélectionnez
préparation

ce qu'il y a à faire
itération
vérification de la condition    ← à la fin de l'itération

ce qu'il y a à faire
itération
vérification de la condition    ← à la fin de l'itération

...

Par exemple, do-while peut-être plus naturel dans le programme suivant où l'utilisateur devine un nombre, puisque l'utilisateur doit faire au minimum une tentative pour deviner le nombre :

 
Sélectionnez
import std.stdio;
import std.random;
 
void main()
{
   int nombre = uniform(1, 101);
 
   writeln("Je pense à un nombre entre 1 et 100.");
 
   int tentative;
 
   do {
      write("Essayez de deviner ce nombre. ");
 
      readf(" %s", &tentative);
 
      if (nombre < tentative) {
            write("Mon nombre est plus petit que cela. ");
 
      } else if (nombre > tentative) {
            write("Mon nombre est plus grand que cela. ");
      }
 
   } while (tentative != nombre);
 
   writeln("Correct !");
}

La fonction uniform() qui est utilisée dans le programme fait partie du module std.random. Elle retourne un nombre dans l'intervalle spécifié. Le deuxième nombre donné en argument est hors de cet intervalle. Cela veut dire que dans ce programme, uniform() ne retournera jamais 101.

31-1. Exercice

Écrivez un programme qui joue au même jeu mais qui devine le nombre que l'utilisateur donne. Si le programme est bien écrit, il devrait deviner le nombre de l'utilisateur en 7 essais au plus.

La solutionLa boucle do-while - Correction.

32. Tableaux associatifs

Les tableaux associatifs sont une fonctionnalité que l'on trouve dans la plupart des langages modernes de haut niveau. Ils constituent une structure de données très rapide qui fonctionne comme une mini base de données et sont couramment utilisés dans beaucoup de programmes.

Nous avons vu les tableaux dans le chapitre sur les tableauxTableaux comme des conteneurs qui stockent leurs éléments côte à côte, éléments auxquels ont peut accéder par des indices. Un tableau qui stocke les noms des jours de la semaine peut être définit comme suit :

 
Sélectionnez
string[] nomsDesJours =
      [ "lundi", "mardi", "mercredi", "jeudi",
         "vendredi", "samedi", "dimanche" ];

On peut accéder au nom d'un jour donné par son indice dans ce tableau :

 
Sélectionnez
writeln(nomsDesJours[1]);   // affiche "mardi"

Le fait que l'on peut accéder aux éléments par les valeurs d'indices peut être décrit comme une association d'indices avec les éléments. En d'autres termes, les tableaux associent les indices aux éléments. Les tableaux ne peuvent utiliser que des entiers comme indices.

Les tableaux associatifs permettent d'indexer les éléments par des entiers mais également par n'importe quel autre type. Ils associent les valeurs d'un type à des valeurs d'un autre type. Les valeurs qui servent d'indices dans un tableau associatif sont appelés clés (keys), plutôt qu'indices. Les tableaux associatifs donnent accès à chacun de leurs éléments par leurs clés.

32-1. Très rapide mais désordonné

Les tableaux associatifs sont une implémentation de table de hashage. Les tables de hashage font partie des collections les plus rapides pour stocker et accéder à des éléments. Sauf cas pathologique, le temps de stockage d'accès à un élément est indépendant du nombre d'éléments qu'il y a dans le tableau associatif.

La contrepartie de la grande efficacité des tableaux associatifs est leur stockage désordonné. À la différence des tableaux, les éléments des tables de hashage ne sont pas côte à côte. Ils ne sont triés d'aucune manière particulière.

32-2. Définition

La syntaxe des tableaux associatifs est similaire à la syntaxe des tableaux. La différence est que c'est le type des clés qui est spécifié entre les crochets, et non la taille du tableau :

 
Sélectionnez
type_des_éléments[type_des_clés] nom_du_tableau_associatif;

Par exemple, un tableau associatif qui associe des noms de jours de type string à des numéros de type int peut être défini de cette manière :

 
Sélectionnez
int[string] numerosDesJours;

La variable numerosDesJours est un tableau associatif qui peut être utilisé comme un tableau qui fournit une association des noms de jours à leurs numéros. En d'autres termes, il peut être utilisé comme l'inverse du tableau nomsDesJours du début de ce chapitre. On utilisera le tableau associatif numerosDesJours dans les exemples qui suivent.

Les clés des tableaux associatifs peuvent être de n'importe quel type, structures et classes définies par l'utilisateur comprises. Nous verrons les types définis par l'utilisateur dans des chapitres ultérieurs.

La taille des tableaux associatifs ne peut pas être spécifiée lors de la définition. Ceux-ci grandissent automatiquement quand des éléments sont ajoutés.

32-3. Ajouter des éléments

L'opérateur d'affectation est suffisant pour construire l'association entre une clé et une valeur :

 
Sélectionnez
// associe l'élément 0 avec la clé "lundi"
numerosDesJours["lundi"] = 0;
 
// associe l'élément 1 avec la clé "mardi"
numerosDesJours["mardi"] = 1;

La table grandit automatiquement avec chaque association. Par exemple, numerosDesJours contient deux éléments après les opérations précédentes. On peut le montrer en affichant le tableau entier :

 
Sélectionnez
writeln(numerosDesJours);

La sortie indique que les éléments de valeur 0 et 1 correspondent respectivement aux clés de valeur "lundi" et "mardi" :

 
Sélectionnez
["lundi":0, "mardi":1]

Il ne peut y avoir qu'un élément par clé. Pour cette raison, quand on affecte une valeur à une clé déjà existante, le tableau ne grandit pas. L'élément associé à la clé est simplement remplacé :

 
Sélectionnez
numerosDesJours["mardi"] = 222;   // modification d'un élément existant
writeln(numerosDesJours);

La sortie :

 
Sélectionnez
["lundi":0, "mardi":222]

32-4. Initialisation

Parfois, certaines associations entre les clés et les valeurs sont déjà connues au moment de la définition du tableau associatif. Les tableaux associatifs sont initialisés de façon similaire aux tableaux, avec un deux-point séparant chaque clé de son élément :

 
Sélectionnez
// clé : élément
int[string] numerosDesJours =
   [ "lundi"    : 0, "mardi"    : 1, "mercredi" : 2,
     "jeudi"    : 3, "vendredi" : 4, "samedi"   : 5,
     "dimanche" : 6 ];
 
writeln(numerosDesJours["mardi"]);    // affiche 1

32-5. Supprimer des éléments

L'élément qui correspond à une clé est supprimé par .remove() :

 
Sélectionnez
numerosDesJours.remove("mardi");
writeln(numerosDesJours["mardi"]);    //  ERREUR lors de l'exécution

La première ligne supprime l'élément de la clé "mardi". Comme cet élément n'est plus dans le conteneur, la seconde ligne entraîne une exception et l'arrêt du programme si l'exception n'est pas attrapée. Nous verrons les exceptions dans un chapitre ultérieur.

Il est possible de supprimer tous les éléments en une fois, comme nous le verrons dans le premier exercice de fin de chapitre.

32-6. Déterminer la présence d'un élément

L'opérateur in détermine si l'élément pour une clé donnée existe dans le tableau associatif :

 
Sélectionnez
int[string] codesDeCouleurs = [ /* ... */ ];
 
if ("violet" in codesDeCouleurs) {
   // il y a un élément pour la clé "violet"
 
} else {
   // aucun élément pour la clé "violet"
}

Parfois, utiliser une valeur par défaut pour les clés qui n'existent pas dans le tableau associatif a un sens. Par exemple, la valeur spéciale -1 peut être utilisée comme le code des couleurs qui ne sont pas dans codesDeCouleurs. .get() est utile dans de telles situations : elle retourne la valeur de l'élément si l'élément pour la clé donnée existe, la valeur par défaut sinon. La valeur par défaut est indiquée en second paramètre de .get() :

 
Sélectionnez
int[string] codesDeCouleurs = [ "bleu" : 10, "vert" : 20 ];
writeln(codesDeCouleurs.get("violet", -1));

Comme le tableau ne contient pas d'élément pour la clé "violet", .get() returne -1 :

 
Sélectionnez
-1

32-7. Propriétés

  • .length retourne le nombre d'éléments dans le tableau.
  • .keys retourne les copies des clés du tableau associatif dans un tableau dynamique.
  • .byKey donne un accès aux clés du tableau associatif sans les copier ; nous verrons comment .byKey est utilisé dans les boucles foreach dans le chapitre suivant.
  • .values retourne les copies de toutes les valeurs du tableau associatif dans un tableau dynamique.
  • .byValue donne un accès aux éléments du tableau associatif sans les copier.
  • .rehash peut rendre le tableau plus efficace dans certains cas comme après avoir inséré un grand nombre d'éléments et avant d'effectivement utiliser le tableau associatif.
  • .sizeof est la taille de la référence du tableau (cela n'a rien n'a voir avec le nombre d'éléments dans le tableau et a la même valeur pour tous les tableaux associatifs).
  • .get retourne l'élément s'il existe, la valeur par défaut sinon.
  • .remove supprime l'élément indiqué du tableau.

32-8. Exemple

Voici un programme qui affiche les noms turcs des couleurs qui sont indiqués en français :

 
Sélectionnez
import std.stdio;
import std.string;
 
void main()
{
   string[string] couleurs = [ "noir" : "siyah",
                              "blanc" : "beyaz",
                              "rouge" : "kırmızı",
                              "vert"  : "yeşil",
                              "bleu"  : "mavi"
                             ];
 
   writefln("Je connais les noms turcs de ces %s couleurs : %s",
            couleurs.length, couleurs.keys);
 
   write("Demandez-moi en une : ");
   string enFrancais = chomp(readln());
 
   if (enFrancais in couleurs) {
      writefln("\"%s\" est \"%s\" en turc.",
               enFrancais, couleurs[enFrancais]);
 
   } else {
      writeln("Je ne connais pas cette couleur.");
   }
}

32-9. Exercices

  1. Comment tous les éléments d'un tableau associatif peuvent-ils être supprimés ? Il y a au moins trois méthodes :

    1. Supprimer les éléments un par un.
    2. Affecter un tableau associatif vide.
    3. De façon similaire à la méthode précédente, affecter la propriété .init du tableau.

       
      Sélectionnez
      nombre = int.init;    // 0 pour int


    Note : la propriété .init de n'importe quelle variable est la valeur initiale de ce type :

  2. Comme pour les tableaux, il ne peut y avoir qu'une valeur par clé. Ceci peut être vu comme une limitation pour certaines applications.
    Supposons qu'un tableau associatif soit utilisé pour stocker des notes d'étudiants. Par exemple, supposons que les notes 18, 17, 19, etc. soient à stocker pour l'étudiant « pierre ».
    Les tableaux associatifs permettent facilement d'accéder aux notes par le nom de l'étudiant comme dans notes["pierre"]. Cependant, les notes ne peuvent pas être ajoutées de la manière suivante parce que chaque note remplacerait la précédente :

     
    Sélectionnez
    int[string] notes;
    notes["pierre"] = 18;
    notes["pierre"] = 17;   //  Remplace la note précédente !
  3. Comment pouvez-vous résoudre ce problème ? Définir un tableau associatif qui peut stocker plusieurs notes par étudiant.

Les solutionsTableaux associatifs - Correction.

33. Boucle foreach

Une des structures de contrôle les plus communes du D est la boucle foreach. Elle est utilisée pour appliquer la même opération à tous les éléments d'un conteneur (ou d'un intervalle).

Les opérations qui sont appliquées aux éléments d'un conteneur sont très répandues en programmation. Nous avons vu dans le chapitre sur la boucle forBoucles for que l'on peut accéder aux éléments d'un tableau dans une boucle for par une valeur d'indice qui est incrémentée à chaque itération :

 
Sélectionnez
for (int i = 0; i != tableau.length; ++i) {
   writeln(tableau[i]);
}

Voici les étapes d'une itération sur tous les éléments :

  • définir une variable compteur, souvent nommée i ;
  • itérer la boucle jusqu'à la valeur de la propriété .length du tableau ;
  • incrémenter i ;
  • accéder à l'élément.

foreach a essentiellement le même comportement mais simplifie le code en gérant ces étapes automatiquement :

 
Sélectionnez
foreach (element; tableau) {
   writeln(element);
}

Une partie du pouvoir de foreach vient du fait qu'elle peut être utilisée de la même manière indépendamment du type du conteneur. Comme nous l'avons vu dans le chapitre précédent, une manière d'itérer sur les valeurs d'un tableau associatif dans une boucle for est d'utiliser la propriété .values du tableau :

 
Sélectionnez
auto valeurs = aa.values;
for (int i = 0; i != valeurs.length; ++i) {
   writeln(valeurs[i]);
}

foreach ne nécessite rien de particulier pour les tableaux associatifs ; elle est utilisée de la même façon qu'avec les tableaux :

 
Sélectionnez
foreach (valeur; aa) {
   writeln(valeur);
}

33-1. La syntaxe de foreach

foreach consiste en trois sections :

 
Sélectionnez
foreach (noms ; conteneur_ou_intervalle) {
   // opérations
}
  • conteneur_ou_intervalle indique où sont les éléments ;
  • // opérations indique les opérations à appliquer à chaque élément ;
  • noms indique le nom de l'élément et potentiellement d'autres variables dépendant du type du conteneur ou de l'intervalle. Même si le choix de noms appartient au programmeur, le nombre et les types de ces noms dépend du type du conteneur.

33-2. continue et break

Ces mots-clés ont le même sens que celui qu'ils ont avec la boucle for : continue mène à l'itération suivante au lieu de finir celle qui est en cours et break sort de la boucle.

33-3. foreach avec les tableaux

Quand il y a un seul nom dans la section noms, c'est la valeur de l'élément à chaque itération :

 
Sélectionnez
foreach (element; tableau) {
   writeln(element);
}

Quand deux noms sont indiqués dans la section noms, il y a respectivement un compteur automatique et la valeur de l'élément :

 
Sélectionnez
foreach (i, element; tableau) {
   writeln(i, ": ", element);
}

Le compteur est incrémenté automatiquement par foreach. Son nom est choisi par le programmeur.

33-4. foreach avec les chaînes et std.range.stride

Comme les chaînes sont des tableaux de caractères, foreach fonctionne de la même manière qu'avec les tableaux : un nom unique est le caractère, deux noms sont le compteur et le caractère :

 
Sélectionnez
foreach (c; "salut") {
   writeln(c);
}
 
foreach (i, c; "salut") {
   writeln(i, ": ", c);
}

Cependant, étant des unités de stockage UTF, char et wchar itèrent sur les unités de stockage, par sur les points de code Unicode :

 
Sélectionnez
foreach (i, code; "abcçd") {
   writeln(i, ": ", code);
}

On accède aux deux unités de stockage UTF-8 qui forment ç par des éléments séparés :

 
Sélectionnez
0: a
1: b
2: c
3:
4: �
5: d

Une manière d'itérer sur les caractères Unicode des chaînes dans une boucle foreach est la fonction stride du module std.range. stride présente la chaîne comme un conteneur constitué de caractères Unicode. Il prend la taille de son pas en second paramètre :

 
Sélectionnez
import std.range;
 
// ...
 
   foreach (c; stride("abcçd", 1)) {
      writeln(c);
   }

Quel que soit le type de caractère de la chaîne, stride présente toujours ses éléments comme des caractères Unicode :

 
Sélectionnez
a
b
c
ç
d

J'expliquerai plus loin pourquoi cette boucle ne peut pas inclure de compteur automatique.

33-5. foreach avec les tableaux associatifs

Un seul nom indique la valeur, deux noms indiquent la clé et la valeur :

 
Sélectionnez
foreach (valeur; ta) {
   writeln(valeur);
}
 
Sélectionnez
foreach (clé, valeur; ta) {
   writeln(clé, ": ", valeur);
}

Les tableaux associatifs peuvent également donner leurs clés et leurs valeurs comme des intervalles. Nous verrons les intervalles dans un chapitre ultérieur. .byKey() et .byValue() retournent des objets intervalle efficaces qui sont aussi utiles dans d'autres contextes. .byValue() n'a pas grand intérêt dans les boucles foreach par rapport aux itérations classiques que l'on a déjà décrites. En revanche, .byKey() est la seule manière efficace d'itérer seulement sur les clés d'un tableau associatif :

 
Sélectionnez
foreach (clé; ta.byKey()) {
   writeln(clé);
}

33-6. foreach avec les intervalles de nombres

Nous avons vu les intervalles de nombres dans le chapitre « Tranches (slices) et autres fonctionnalités des tableaux ». Il est possible d'indiquer un intervalle de nombre dans la section conteneur_ou_intervalle :

 
Sélectionnez
foreach (nombre; 10..15) {
   writeln(nombre);
}

Rappel : 10 est inclus dans l'intervalle mais pas 15.

33-7. foreach avec les structures, les classes et les intervalles

foreach peut également être utilisé avec des objets d'un type de l'utilisateur qui définit sa propre itération dans les boucles foreach. Comme le type lui-même définit sa propre façon d'itérer, il n'est pas possible de dire grand chose ici. Les programmeurs doivent se référer à la documentation de ce type particulier.

Les structures et les classes apportent une prise en charge de l'itération foreach soit avec leur méthode opApply(), soit par un ensemble de méthodes d'intervalle. Nous verrons ces fonctionnalités dans des chapitres ultérieurs.

33-8. Le compteur n'est automatique que pour les tableaux

Le compteur automatique est fourni seulement quand on itère sur les tableaux. Quand un compteur est nécessaire lors d'une itération sur d'autres types de conteneurs, le compteur peut être défini et incrémenté de façon explicite :

 
Sélectionnez
int i;
foreach (element; conteneur) {
   // ...
   ++i;
}

Une telle variable est aussi nécessaire quand on compte une condition spécifique. Par exemple, le code suivant compte seulement les valeurs qui sont divisibles par 10 :

 
Sélectionnez
import std.stdio;
 
void main()
{
   auto nombres = [ 1, 0, 15, 10, 3, 5, 20, 30 ];
 
   int compteur;
   foreach (nombre; nombres) {
      if ((nombre % 10) == 0) {
            ++compteur;
            write(compteur);
 
      } else {
            write(' ');
      }
 
      writeln(" : ", nombre);
   }
}

La sortie :

 
Sélectionnez
  : 1
1 : 0
  : 15
2 : 10
  : 3
  : 5
3 : 20
4 : 30

33-9. La copie de l'élément, pas l'élément lui-même

La boucle foreach fournit normalement une copie de l'élément, pas l'élément qui est stocké dans le conteneur. Ceci peut être la cause de bogues.

Pour voir un exemple de ceci, jetons un œil sur le programme suivant qui essaie de doubler les valeurs des élément d'un tableau :

 
Sélectionnez
import std.stdio;
 
void main()
{
   double[] nombres = [ 1.2, 3.4, 5.6 ];
 
   writefln("Avant : %s", nombres);
 
   foreach (nombre; nombres) {
      nombre *= 2;
   }
 
   writefln("Après : %s", nombres);
}

La sortie du programme montre que l'affectation faite à chaque élément à l'intérieur du corps de foreach n'a aucun effet sur les éléments du conteneur :

 
Sélectionnez
Avant : [1.2, 3.4, 5.6]
Après : [1.2, 3.4, 5.6]

Ceci s'explique par le fait que nombre n'est pas un élément du tableau, mais une copie d'élément. Quand on a besoin de modifier les éléments eux-même, le nom doit être défini comme une référence à l'élément par le mot-clé ref :

 
Sélectionnez
foreach (ref nombre; nombres) {
   nombre *= 2;
}

La nouvelle sortie montre que maintenant, les affectations modifient les éléments du tableau :

 
Sélectionnez
Avant : [1.2, 3.4, 5.6]
Après : [2.4, 6.8, 11.2]

Le mot-clé ref fait de nombre un alias de l'élément à chaque itération. De ce fait, les modifications apportées à nombre sont apportées à cet élément du conteneur.

33-10. L'intégrité du conteneur doit être préservée

Même s'il est correct de modifier les éléments du conteneur à travers des variables ref, la structure du conteneur ne doit pas changer. Par exemple, les éléments ne doivent pas être supprimés ou ajoutés au conteneur pendant une boucle foreach.

De telles modifications peuvent perturber le fonctionnement interne de l'itération de la boucle et mettre le programme dans un état incohérent.

33-11. foreach_reverse pour itérer dans la direction inverse

foreach_reverse fonctionne de la même manière que foreach mais itère dans la direction inverse :

 
Sélectionnez
auto conteneur = [ 1, 2, 3 ];
 
foreach_reverse (element; conteneur) {
    writefln("%s ", element);
}

La sortie :

 
Sélectionnez
3
2
1

L'utilisation de foreach_reverse n'est pas répandue parce que la fonction d'intervalle retro() fait la même chose. Nous verrons cette fonction dans un chapitre suivant.

33-12. Exercice

Nous savons que les tableaux associatifs proposent une relation de type clés-valeurs. Cette relation est unidirectionnelle : on accède aux valeurs par les clés mais ce n'est pas vrai dans l'autre sens.

Supposons que l'on ait ce tableau associatif :

 
Sélectionnez
string[int] noms = [ 1:"un", 7:"sept", 20:"vingt" ];

Utilisez ce tableau associatif et une boucle foreach pour remplir un tableau associatif nommé valeurs. Ce nouveau tableau associatif doit donner les valeurs qui correspondent aux noms. Par exemple, la ligne suivante devrait afficher 20 :

 
Sélectionnez
writeln(valeurs["vingt"]);

La solution.La boucle foreach - Correction.

34. switch et case

switch est une structure qui permet de comparer la valeur d'une expression avec de multiples valeurs spéciales. Elle est similaire à une chaîne « if, else if, else » mais fonctionne un peu différemment. case est utilisé pour spécifier les valeurs à comparer avec l'expression du switch. case fait partie de la structure switch et n'est pas une structure en lui-même.

switch prend une expression entre parenthèses, compare la valeur de cette expression aux valeurs des case et exécute les opérations du case qui est égal à la valeur de l'expression. Sa syntaxe consiste en un bloc switch qui contient une ou plusieurs sections case et une section default :

 
Sélectionnez
switch (expression) {
 
case valeur_1:
   // opérations à exécuter si l'expression est égale à valeur_1
   // ...
   break;
 
case valeur_2:
   // opérations à exécuter si l'expression est égale à valeur_2
   // ...
   break;
 
// ... autres cases ...
 
default:
   // opérations à exécuter si l'expression ne correspond à aucun case
   // ...
   break;
}

Même si elle est utilisée dans des vérifications conditionnelles, l'expression que le switch prend n'est pas utilisée comme une expression logique. Elle n'est pas évaluée comme « si cette condition est vraie » comme dans la structure if. La valeur de l'expression du switch est utilisée dans des comparaisons d'égalité avec les valeurs des case. C'est similaire à une chaîne « if, else if, else » qui n'a que des opérations d'égalité :

 
Sélectionnez
auto valeur = expression;
 
if (valeur == valeur_1) {
   // opérations pour valeur_1
   // ...
 
} else if (valeur == valeur_2) {
   // opérations pour valeur_2
   // ...
}
 
// ... autres 'else if' ...
 
} else {
   // opérations pour les autres valeurs
   // ...
}

Cependant, la chaîne « if, else if, else » ci-dessus n'est pas l'équivalent de la structure switch. Les raisons à cela seront expliquées dans les sections suivantes.

Si une valeur case correspond à la valeur de l'expression du switch, alors les opérations qui sont sous ce case sont exécutées. Si aucune valeur ne correspond, alors les opérations qui sont en dessous de default sont exécutées. Les opérations sont exécutées jusqu'à ce qu'une instruction break ou goto est rencontrée.

34-1. L'instruction goto

Certains usages de goto sont en général déconseillés dans la plupart des langages de programmation. Cependant, goto est utile dans les structures switch dans quelques rares cas. L'instruction goto sera vue plus en détail dans un chapitre ultérieur.

case n'introduit pas un nouveau bloc comme le fait la structure if. Une fois que les opérations dans un bloc if ou else sont finies, l'évaluation totale de la structure if est également terminée. Ce n'est pas le cas avec les sections case ; une fois qu'un case correspondant est trouvé, l'exécution du programme saute vers ce case et exécute les opérations sous ce case. Quand ceci est nécessaire dans certains cas rares, goto fait sauter l'exécution du programme vers le case suivant :

 
Sélectionnez
switch (valeur) {
case 5:
   writeln("cinq");
   goto case;   // continue sur le prochain case
 
case 4:
   writeln("quatre");