Langage D de A à Z
Bonjour cher lecteur,
Dans cet article, nous allons découvrir ou re-découvrir le langage D dlang. L'article n'est pas finis et il est en constante amélioration. En effet face à la demande croissante d'information sur le langage D dans la langue de molière, il est intéressant de pouvoir consulter les explications déjà rédigé bien qu'imparfaites.
Bonne lecture
- Pourquoi le D
- Environnement du langage
D
- Premier programme
- Compiler le
programme
- Les types et les
variables
- Les opérateurs
arithmétiques
- Les opérateurs de
comparaison
- Structure de
contrôles
- Les tableaux
- Les boucles
- Les fonctions
- Les bases
- Les fonctions
avec plusieurs arguments
- Les fonctions sans
arguments
- Les fonctions
avec des valeurs par défauts
- Les
fonctions avec un nombre variable d'argument
- Les fonctions modèles
(template)
- Les fonctions macros
(template)
- Les delegates ( closures
)
- La syntaxe d'appelle uniforme de fonction (
UFCS )
- Les bases
- Les entrées / Sorties
- Les exceptions
- Le transtypage (cast)
- Les structures
de données et les classes
- Range
- Interfrace graphique en
GTK avec GTKD
Pourquoi le D ?
Avant de commencer je me dois de vous expliquer pourquoi choisir ce langage. Le language D à vu le jour pour répondre au besoin du développement d'une application moderne. En effet les langages actuellement répandue ne sont pas tout jeunes:
- C++ 1983
- Perl 1987
- Python 1990
- Java 1995
- Ruby 1995
Les langages de programmation sont axés sur 3 grands thèmes:
- Rapidité d'éxécution
- Productivité
- Portabilité
La portabilté est la plupart du temps gérer par la bibliothèque standard où part une machine virtuelle suivant les langages. La vrai difficulté est de concilier Rapidité d'éxécution avec Productivité. Jusqu'alors les langages de programmation apartenais à l'une ou l'autre des catégories. Le D réussi à allier parfaitement Rapidité d'éxécution avec Productivité.
En effet les applications écrite en D sont aussi rapides que les appllication en C++. Une syntaxe simple avec des principes retrouvé dans de nombreux autres langages permet une prise en main rapide. La gestion de la mémoire par un ramasse miette simplife le travaille du développeur. Au final le D laisse au développeur le soin de se concentrer sur ça tâche qui est de développer.
Environnement du langage D
Afin de pouvoir débuter avec ce langage vous devez installer le compilateur, la bibliotèque standard et le ramasse miette. Sur la distribution GNU Linux Fedora, tous ceci est fourni via le dépôt officiel:
# yum install ldc-phobos-devel ldc-druntime-devel
Si vous utiliser l'éditeur geany je vous invite à installer le paquet ldc-phobos-geany-tags afin d'avoir l'auto-complétion.
Si vous avez un autre système d'exploitation vérifier qu'il n'y a pas de paquet se référant au D autrement récupérer via le site officiel ces outils.
Configuration de geany
Premier programme
Écrire le code
Débutons par le traditionnelle Hello world (Bonjour le monde). A l'aide de votre éditeur favoris (geany pour moi) saisissez le code suivant:
import std.stdio; void main ( ){ writeln("Bonjour le monde" ); }
- Toutes les instructions se termine par un
;
. - Le programme débute toujours par la fonction principale se nommant main
- Une fonction se délimite par les accolades
{…}
- les paramètres envoyé à une fonction se font entre les parenthèses
(…)
de cette dernière.
Ici nous avons deux instructions:
import std.stdio
déclarant que nous allons utiliser des fonctionnalités définis dans le modulestdio
du packagestd
. ( la fonctionwriteln
provient de ce module )writeln("Bonjour le monde" )
indiquant que nous allons écrire sur la sortie standard (le terminal) la phrase "Bonjour le monde" suivis d'un retour à la ligne.
Compiler le programme
La seconde étape connsite de compiler ce bout de code c'est à dire de tranformer un code lisble pour l'Homme en un programme lisible pour la machine.
Pour ceux sous fedora ayant installer le compilateur ldc
vous
faites comme ceci:
$ ls *.d # vérifions que le fichier est bien dans le rpertoire courant hello.d $ ldc2 hello.d # compilation $ ls hello* # vérifions si le binaire a été compilé hello hello.d hello.o
Pour les autres remplacer ldc2
par le compilateur que vous
utilisez (gdc
, dmd
, …)
Éxécutons le programme
Tout simplement en faisant comme ceci:
$ ./hello
Bonjour le monde
Grand moment de joie, notre programme dit bonjour
Les types et les variables
Il est nécessaire de préciser en D quel type de donnée on manipule. Ainsi lorsque vous créer un variable on indique en même temps son type. La syntaxe générale est sous la forme:
<type> <nom variable>;
Les nombres
Il existe deux catégories de nombre. On distingue
- les chiffres entiers (sans virgule)
- les chiffres décimaux (également apellé flottant)
Parmis les chiffres entiers on différencie également:
- les entiers pouvant être uniquement positif ( ex:
uint
) - les entiers pouvant être positif ou négatif( ex:
int
)
Il faut savoir que la mémoire n'est pas infini est donc la représentation
d'un nombre est limité. Ainsi si un type autorise des valeurs négatives la
valeur maximum positive sera plus faible que son type équivalent uniquement
positif ( int
vs uint
).
Pour créer une valeur décimal on a recour au type float
ou
double
. Le type double
peut contenir plus
d'information donc sa valeur maximum est plus grande. Mais en contrepartie une
variable double
occupera plus de place et ceci quelque soit sa
valeur.
Il en est de même avec les types entiers ( long
et
ulong
peuvent contenir une valeur plus importante que
int
et uint
).
Note : On utilisera au temps que possible
l'alias size_t
en lieu et place de uint
et
ulong
. Car size_t
s'adapte selon si le programme est
en 64 ou 32 bits. En 664 bits ils corresponds au type uong et en 32 bits au
type uint.
Pour revenir sur le choix du type par exemple quand on veut stocker un âge dans une variable on va s'orienter vers un type entier positif. Personne n'a eu à soufflé un jour s'est -20 ans .
uint age = 2;
Création d'une variable age de type entier positif(uint) de valeur 2.
Autre exemple si on souhaite manipuler une forme simplifier du nombre
PI
on utilisera un type décimal.
float PI = 3.1416;
Les valeurs booléenne
Il est fréquent de vouloir stocker une valeur correspondant à
vrai
ou faux
.
- Est-ce que la valeur de
x
vaut 0 - Est-ce que
x
est plus grand quey
- …
On utilise dans ces là un type booléen pouvant stocker seulement 2 valeurs.
Soit vrai true
soit faux false
. Le type s'apelle
simplement bool
.
bool test = true;
Les caractères
Nous arrivons aux derniers type fréquement rencontré qui est le caractère.
Il permet de stocker une et seulement une lettre. Le type
correspondant s'apelle char
.
char l = 'L';
Attention : Le caractère est entre simple
guillemet '
. Les doubles guillemet "
sont
réservés pour les chaines de caractères que nous aborderons un peu plus
loin.
Tous les types primitifs
Type | Description | Min | Max |
---|---|---|---|
char | Un caractère imprimable encodé en UTF-8 sur 1 octet (8bits) | 0 | 255 |
wchar | Un caractère imprimable encodé en UTF-16 sur 2 octets (16bits) | 0 | 65535 |
dchar | Un caractère imprimable encodé en UTF-32 sur 4 octets (32bits) | 0 | 4294967293 |
byte | Valeur entière sur 1 octet (8 bits) | -128 | 127 |
ubyte | Valeur entière positive sur 1 octet (8 bits) | 0 | 255 |
short | Valeur entière sur 2 octets (16 bits) | -32768 | 32767 |
ushort | Valeur entière positive sur 2 octets (16 bits) | 0 | 65535 |
int | Valeur entière sur 4 octets (32 bits) | -2147483648 | 2147483647 |
uint | Valeur entière positive sur 4 octets (32 bits) | 0 | 4294967296 |
long | Valeur entière sur 8 octets (64 bits) | -9223372036854775808 | 9223372036854775807 |
ulong | Valeur entière positive sur 8 octets (64 bits) | 0 | 18446744073709551615 |
float | Valeur numérique sur 4 octets (32 bits) | 1.18×10-38 | 3.40×10+38 |
ifloat | Valeur numérique imaginaire pure sur 4 octets (32 bits) | 1.18×10-38i | 3.40×10+38i |
cfloat | Nombre complexe de 2 valeurs flottantes (float) | 1.18×10-38 +1.18e-38i | -nan |
double | Valeur numérique sur 8 octets (64 bits) | 2.23×10-308 | 1.80×10+308 |
idouble | Valeur numérique imaginaire pure sur 8 octets (64 bits) | 2.23×10-308i | 1.80×10+308i |
cdouble | Nombre complexe de 2 doubles | 2.23×10-308 +2.23e-308i | 1.19×10+4932 |
real | Le plus grand numérique supporté par le processeur soit 16 octets (128 bits) | 3.36×10-4932 | 1.19×10+4932 |
ireal | Le plus grand numérique supporté par le processeur soit 16 octets (128 bits) | 3.36×10-4932i | 1.19×10+4932 |
creal | Nombre complexe réel | 3.36×10-4932 +3.36e-4932i | 1.19×10+4932 |
Les pointeurs
Les pointeurs sont utilisés dans le seul cas d'interfacé du code avec une bibliothèque écrite en C . Donc la plupart d'entre vous n'en aura jamais besoin. Pour les curieux
int* p;
Ceci est un simple pointeur vers des données [1]. Il représente la même chose qu'en C.
Attention cependant, contrairement au C, le code suivant :
int* p1, p2;
Déclare deux pointeurs vers des entiers, alors qu'en C, il déclare un pointeur et un entier ! Pour cette raison, il est d'usage de toujours rattacher l'astérisque au type de données plutôt qu'au nom de la variable.
Les opérateurs arithmétiques
Opération | Addition | Soustraction | Multiplication | Division | Modulo |
---|---|---|---|---|---|
Symbole | + | - | * | / | % |
Note : Le modulo est le reste de la division euclidienne de deux variables de type numérique
import std.stdio; void main(){ size_t a = 2; size_t b = 2; size_t c = a + b; size_t d = a - b; size_t e = a * b; size_t f = a / b; size_t g = a % b; writeln("une addition: ", a, " + ", b, " = ", c); writeln("une soustraction: ", a, " - ", b, " = ", d); writeln("une multiplication: ", a, " * ", b, " = ", e); writeln("une division: ", a, " / ", b, " = ", f); writeln("le modulo: ", a, " % ", b, " = ", g); }
$ ldc2 operation.d $ ./operation une addition: 2 + 2 = 4 une soustraction: 2 - 2 = 0 une multiplication: 2 * 2 = 4 une division: 2 / 2 = 1 le modulo: 2 % 2 = 0
size_t a = 2; size_t b = 3; size_t c = a + b; // c vaut 5 auto d = a + b; /* d vaut 5 et le type de d est déterminer automatiquement soit size_t */
Une histoire d'incrémentation et décrémentation
Dans la même catégorie, il existe l'incrémentation et la décrémentation :
- l'incrémentation consiste à augmenter de 1 la valeur de la variable.
- la décrémentation consiste à diminuer de 1 la valeur de la variable.
Pour cela, il y a plusieurs façons de le faire. Prenons le cas de
l'incrémentation : int a = 2; a = a + 1; a += 1; a;
a
Vous remarquez que les 2 dernières formes sont plus rapides à écrire
(oui, le programmeur est un fainéant ).
Y a t-il une différence entre ++a
et a++
Oui, il y en a une et elle est subtile :
- a++ : utilise la valeur de a, puis l'incrémente ( post-incémentation )
- ++a: incrémente la valeur de a, puis l'utilise( pré-incémentation )
Par exemple :
import std.stdio; void main(){ size_t a = 2; // donne la valeur de a puis incrémente writeln("post-incrémentation de: ", a, " -> ", a++); a = 2; // incrémente puis donne la valeur de a writeln("pré-incrémentation de: ", a, " -> ", ++a); a = 2; // donne la valeur de a puis décrémente writeln("post-décrémentation de: ", a, " -> ", a--); a = 2; // décrémente puis donne la valeur de a writeln("pré-décrémentation de: ", a, " -> ", --a); }
post-incrémentation de: 3 -> 2 pré-incrémentation de: 3 -> 3 post-décrémentation de: 1 -> 2 pré-décrémentation de: 1 -> 1
Autres raccourcis
On peut également utiliser les raccourcis pour la multiplication, la division et le reste de la division euclidienne. un court exemple :
size_t a = 5; a *= 2; // 10 a /= 2; // 2.5 mais comme c'est un entier tronqué à 2 a %= 2; // 1
Attention : Je vous mets en garde sur le type
utilisé. Ce dernier peut avoir des conséquences graves ! En effet,
jusqu'ici on a utilisé le type size_t
et nos résultats étaient des
entiers. Mais ceci n'est pas systématiquement vrai. Par exemple, diviser 1 par
3 donne un résultat décimal ne pouvant pas correspondre à un entier positif
(size_t
). Par conséquent, vous devez utiliser un autre type plus
approprié, comme le type float
ou double
:
double a = 1; a /=3; // a vaut 0.33333333 int b = 1; int c = 3; double d = b / c; // d vaut 0.33333333
Les opérateurs de comparaison
Il est fréquent dans un programme que l'on ai de besoin de comparer. Pour cela, en D il est possible de tester différents cas :
- tester une égalité
3 == 3; // cette expression retournera vrai 3 == 4; // cette expression retournera faux
- tester une différence
int a = 4; 3 != a; // 3 est différent de 4 donc cette expression retournera vrai
- comparer a ou b
a || b // si l'une des expressions est vraie, l'expression globale sera vraie
- comparer a et b
a && b // si l'une des expressions est fausse, l'expression globale sera fausse a < b // savoir si a est strictement plus petit que b a <= b // savoir si a est plus petit ou égal à que b a > b // savoir si a est strictement plus grand que b a >= b // savoir si a est plus grand ou égal à b a is b // savoir si a est identique à b a !is b // savoir si a n'est pas identique à b
Test d'identité
Les comparateurs d'identités sont particuliers et méritent plus d'explications. Si l'on compare :
- des types entre eux (par exemple int)
int a = 2; int b = 2; int[1] c = [ 2 ]; // tableau de taille 1 int[1] d = a; // d est une référence vers tableau c bool r1= a is b; // r1 vaudra false ( faux ) bool r2= c is d; // r1 vaudra true ( vrai )
a
et b
sont deux variables distincte donc a n'est
pas b. En revanche d
est a
car il réfère sur le même
tableau 2
- des instances
Pour apréhder la notion d'instances vous deveriez lire le chapitre Les structures de données et les classes .
Personne jean = new Personne("jean"); Personne paul = new Personne("paul"); jean is paul; // retournera faux
Lorsque l'on compare des objets entre eux, ce comparateur va dire si oui ou non c'est le même objet qui est référencé. On reviendra plus tard sur les références, gardez les à l'esprit et n'hésitez pas à revenir dessus.
Le comparateur !is
est la négation du comparateur
is
donc
Personne jean = new Personne("jean"); Personne paul = new Personne("paul"); jean !is paul; // retournera vrai
Structure de contrôles
Nous avons vu les opérateurs de comparaison : maintenant, nous allons les utiliser avec les conditions. Rien de plus simple ! Commençons de suite.
Si, Sinon Si, Si ( if
, else if
, else
)
import std.stdio; … size_t a = 2; if ( a == 2 ){ // test une égalité avec l'opérateur == , si a vaut 2 writeln("la variable a vaut bien 2"); // écrit la phrase puis retour à la ligne } else if ( a == 3){ // sinon si la variable a vaut 3 writeln("la variable a vaut bien 3"); // écrit la phrase puis retour à la ligne } else{ // sinon (pour tous les autres cas) writeln("la variable a vaut ", a); // écrit la phrase puis retour à la ligne }
Si a
vaut 2 alors écrire sur la sortie standard la
variable a vaut bien 2@. Si la variable
a vaut pas 2 testons si
elle vaut 3. Si elle vaut 3 alors écrire sur la sortie standard
la
variable a vaut bien 3 . Si la valeur de
a ne correspond à
aucun de ces test alors écrire sur la sortie standard
la variable a vaut
%d %d est remplacé par la valeur de
a@@.
C'était un exemple pour vous dégourdir les méninges. Complexifions légèrement avec divers exemples d'utilisation :
import std.stdio; … size_t a = 2; size_t b = 2; if ( a == 2 && b == 3 ){ // si a vaut 2 et b vaut 3. Les 2 conditions doivent être vraies pour rentrer dans le bloc writeln("la variable a vaut ", a, " et la variable b vaut ", b); } if ( a == 2 || b ==3 ){ // si a vaut 2 ou bien si b vaut 3. Au moins une des 2 conditions doit être vraie pour rentrer dans le bloc writeln("la variable a vaut ", a, " et la variable b vaut ", b); } if ( a > 1 && b >= 3 ){ // si a est strictement plus grande que 1 et b plus grande ou égale à 3. Les 2 conditions doivent être vraies pour rentrer dans le bloc writeln("la variable a vaut ", a, " et la variable b vaut ", b); } if ( a <= 2 || b < 3){ // si a est plus petite ou égale à 2 ou bien si b est strictment plus petite que 3. Au moins une des 2 conditions doit être vraie pour rentrer dans le bloc writeln("la variable a vaut ", a, " et la variable b vaut ", b); }
Switch et case
Vous avez vu précédemment la possibilité d'imbriquer des si - sinon de la manière suivante :
int a = 4; if ( a == 0 ){ … } else if ( a == 1 ){ … } else if ( a == 2 ){ … } else if ( a == 3 ){ … } else if ( a == 4 ){ … } else { … }
Sachez qu'il existe une manière plus élégante d'écrire cela à l'aide des mots clés switch et case :
size_t a = 4; switch (a){ // on met la variable à tester case 1: // cas où la valeur est 1 … break; case 2: // cas où la valeur est 2 … break; case 3: // cas où la valeur est 3 … break; case 4: // cas où la valeur est 4 … break; default: // cas où la valeur est différente … }
Pourquoi retrouve-t-on break
à chaque fois Admettons, pour
imager, 2 choses :
- que la variable
a
vaut 3 - que le mot clé
break
soit absent
D'après l'exemple précédent, on entrerait dans le bloc à partir du cas 3 et
on effectuerait tous les cas suivants, c'est à dire ici le cas 4 et le cas par
défaut. Dans la majeure partie des cas, on n'a pas besoin d'exécuter 2 règles,
c'est pour cela que l'on met l'instruction break afin de quittr le
switch
après avoir effectué l'opération correspondant à ça valeur.
Un dernier exemple du même type pour la route :
int a = 1; switch (a) { // on met la variable à tester case 1: // cas où la valeur est 1 … case 2: // cas où la valeur est 2 … case 3: // cas où la valeur est 3 … break case 4: // cas où la valeur est 4 … break default: // cas où la valeur est différente … }
Ici, on va rentrer dans le cas 1, 2 et 3 puis sortir.
Ce comportement est similaire en C et en C++. Mais en D, on peut également utiliser des chaînes de caractères ( voir Les chaines de caractères ):
string prenom = "jonathan"; switch (prenom){ // on met la variable à tester case "jonathan": // cas où la valeur est "jonathan" … break case "jean": // cas où la valeur est "jean" … break case "paul": // cas où la valeur est "paul" … break case "remi": // cas où la valeur est "remi" … break default: // cas où la valeur est différente … }
Les tableaux
Il est fréquent de vouloir stocker une suite de nombre pour celà on utilise les tableaux qui pourront stocker une liste de valeur dans une variable.
Code | Description |
---|---|
int* p; | Pointeur vers les données |
int[3] s; | Tableau statique |
int[] a; | Tableau dynamique |
int[string] x; | Tableau associatif |
int[][] m; | Matrice |
Les tableaux dynamiques
int[] s;
Un tableau dynamique est un tableau dont la taille peut varier. Lors de sa déclaration, on ne spécifie pas sa taille. En fait, c'est le ramasse-miette (garbage collector) qui gère ce tableau au moyen d'un pointeur.
Exemple :
int[] a; a.length = 2; // on donne une taille de 2 a[0] = 5; a[1] = 8; a.length = a.length + 5 // ici 2+5 = 7 soit la nouvelle taille du tableau a[2] = 5; a[3] = 7; a[4] = 9; a[5] = 3 a[6] = 1;
Il est conseillé de faire le minimum de redimensionnements de tableau possibles afin de garder de bonnes performances. Ainsi, on évitera :
int[] a; for ( i = 0 , i < 50, i++) { a.length = a.length + 1; a[i] = 0; }
On utilisera de préférence :
int[] a; for ( i = 0 , i < 50, i++){ if (a.length == i) { a.length = a.length * 2; // si la taille du tableau est égale à l'indice i, on double sa taille; } a = 0; } a.length = i; // Afin d'économiser la mémoire, on ajuste exactement la taille du tableau au besoin soit la valeur de l'indice i
Voir le chapitre sur les boucles: La
boucle Pour ( for ). Ici au lieu d'augmenter à chaque tour de boucle de 1
la taille du tableau on double la cpacité de stockage du tableau si on atteint
la capacité maximum. Ceci évite de faire des coûteuses allocations mémoire pour
agrandir le tableau trop fréquement. Vous pouvez grandir de 10, 20, 50 … au
lieu de le doubler selon vos besoin. Nous verrons plus tard que la bibliothèque
standard phobos
fournit std.array.appender pour faciliter cette tâche.
Les tableaux statiques
int[3] s;
Un tableau statique est un tableau dont on spécifie la taille lors de sa déclaration, taille qui ne changera plus. La taille d'un tableau statique est fixée au moment de la compilation.
Par exemple :
int[3] s; // tableau statique dans lequel on peut stocker 3 entiers s[0] = 50; s[1] = 24; s[2] = 98 int [5] a = 3; // les 5 colonnes valent 3
Les chaines de caractères
Les langages de programmation doivent bien gérer les chaînes de caractères. Le C et le C++ ne sont pas très bons pour cela. La première difficulté est de gérer la mémoire, les traitements temporaires, de constamment scanner les chaînes de caractères pour rechercher la terminaison par "\\0" et de fixer la taille.
Les tableaux dynamiques en D suggèrent la solution la plus évidente. Une chaîne est simplement un tableau dynamique de caractères. Les chaînes littérales sont juste un moyen facile d'écrire des tableaux de caractères.
string str0 = "Le D c'est génial"; // déclaration et initialisation string str1 = str0.dup; // copie de la chaîne de caractères str0 ~= " et je l'ai adopté"; /* on ajoute à la suite de la chaîne "Le D c'est génial et je l'ai adopté" */ string str2 = str0 ~ str1; /* on concatène str0 et str1, grâce au ~ et on stocke le tableau résultant dans str2 */
Le type string est en faite un alias pour immutable(char)
. Soit un tableau de caractère dont on ne peut pas changer le
contenu. Les valeurs sont immuables voir plus loin (intérêt sécurité,
multi-threading).
Par défaut les chaînes de caractère sont encodées en utf8. Il existe également les types:
Si une chaîne est ambigüe et peut correspondre à plusieurs types, il faut préciser.
Si on veut de l'UTF-16, on peut faire un cast (voir « Le transtypage (cast) ») :
(wchar [])"J'aime le D";
Ou si on veut de l'UTF-32 :
(dchar[])"J'aime le D"; char c; wchar u; dchar a; c = 'b'; // le caractère b (UTF-8) est assigné à c u = 'b' // le caractère b (UTF-16) est assigné à u a = 'b' // le caractère b (UTF-32) est assigné à a u = '\\n'; // un retour à la ligne est assigné à u
Écrire une phrase literral, c'est à dire écrire tel qu'on le voit est
réalisé à l'aide des symbôles des backtick `
string phrase = `C'est une phrase sur plusieurs lignes avec des apostrophes double " `
Le slicing
Le slicing est une technique particulière qui permet de copier une série d'éléments d'un tableau dans un autre. Voici un exemple de cette technique :
int[10] a = [1,2,3,4,5,6,7,8,9,10]; // déclare un tableau de 10 entiers int[] b; // déclare un tableau dynamique int[] c; // déclare un tableau dynamique int[3] d; // déclare un tableau statique de 3 éléments b = a[1..3]; // le fameaux slicing, on copie ici les éléments des indices 1 à 3 dans le tableau b. Rapel: le 1er indice est le 0 a[0] c = a[4..$]; // ici on copie tous les éléments à partir de l'indice 4 jusqu'à la fin d[0..2] = 5; // ici on assigne la valeur 5 à tous les éléments du tableau. Cela équivaut à d[0] = 5 d[1] = 5 d[2] = 5
Concaténation de tableaux
int[10] a = [1,2,3,4,5,6,7,8,9,10]; // déclare un tableau de 10 entiers int[10] b = [11,12,13,14,15,16,17,18,19,20]; // déclare un tableau de 10 entiers int[] c; c = a ~ b; // soit c contient 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20
On peut combiner le slicing et la concaténation :
int[10] a = [1,2,3,4,5,6,7,8,9,10]; // déclare un tableau de 10 entiers int[10] b = [11,12,13,14,15,16,17,18,19,20]; // déclare un tableau de 10 entiers int[] c; c = a ~ b[0..5]; // soit c contient 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
Tableau et référence
Une référence est une variable faisant référence à une donnée. Par exemple
une variable a
et une variable b
peuvent pointer sur
la même valeur. Si on modifie la valeur de a
on modifie
b
car a
et b
se réfère sur la même
valeur. Ainsi plusieurs variable peuvent faire référence à une même
donnée. L'avantage:
- c'est simple à manipuler (par rapport au pointeur)
- évite de copier un tableau (plus le tableau est grand plus c'est couteux à copier)
L'inconvénient c'est simplement de faire attention que l'on utilise une référence. Si les les données sont modifiées alors toutes les variables faisant référence auront la nouvelle données.
import std.stdio; import std.ascii : newline; void main( ){ size_t[3] a = 5; // les 3 éléments du tableau prennent la valeur de 5 size_t[3] b = a; // b étant stant statique les données de a sont copiées size_t[] c = a; // c étant stant dynamique se réfère au même donné que a size_t[3] d = a.dup; // d est un tableau statique recevant une copie de a size_t[] e = a.dup; // e est un tableau dynamique recevant une copie de a a = 6; // les 3 éléments du tableau prennent la valeur de 6 writeln( a, newline , b, newline, c, newline, d, newline, e ); }
$ ldc2 ref.d $ ./ref [6, 6, 6] [5, 5, 5] [6, 6, 6] [5, 5, 5] [5, 5, 5]
La variable c
étant une référence sont contenus change lorsque
l'on modifie a
. Si c
est modifié a
l'est
également car les deux variables se réfèrent aux mêmes données.
En D comme en python les tableaux dynamique sont passés par défaut par référence aux fonctions. Ceci est plus efficace que le passage par copie mais peut provoquer des comportements non voulue.
import std.stdio; void ajoute(uint nombre, size_t[] tableau){ tableau[] += nombre; } void main(){ size_t[] montableau = [1,2,3,4,5,6]; // tableau dynamique ajoute(2, montableau); writeln(montableau); }
$ ldc2 copy.d $ ./copy [3, 4, 5, 6, 7, 8]
Dans cet exemple le tableau montableau
a était modifié dans la
fonction ajoute
mais ceci a modifié les valeurs du tableau
original. Ce comportement, n'est pas désiré dans certains cas, où l'on souhaite
rais travailler sur une copie du tableau pour ne pas modifier l'originel. Dans
ce cas il suffit d'utiliser la propriété .dup
à la ligne 8.
ajoute(2, montableau.dup);
Ce qui donne:
$ ldc2 copy.d $ ./copy [1, 2, 3, 4, 5, 6]
Note : Vous avez remarquez comment ajouter + x a tous les élément du tableaux sans faire un boucle (indice re-regarder la fonction ajoute )
Attention : Si montableau est un tableau statique alors par défaut il est passé par valeur Ce changement permet une meilleure vectorisation du code (notamment pour l'utilisation par le compilateur d'instructions comme les SSE) et facilite la programmation concurrente.
int[4] tableauStatique; int[] tableauDynamique = new int[](4); fonction(tableauStatique); // passage par valeur ! fonction(tableauStatique[]); // avec un slice, on passe par référence, comme en D1 fonction(tableauDynamique); // passage par référence, ça ne change pas entre D1 et D2
Les matrices
Peu de langages supportent nativement les matrices, mais le langage D en fait partie.
Qu'est-ce qu'une matrice
Mieux qu'un long discours voici un exemple :
[1, 1, 1] [1, 1, 1] [1, 1, 1] [1, 1, 1]
Ceci est une matrice de 3 colonnes par 4 lignes. Un tableau de tableau
uint[3][4] matrix; // déclaration de la matrice tous les élément de la matrice sont à 0 foreach (ligne; matrix){ ligne[0..$] = 1;// on remplit la matrice de "1" en utilisant le slicing, c'est pratique ! }
Opérations arithmétiques sur les tableaux
Le D permet de manipuler élégament les tableaux mais en plus facilite grandement les opérations sur un tableau.
Comment ajouter, soustraire, multiplier ou diviser tous les éléments du tableau
size_t[] arr = [0, 1, 2, 3] arr[] += 1; // --> [ 1, 2, 3, 4 ] arr[] *= 2; // --> [ 2, 4, 6, 8 ] arr|] /= 2; // --> [ 1, 2, 3, 4 ] arr[] -= 1; // --> [ 0, 1, 2, 3 ]
Calculer simplement un produit scalaire (dot prouduct)
Voici comment calculer un produit scalaire
Et voici comment on pourrait le calculer en D
import std.stdio; void main( ){ size_t[3][3] m = [ [0,1,2], [0,1,2], [0,1,2] ]; size_t[3] v = [2,4,6]; size_t[3] result; writeln( m ); writeln( v ); foreach( index, row; m ){ row[] *= v[]; result[ index ] = row[0] + row[1] + row[2]; } writeln( result ); }
Les tableaux associatif ( hash / dictionnaire )
int[char[]] x;
Un tableau associatif est un tableau où l'on associe 2 valeurs ensemble. Ici, on associe une chaîne de caractères à un entier.
Astuce: pour comprendre ce que que représente un tableau associatif, je vous conseille de le lire de la droite vers la gauche. Soit :
Le tableau associatif x contient des chaînes de caractères qui sont associées à un entier
Exemple :
size_t[string] x; // association d'une chaîne de caractère à un entier positif x["pomme"] = 2; // on met 2 pommes x["poire"] = 5; // on met 5 poires x["orange"] = 7; // on met 7 oranges size_t quantite = x["poire"]; // renvoie le nombre de poire delete x["orange"]; // on supprime la clé orange
Dans l'exemple ci-dessus, on associe un fruit à un nombre (pratique pour connaitre la quantité restante de chaque fruit).
Les propriétés
Propriétés | Descriptions |
---|---|
.sizeof | Retourne la taille de la réference du tableau dynamique, qui est de 8 sur un ordinateur 32 bits |
.length | Permet d'obtenir ou de changer la taille du tableau |
.ptr | Retourne un pointeur sur le premier élément du tableau |
.reverse | Inverse l'ordre des élements (le premier devient le dernier) et retourne le tableau inversé |
.sort | Trie les éléments du tableau et renvoie le tableau trié |
.dup | Crée un tableau dynamique de la même taille, et copie tous les éléments dans ce tableau puis retourne ce tableau |
Propriétés | Descriptions |
---|---|
.sizeof | Retourne la taille du tableau multipliée par le nombres d'octets pour chaque élément du tableau |
.length | Retourne le nombre d'éléments dans le tableau. Cette valeur ne peut pas être modifiée pour les tableaux statiques |
.ptr | Retourne un pointeur sur le premier élément du tableau |
.sort | Trie les éléments du tableau et renvoie le tableau trié |
.dup | Crée un tableau dynamique de la même taille et copie tous les éléments dans ce tableau puis renvoie ce tableau |
Propriétés | Descriptions |
---|---|
.sizeof | Retourne la taille du tableau multipliée par le nombres d'octets pour chaque élément du tableau |
.length | Retourne le nombre d'éléments dans le tableau. Cette valeur ne peut pas être modifiée pour les tableaux associatifs |
.keys | Retourne un tableau dynamique, dans lequel les éléments du tableau sont les clés du tableau associatif |
.values | Retourne un tableau dynamique, dans lequel les éléments du tableau sont les valeurs du tableau associatif |
.rehash | Réorganise le tableau associatif sur lui même afin que la recherche d'élément soit plus éfficace. Rehash est intéressant par exemple quand le programme à terminé de charger les données et que l'on va ensuite interoger le tableau associatif. Retourne une référence vers le tableau associatif. |
.byKey() | Retourne un delegate pour l'utiliser dans une boucle foreach, itérant sur les clés du tableau associatifs |
.byValue() | Retourne un delegate pour l'utiliser dans une boucle foreach, itérant sur les valeurs du tableau associatifs |
.get(Key key, lazy Value defaultValue) | Recherche la clé; si elle existe retourne la valeur correspondante sinon évalue puis retourne la valeur par défaut |
Les boucles
Une boucle permet de répéter un même code x fois. Comme par exemple d'écrir sur la sortie standard (terminal) la valeur de chaque élment d'un tableau.
La boucle Tant que (while
)
import std.stdio; void main(){ size_t = 0; while (i < 10){ // tant que i strictement inférieur à 10 writeln(i); // écrire sur la sortie standard la valeur de i i++; // Incremente la valeur de i } }
Attention: pensez à bien augmenter la valeur de
i
pour qu'elle puisse atteindre la condition d'arrêt de boucle.
sinon vous aurez une jolie boucle infinie
La boucle Faire tant que (do while
)
Cette boucle est semblable à la précédente, seulement ici on garantit au moins une fois le passage dans la boucle.
import std.stdio; void main(){ size_t i = 20; do{ // faire: writeln(i); // écrire sur la sortie standard la valeur de i i++; // Incremente la valeur de i }while (i < 10) // tant que i strictement inférieur à 10 répéter les instructions }
Bien que i
est plus grand que 10 les instructions sont éxécuté
une fois.
La boucle Pour (for
)
On peut effectuer la déclaration d'une variable, définir les conditions de la boucle et définir une action exécutée à chaque début de boucle (généralement l'incrémentation de cette variable) dans la déclaration de la boucle for :
for (size_t i = 0; i < 10; ++i){ // pour i de 0 à 10 exclu writeln(i); // écrire sa valeur sur la sortie standard }
La boucle Pour chaque (foreach
)
size_t[5] a = [1,5,4,6,8]; foreach(element;a){ writeln(element); }
Pour chaque élément de a, on imprime tour par tour sa valeur sur la sortie standard ( 1 puis 5 puis 4 …).
On peut également compter le nombre d'itérations dans la boucle foreach. Par exemple, pour connaître le numéro de ligne ou l'indice du tableau en cours de traitement.
size_t[5] a = [1,5,4,6,8]; foreach(compteur,element; a){ writeln("index: ", compteur, "valeur: ", element); }
Aller à (goto
)
Le goto n'est à utiliser que dans des cas précis. Il ne faut surtout pas en abuser et la plupart du temps les autres types de boucles suffisent. Depuis le temps que je programme je n'ai eu à l'utiliser qu'une seule fois. Le goto permet d'aller directement à un endroit du code défini par une étiquette :
import std.stdio; void main(){ size_t i = 0; writeln("Bonjour"); monEtiquette: i++; writeln("Valeur de i ", i); if ( i < 2 ){ goto monEtiquette; } writeln("Fin"); }
Les fonctions
Les fonctions peremttent de définir des bouts de code qui seront utilisé plusieurs fois. Ainsi ça évite d'écrire un peu partout des bouts de code qui font la même chose. et c'est donc bien plus pratique de corriger à un endroit l'éventuel problème qu'un peu partout dans le code. L'objectif est de factoriser son code c'est à dire de faire au plus possibles des fonctions.
Les bases
Commençons par une fonction simple qui multiplie par deux la valeur qu'on lui donne.
size_t double( size_t x ){ return x * 2; }
Décorticons la syntaxe:
- tout d'abord on indique le type de valeur retourné par la fonction ici un entier positif
- puis le nom de la fonction elle s'appelle
double
(simple et intuitif) - entre parenthèse on définit les arguments acceptés par la fonction, le type et le nom qu'aura la variable dans la fonction
- entre les accollades
{}
est écrit le corps de la fonctions ( les instructions qui seront éxécuté à chaque appel) - et on oublie pas de dire à la fin ce qui est retourné (avec le mot clé
return
)
Utilisation de la fonction
import std.stdio; size_t double( size_t x ){ return x * 2; } void main(){ size_t i = double( 4 ); writeln( "le double du chiffre 4 est ", i ); size_t x = 6; writeln( "le double du chiffre ", x, " est ", double( x ) ); }
$ ldc2 double.d $ ./double le double du chiffre 4 est 8 le double du chiffre 6 est 12
- On assigne le résultat de la fonction
double( 4 )
à la variablei
- la fonction
double
reçoit la valeur 4 qui est attribué à la variablex
au sein de la fonction - la fonction returne le résultat de l'opération
4 * 2
- la fonction
- On écrit sur la sortie standard
le double du chiffre 4 est 8
- On assigne la valeur de 6 à la variable
x
- On écrit sur la sortie standard
le double du chiffre x est double( x )
- la valeur de
x
est évalué ( 6 ) - la fonction
double( 6 )
est évalué- la fonction
double
reçoit la valeur 6 qui est attribué à la variablex
au sein de la fonction - la fonction returne le résultat de l'opération 6 * 2
- la fonction
- la valeur de
- On écrit sur la sortie standard
le double du chiffre 6 est 12
Les fonctions avec plusieurs arguments
Pour définir des fonctions devant manipuler plusieus variables il faut créer atant de paramètre que de variable nécessaire. Par exemple une addition nécessite au minimum deux variables et retourne le résultat. Cette fonction peut s'écrire ainsi:
size_t addition( size_t a, size_t b ){ return a + b; }
Il suffit donc de séparer les différents paramètre par une virgule
,
Les fonctions sans arguments
On peut avoir besoin d'une fonction qui ne prends pas de paramètre. Elle à
connaissance de toutes les informations nécessaire pour son fonctionnement..
Par exemple une fonction qui écrirait Bonjour
dans le
terminal.
import std.stdio; void bonjour(){ writeln( "Bonjour" ): }
Simplement il suffit de ne rien mettre en les parenthèses ()
.
De plus ici la fonction ne retourne aucune valeur pour cette raison on utilise
le mot clé void
.
Les fonctions avec des valeurs par défauts
On peut définir des des valeurs par défaut aux paramètres dans le cas ou lors de l'appelle on ommetrait de les fournirs. Et ainsi obtenir un comportement par défaut. Par exemple: une fonction qui retourne la valeur d'une variable puissance x , et qui par défaut se soit la valeur de la variable puissance 2 qui est calculé.
import std.stdio; size_t puissance( size_t valeur, size_t exposant = 2 ){ return valeur^^exposant; } void main(){ size_t x = 3; size_t e = 4; size_t i = puissance(x); size_t j = puissance(x, e); writeln( x, " puissance ", 2, " = ", i ); writeln( x, " puissance ", e, " = ", j ); }
$ ldc2 puissance.d $ ./puissance 3 puissance 2 = 9 3 puissance 4 = 81
Attention : Les paramètres prenant des valeurs par défaut doivent être listées en dernier.
Les fonctions avec un nombre variable d'argument
Reprenons la fonction addition précédente, si l'on souhaite faire 2 + 4 +
3
ou encore 12 + 4 + 8 +1
… On devrait écrire toutes les
vraiantes de fonctions avec le nombre de paramètre à gérer . Mais
heureusement en D y a plus simple .
size_t addition( size_t[] a... ){ size_t resultat = 0; foreach( valeur; a ) resultat += valeur; return resultat; }
Facile, hein ceci s'appelle une fonction variadic ( pour le jargon ).
import std.stdio; size_t addition( size_t[] a... ){ size_t resultat = 0; foreach( valeur; a ) resultat += valeur; return resultat; } void main(){ writeln( "2 + 4 = ", addition( 2, 4 ) ); writeln( "3 + 5 + 1 + 8 = ", addition( 3, 5, 1, 8) ); }
$ ldc2 addition.d $ ./addition 2 + 4 = 6 3 + 5 + 1 + 8 = 17
Note: Il ne peut avoir qu'un seul paramètre variadic par fonction et ce dernier doit se trouver à la fin, car autrement on ne pourrait déterminer à quel variable attribuer les valeurs.
Les fonctions modèles (template)
Présentation
Il est fréquent ou l'on souhaite définir une fonction générique acceptant
des entiers positifs, des flotants ou autres. Par exemple avoir une fonction
addition qui permet de travailler quelque soit le type qu'on lui donne.
Comme : 2 + 5
; 2.5 + 9.1
; 2.5
+ 6
. Dans les deux premiers exemples on utilise le même type, entier
positif pour le premier et nombre décimaux pour le second.
On peut écrire dans ce cas:
T add( T )( T a , T b ){ return a + b; }
On indique que si l'on souhaite utiliser la fonction add
on
doit préciser quel type vaut T .
size_t a = 2; size_t b = 3; size_t c = add!(size_t)( a, b ); // vaut 5
Lors de l'apelle de la fonction add
on indique que
T
est de type size_t
. Vous pouvez faire de même avec
les autres types. Les types utilisés par la fonction sont entre !(
… )
comme on le fait pour les variables. Le symbole !
indique donc que l'on fournit un ou plusieurs types.
float d = 7.5; float e = 8.1; float f = add!(float)( d, e ); // vaut 15.6
La fonction générique nous évite décrire toutes les formes possibles :
int Add( uint a, uint b ){ return a+b; } uint Add( uint a, uint b ){ return a+b; } long Add( long a, long b ){ return a+b; } ulong Add( ulong a, ulong b ){ return a+b; } float Add( float a, float b ){ return a+b; } float Add( float a, float b ){ return a+b; } double Add( double a, double b ){ return a+b; }
Note : Lorsqu'il y a un seul type à indiquer les parenthèses autour de celui-ci sont facultatives ( voir ci-dessous ).
float a = add!(float)( 7.5, 8.1 ); // vaut 15.6 float b = add!float( 7.5, 8.1 ); // vaut 15.6
Notion avancé
La fonction générique précédente ne permet pas de calculer 2.5 +
6
car le premier est un nombre décimal et le second un nombre entiers et
T
ne peut être que de un type. Pour celà il suffit simplement de
rajouté une nouvelle "variable" de type que l'on va nommer U
(
vous choisisseez le nom que vous Souhaitez la convention veut que la
première lettre soit en majuscule ).
T add(T, U)( T a , U b ){ return a + b; } size_t a = add!(size_t, size_t)( 2, 3 ); // vaut 5 float f = add!(float, size_t)( 2.5, 6 ); // vaut 8.5
Attention Le type de valeur retourné par la fonction dépends ici du type premier paramètre est peu donc avoir un comportement non voulu dans certains cas. Comme :
T add( T, U )( T a , U b ){ return a + b; } float a = add!(size_t, float)( 2, 3.5 ); // vaut 5 et non 5.5 car T est un entier donc la valeur est tronqué
Il est donc nécessaire de définir une "variable" pour le type de retour.
R add( T, U, R )( T a , U b ){ return a + b; } size_t a = add!(size_t, size_t, size_t)( 2, 3 ); // vaut 5 float f = add!(float, size_t, float)( 2.5, 6 ); // vaut 8.5
Appelé la fonction add devient un ppeu long pour l'alléger il est possible d'ajouter des types par défaut.
R add( T, U, R = float )( T a , U b ){ return a + b; } size_t a = add!(size_t, size_t, size_t)( 2, 3 ); // vaut 5 float f = add!(float, size_t)( 2.5, 6 ); // vaut 8.5
Ainsi si on précise pas le type de retour sera un float, ce qui est le plus sûre.
Les fonctions macros (template)
La syntaxe d'appelle uniforme de fonction (UFCS
)
Les delegates ( closures )
Les entrées / Sorties
Nous allons voire dans ce chapitre, comment lire un fichier ou bien une
entrée clavier, écrire dans un fichier et sur le terminal. Les opérations
d'entrée / sortie ( Input/Output i/o) sont définis dans le module
std.stdio
.
Écrire sur le terminal
Pour écrire dans le terminal vous avez vu au moins une façon de faire:
writeln
. Il y a principalment quatres fonctions pour écrire:
write
writeln
writef
writefln
Par convention :
- le suffix
ln
signifieline new
soit nouvelle ligne, les fonctionswriteln
etwritefln
rajoute une nouvelle ligne à la fin de la phrase. - le suffix
f
signifieformat
c'est à dire on pré-construit la chaine de caractère en indiquant l'emplacement futur des valeurs des variables%<lettre>
.
Exemple avec write
et writeln
import std.stdio; void main(){ write( "Coucou " ); writeln( "le D c'est cool!" ); writeln( "Je dirais même plus: le D c'est cool!" ); }
Coucou le D c'est cool! Je dirais même plus: le D c'est cool!
La fonction write
ne rajoutant pas de nouvelle ligne à la fin
on peut écrire à la suite.
Utilisation de writef
et writefln
Ces deux fonctions permettent de formater une chaine de caractère. C'est à
dire de pré-construire la chaine avec les éléments invariants et de spécifier
via %<spécificateur>
l'endroit ou seront insérer les futur
valeurs des variables.
Késako ?
Un exemple vaut mieux qu'un long discours.
import std.stdio; void main(){ string prénom = "Paul"; size_t âge = 36; writef( "Coucou %s ", prénom ); writefln( "as-tu %d ans ?", âge ) }
Coucou Paul as-tu 36 ans ?
Dans cet exemple :
"Coucou %s "
indique que le motCoucou
sera suivis d'une valeur de typestring
car la lettre utilisée ests
. La valeur de la variable prénom ira à cet endroit."as-tu %d ans ?"
indique que l'on va insérer une valeur de type nombre entier car la lettred
est employée.
Syntaxe générale: % drapeaux largeur .precision modificateur type
( sans les espaces entre chaque
)
Voici le tableau informant sur les caracteristiques du formattage de chaine
et les informations correspondant l'usage de la lettre s
,
d
, … .
Spécificateur | Sortie | Example |
---|---|---|
c | Caractère | a |
d or i | Nombre décimal ou entier positif/négatif | 392 |
e | Notation scientifique (mantisse/exposant) utilisant la lettre e | 3.9265e+2 |
E | Notation scientifique (mantisse/exposant) utilisant la lettre E | 3.9265E+2 |
f | Nombre décimal | 392.65 |
g | Utilise la courte notation soit %e ou %f | 392.65 |
G | Utilise la courte notation soit %E ou %F | 392.65 |
o | Octet positif | 610 |
s | Chaine de charactères | sample |
u | Nombre décimal ou entier positif | 7235 |
x | Nombre hexadécimal positif | 7fa |
X | Nombre hexadécimal positif (lettres capital) | 7FA |
% | Un symbôle % suivis par un autre % écrira % | % |
Quelques exemples d'utilisation
import std.stdio; void main(){ writefln( "Date: %u/%u/%u", 30, 8, 2012 ); writefln( "Nom: %s | Prénom: %s", "Doe", "John"); writefln( "Taille: %f", 1.8 ); writefln( "Distance: %e", 5000000.0); writefln( "Distance: %E", 5000000.0); writefln( "Distance: %g", 5000000.0); writefln( "Distance: %G", 5000000.0); writefln( "Pourcentage: %u%%", 50 ); writefln( "Tableau: %s", [0, 11, 54, 42] ); }
Date: 30/8/2012 Nom: Doe | Prénom: John Taille: 1.800000 Distance: 5.000000e+06 Distance: 5.000000E+06 Distance: 5e+06 Distance: 5E+06 Pourcentage: 50% Tableau: [0, 11, 54, 42]
drapeaux | description |
---|---|
- | Justifie à gauche dans la largeur du champs données; Justifcation à droite est le comportement par défaut (voir largeur sous-spécificateur). |
+ | Force l'écriture du signe avant le résltat un signe plus ou un moin (+ ou -) même pour les nombres popsitives. Par défaut, seulement les nombres négatifs sont précédés par le signe -. |
(espace) | Si aucun signe est écrit, un espace est inséré avant la valeur. |
# | Utilisé avec les spécificateurs o, x ou X la
valeur est précédée de 0, 0x ou 0X respectivment
pour les valeurs pour les valeurs différentes de 0. Utilisé avec les spécificateurs e, E et f, il force l'écriture pour contenir un point décimal même s'il y a pas de chiffre qui se suivent. Par défaut, s'il n'y a pas de chiffre qui se suivent le point décimal n'est pas écrit. Utilisé avec les spécificateurs g ou G le résultat est le même qu'obtenu avec e ou E mais des zéros à droite ne sont pas supprimés. |
0 | Met sur la gauche le nombre avec des zéros (0) au lieu des espaces, où l'espace à gauche est spécifié (voir largeur sous-spécificateur). |
largeur | description |
---|---|
(nombre) | Nombre minimum de caractère à écrire. Si la valeur à écrire est plus courte que ce nombre, le résultat est aligné avec des spaces. La valeur n'est pas tronqué même si la valeur est plus grande que le nombre donné. |
* | La largeur n'est pas spécifier dans la chaîne formatté, mais un argument supplémentaire de type entier prrécèdant l'argument devant être formatté. |
import std.stdio; void main(){ // écrit un nombre décimal writefln( "Taille: %f", 1.8 ); // garantit l'écriture du point décimal writefln( "Taille: %#f", 1.8 ); // écrit le nombre dans un espace de 10 champs writefln( "Taille: %10f", 1.8 ); // garantit l'écriture du point décimal, écrit le nombre dans un espace de 10 champs writefln( "Taille: %0#10f", 1.8 ); // écrit le nombre dans un espace de 10 champs en commençant par la gauche writefln( "Taille: %-10f", 1.8 ); // ecrit le symbôle positif, garantit l'écriture du point décimal, le nombre décimal writefln( "Taille: %+#f", 1.8 ); }
Taille: 1.800000 Taille: 1.800000 Taille: 1.800000 Taille: 001.800000 Taille: 1.800000 Taille: +1.800000
.précision | description |
---|---|
.number | Pour un spécificateur de type entier (d, i, o,
u, x, X) la précision spécifie le nombre
minimum de chiffres à écrire. Si la valeur devant être écrite est plus courte
que ce nombre, le résultat est complétée par des zéros. La valeur n'est pas
tronquée, même si le résultat est plus grand. Une précision de
0 signifie que pas de caractère est écrit pour la valeur 0 . Pour les spécificateurs: e, E et f c'est le nombre de chiffres à imprimer après le point décimal. Pour les spécificateurs: g et G C'est le nombre maximal de chiffres significatifs à imprimer. Pour s: c'est le nombre maximal de caractères à être imprimés. par par défaut tous les caractères sont imprimés jusqu'à ce que le caractère nul de fin est rencontrées. Pour c Type: elle n'a pas effet. En l'absence de précision est spécifié, la valeur par défaut est 1. Si la période est spécifiée sans valeur explicite pour précision, 0 est pris en charge. |
.* | La précision n'est pas spécifier dans la chaîne formatté, mais un argument supplémentaire de type entier prrécèdant l'argument devant être formatté. |
import std.stdio; void main(){ // écrit un nombre décimal writefln( "Taille: %f", 1.8 ); // écrit un nombre décimal avec une précision de 2 chiffres après la virgule writefln( "Taille: %.2f", 1.8 ); // écrit un nombre décimal avec une précision de 4 chiffres après la virgule writefln( "Taille: %.*f", 4, 1.8, ); // permet de varier la précision via une variable }
Taille: 1.800000 Taille: 1.80 Taille: 1.8000
Note: le module std.string
permet
de formatter des chaînes de caractère.
import std.string; … string date= "30/08/12"; string a = "Nous sommes le %s".format( date ); // -> Nous sommes le 30/08/12 …
Lire et écrire dans un fichier
Pour lire ou écrire dans un fichier on utilise std.stdio.File
.
File est une structure permettant d'ouvrir et fermer un fichier. Il y a
plusieurs mode pour ouvrir un fichier:
"r" | Ouvre un fichier en lecture. Le fichier doit exister. |
"w" | Créer un fichier vide pour l'écriture. Si le fichier existe déjà le contenu sera écrasé et le fichier traité comme un fichier vide . |
"a" | Ajoute à un fichier. Écrit les données à la fin duu fichier. Le fichier est créer s'il n'existe pas. |
"r+" | Ouvre un fichier pour mettre à jour la lecture et l'écriture. Le fichier doit exister. |
"w+" | réer un fichier vide pour la lecture et l'écriture. Si le fichier existe déjà le contenu sera écrasé et le fichier traité comme un fichier vide . |
"a+" | Ouvre un fichier pour la lecture et l'ajout. toutes les opératiosn d'écriture sont effectué à la fin du fichier, protegeant le contenu d'être écrasé. Vos pouvez repositionnez (seek, rewind) . La lecture peut se faire n'importe où dans le fichier, tandis que les opérations d'écriture se feront à la fin du fichier. Le fichier est créer s'il n'existe pas. |
Lire dans un fichier
Créer un fichier nommé monFichier.txt
cat <<EOF> monFichier.txt bla bloh bloh bluh bluh bluh EOF
Ligne par ligne avec foreach
import std.stdio; void main(){ File f = File( "monFichier.txt", "r" ); // ouvre le fichier en lecture foreach( line; f.byLine ) // lit ligne par ligne writeln( line ); // écrit le contenu de la ligne sur la sortie standard f.close(); // ferme le fichier }
bla bloh bloh bluh bluh bluh
Note: la méthode byLine ne stock pas le retour à
la ligne présent dans le fichier. C'est la fonction writeln
qui
créer le retour à la ligne sur la sortie standard.
Ligne par ligne avec while
import std.stdio; void main(){ File f = File( "monFichier.txt", "r" ); // ouvre le fichier en lecture while( ! f.eof ){ // boucle tant que l'on a pas atteint la fin du fichier string line; f.readln(line); /* stock le contenu de la ligne courrante dans la variable puis se positionne sur la ligne suivante */ writeln( line ); // écrit le contenu de la ligne sur la sortie standard } f.close(); // ferme le fichier }
bla bloh bloh bluh bluh bluh
Note: La méthode readln stock le retour à ligne
présent dans la ligne, donc avec l'utilsation de writeln
on écrit
deux retours à la ligne d'où ces espaces.
Chargé le contenu d'un fichier en mémoire
import std.stdio; void main(){ File f = File( "monFichier.txt", "r" ); char[2048] buffer; char[] content = f.rawRead( buffer ); f.close(); writeln( content ); }
bla bloh bloh bluh bluh bluh
La fonction rawRead
nécessite de lui spécifier un buffer
évitant ainsi des allocation mémoire trop fréquente pour stocker les données en
train d'être lu, gagnant en rapidité de traitement. Ici j'ai creer un buffer
pouvant contenir 2048 caractères, vous pouvez changer ce nombre à votre
convenance. Les données lu ne sont pas découpés par ligne mais mise de façon
brute ainsi que les caractère saut de ligne comme "\\n", \\r\\n" …
Écrire dans un fichier
Pour écrire dans un fichier nous utilisont toujours la structure
File
.
import std.stdio; void main(){ File f = File( "monFichier.txt", "w" ); // ouvre le fichier en écriture f.write( "Coucou" ); // écrit Coucou dans le fichier f.writeln( " , j'apprends le langage D" ); // écrit à la suite de Coucou puis saute une ligne f.writefln( "Je mets %d/20 à l'auteur du tuto", 16 ); // écrit "Je mets 16/20 à l'auteur du tuto" puis saute une ligne f.close(); // ferme le fichier }
$ cat monFichier.txt Coucou , j'apprends le langage D Je mets 16/20 à l'auteur du tuto
Lire les entrées clavier
import std.stdio; void main(){ // lit une chaine de caractere juqu'au retour à ligne inclus. string nom = readln(); writefln( "Je m'appel %s", nom ); }
Afin de supprimer le retour à la ligne lorsque l'on valide le mot on utilise
std.string.chomp
import std.stdio; import std.string; void main(){ /* lit une chaine de caractere jusqu'au retour à ligne inclus puis supprime le retour à la ligne */ string nom = readln().chomp(); writefln( "Je m'appel %s", nom ); }
Il est égalment possible d'indiquer le type de la valeur données par
l'utilisateur. Car par défaut tout est du texte. Pour celà on peut utiliser
std.stdio.readf
.
import std.stdio; import std.string; void main(){ string nom; string prénom; size_t âge; write( `Entrer les informations sous la forme "NOM Prénom âge" : ` ); // lire une chaine de caractere dans le format spécifié readf( "%s %s %u", &nom, &prénom, &âge ); writefln( "Je m'appel %s %s et j'ai %u ans", nom, prénom, âge ); }
Entrer les informations sous la forme "NOM Prénom âge" : MERCIER Jonathan 26 Je m'appel MERCIER Jonathan et j'ai 26 ans
Ici on demande à l'utilisateur d'entrer, une chaine de cacractère, espace, une autre chaine de cacractère, espace, un nombre entier positif
Si le format n'est pas correcte, par exemple on donne une chaîne de carctère
alors qu'un nombre est attendu, l'exception std.conv.ConvException
est levée.
Les exceptions
Je suis venu, j'ai codé, l'application a planté. Julius C'ster
La démarche pour gérer les erreurs peut être différentes, comme le pense les développeurs du langage D. Ils partent du principe que toutes les applications doivent gérer les erreurs. Les erreurs sont des conditions inattendues ne faisant pas partie du déroulement normale du programme. Exemple d'erreur communément rencontré:
- Plus de mémoire ( out of memory )
- Plus d'espace sur le disque dur ( out of disk space )
- Tentative d'écrire dans un fichier ouvert en lecture seulement
- Tentative de lire un fichier inexistant
- Demander un service système qui n'est pas pris en charge.
Comme elles sont innatendues il est comlpiqué de réalisé unocde robuste pour gérer toutes les erreurs.
Cette partie est une traduction provenamnt du document sur les erreurs
Le problème de capture des erreurs
La philosophie C
La traditionnelle approche de détection et de signalement n'est pas conventionnel. Ils ont été institué spécialement pour répondre à un besoin et varie d'une fonction à l'autre:
- Retour d'un pointeur
NULL
- Retour d'une valeur 0
- Retour d'un code d'erreur non nul
- Exiger qu'une fonction doit être appelée pour vérifier si la fonction précédente a échoué.
Pour faire face à ces éventuelles erreurs vous devriez écrire un fastidieux
code de gestion d'erreur et ceci pour chaque fonction. Si une erreur survient,
le code doit être écrit afin de récupérer de l'erreur, puis l'erreur doit être
signalée à l'utilisateur d'une certaine façon conviviale. Si une erreur ne peut
être traitée localement, il doit être explicitement propagé en retour à son
appelant. La longue liste des valeurs de errno
doit être convertie
en texte approprié pour être affiché. Ajouter tout ce code pour implémenter ce
comportement peut consommer une grande partie du temps passé à coder un projet
- et encore, si une nouvelle valeur errno
est ajouté au système
d'exécution, l'ancien code ne peut pas afficher correctement un message
d'erreur significatif.
Un bon code de gestion d'erreur tend à encombrer ce qui serait autrement une mise en œuvre soignée et propre à la recherche.
Pire encore, une bonne gestion des erreurs de code est elle-même sujette aux erreurs, tend à être le moins testé (et donc buggy) dans le cadre du projet, et il est souvent tout simplement omis. Le résultat final est probablement un «écran bleu de la mort», comme quoi le programme a échoué pour faire face à une erreur inattendue.
Un programmes rapidement écrit et sales ne peut pas avoir le code fastidieu pour la gestion des erreurs, et ainsi ces services publics ont tendance à être comme utiliser une scie circulaire à table sans protège-lames.
Ce qu'il faut, c'est une philosophie de gestion des erreurs et la méthodologie de telle sorte que:
- Elle est standardisée - utilisation cohérente, Elle est plus utile.
- Le résultat est raisonnable, même si le programmeur ne parvient pas à vérifier les erreurs.
- Les anciens code peuvent être réutilisé avec un nouveau code sans avoir à modifier l'ancien code pour être compatible
avec les types d'erreurs nouvelles.
- Pas d'erreurs ignoré par inadvertance.
- «Rapide et sale» desnouveaux services publics peuvent être écrit et toujours gérer correctement les erreurs.
- Il doit être facile de visualiser le code source dont provient l'erreur.
La philosophie D
Nous devons d'abord faire quelques observations et des hypothèses au sujet des erreurs:
- Les erreurs ne font pas partie de l'écoulement normal d'un programme. Les erreurs sont exceptionnelles, inhabituelles
et inattendues.
- Parce que les erreurs sont rares, l'exécution de code de gestion d'erreur n'est pas la performance critique.
- Le flux normal du programme est la performance critique.
- Toutes les erreurs doivent être traitées avec une certaine façon, soit par un code écrit explicitement à les
manipuler, ou par quelque manipulation du système par défaut.
- Le code qui détecte une erreur en sait plus sur l'erreur que le code qui doit récupérer de l'erreur.
La solution consiste à utiliser la gestion des exceptions pour signaler les
erreurs. Toutes les erreurs sont des objets issus de la classe abstraite
d'erreur. La classe Erreur a une fonction virtuelle pure appelée toString
()
qui produit un char
avec une description
lisible par l'homme de l'erreur.
Si le code détecte une erreur comme out of memory@, puis une erreur
est levée avec un message disant
Out of memory@@. La pile d'appel est
déroulée, à la recherche d'un gestionnaire pour l'erreur. Enfin les blocs sont
exécutées tel que la pile est déroulée. Si un gestionnaire d'erreur est
trouvée, l'exécution reprend là. Si non, le gestionnaire d'erreur par défaut
est exécutée, affichant le message et termine le programme.
Comment cela peut répondre à nos critères?
Elle est standardisé - utilisation cohérente, elle est plus utile.
- C'est le moyen utilisé en D, et il est utilisé de manière cohérente dans la bibliothèque d'exécution D et dans les exemples.
Le résultat est un résultat raisonnable, même si le programmeur ne parvient pas à vérifier les erreurs.
- Si aucun gestionnaire de captures est là pour les erreurs, puis le programme proprement sorties par le biais du gestionnaire d'erreur par défaut avec un message approprié.
Les anciens code peuvent être réutilisé avec un nouveau code sans avoir à modifier l'ancien code afin d'être compatible
avec les types d'erreurs nouvelles.
- D'ancien code peuvent décider de rattraper toutes les erreurs, ou seulement des particulières, propageant le reste vers
le haut. Dans tous les cas, il n'est pas nécessaire de corréler en plus des numéros d'erreur avec les messages, le message correct est toujours fourni.
Pas d'erreurs ignoré par inadvertance.
- Les exceptions sont capturés d'une manière ou d'une autre. Il n'y a rien de
tel que le retour d'un pointeur
NULL
indiquant une erreur, puis essayant d'utiliser ce pointeurNULL
pour afficher le message d'erreur.
«Rapide et sale» des services publics peuvent être écrit et toujours gérer correctement les erreurs.
- Code rapide et sale ne doit pas écrire toutes les erreurs issue de la manipulation de code du tout, et n'ont pas besoin de vérifier
les erreurs. Les erreurs seront capturé, un message approprié s'affiche, et le programme peut proprement s'arrêter tous ça par défaut.
Il doit être facile de visualiser le code source dont provient l'erreur.
- Le
try
/catch
/finally
semble mieux que l'interminable code disantif (erreur) goto errorhandler; déclarations
.
Comment cela peut répondre à nos hypothèses sur les erreurs?
Les erreurs ne font pas partie de l'écoulement normal d'un programme. Les erreurs sont exceptionnelles, inhabituelles et inattendues.
- La gestion des exceptions D est en parfaite accord avec cela.
Parce que les erreurs sont rares, l'exécution de code de gestion d'erreur n'est pas la performance critique.
- La gestion des exceptions est un processus relativement lent.
Le flux normal du programme suit une logique de performance.
- Puisque le flux normal n'a pas à vérifier chaque appel de fonction pour les retours d'erreurs, il peut être plus
rapide d'utiliser de manière réaliste la gestion des exceptions pour gérer les erreurs.
Toutes les erreurs doivent être traitées avec une certaine façon, soit par un code écrit explicitement pour les manipuler, ou par quelque manipulation du système par défaut.
- S'il n'ya pas de gestionnaire pour une erreur particulière, il est géré par le gestionnaire par défaut bibliothèque d'exécution. Si une erreur est ignorée, c'est parce que le programmeur spécifiquement ajouté du code pour ignorer une erreur, ce qui signifie sans doute qu'il était intentionnelle.
Le code qui détecte une erreur en sait plus sur l'erreur que le code qui doit récupérer de l'erreur.
- Il n'y a plus nécessité de traduire les codes d'erreurs afin d'être humainement lisibles, le message lisible est généré par le code de détection d'erreur, pas le code de reprise sur erreur. Cela conduit aussi à des messages d'erreur uniformes pour la même erreur entre les applications.
L'utilisation des exceptions pour gérer les erreurs conduit à une autre question - comment écrire des programmes manipulant des exceptions de sécurité. Voici comment .
Les bases
Durant la vie d'une application il y a certaines valeurs pouvant compromettre ce dernier. Par exemple une chaine de caractères vide peut ne pas être désiré. Il est donc nécessaire de manipulé cette chaine de caractères pour qu'elle soit valide ou si ce n'est pas possible stopper le programme et avertir avec un message explicatif le problème. Afin de protéger le programme durant son exécution on peut utiliser les exceptions.
import std.stdio; void écrire( string phrase ){ try{ File f = File( "monFichier.txt", "a" ); if( phrase == "" ) throw new Exception( "est vide" ); else f.write( phrase ); f.close(); } catch( Exception e ){ throw new Exception( "Exception non gérée: " ~ e.msg ); } } void main(){ écrire( "Bonjour" ); écrire( "" ); writeln( "fin" ); }
object.Exception@t.d(14): Exception non gérée: est vide
Dans cette exemple nous avons une fonction écrire
qui écrit
dans un fichier nommer monFichier.txt
. Lors du premier passage
dans la fonction écrire
.
- l'argument phrase vaut
"Bonjour"
- on entre dans le bloc de capture des exceptions (
try
) - Ouverture du fichier en mode ajout
- phrase n'est pas vide
- écrire dans le fichier la phrase
Bonjour
- fermeture du fichier
Lors du second passage dans la fonction écrire
.
- l'argument phrase vaut
""
( vide ) - on entre dans le bloc de capture des exceptions (
try
) - Ouverture du fichier en mode ajout
- phrase est vide
- On lève une exception ayant le message
"est vide"
- L'exception est capturé par le bloc
catch
- L'exception capturé est gérée comme un objet de type
Exception
et la variable s'appele
- On lance une nouvelle exception complétant le message
"Exception non gérée: est vide"
Il est possible de modifier le comportement du programme afin qu'il puissent retrouvé un comportement "normal" comme mettre un espace si la phrase est vide.
import std.stdio; void écrire( string phrase ){ try{ File f = File( "monFichier.txt", "a" ); if( phrase == "" ) throw new Exception( "est vide" ); else f.write( phrase ); f.close(); } catch( Exception e ){ if( e.msg == "est vide" ) écrire( " " ); else throw new Exception( "Exception non gérée: " ~ e.msg ); } } void main(){ écrire( "Bonjour" ); écrire( "" ); writeln( "fin" ); }
Lors du second passage dans la fonction écrire
.
- l'argument phrase vaut
""
( vide ) - on entre dans le bloc de capture des exceptions (
try
) - Ouverture du fichier en mode ajout
- phrase est pas vide
- On lève une exception ayant le message
"est vide"
- L'exception est capturé par le bloc
catch
- L'exception capturé est gérée comme un objet de type
Exception
et la variable s'appele
- le message de l'exception étant
"est vide"
on relance la fonctionécrire
avec un argument ayant pour valeur un espace - le programme reprend son cours normal
fin
Vous remarquerez que si une exception est levée le fichier n'est jamais
fermé. Pour remédier à cela on utilise finally
qui est en charge
de terminer les opérations avant de quitter le programme.
import std.stdio; void écrire( string phrase ){ File f; try{ f = File( "monFichier.txt", "a" ); if( phrase == "" ) throw new Exception( "est vide" ); else f.write( phrase ); f.close(); } catch( Exception e ){ if( e.msg == "est vide" ) écrire( " " ); else throw new Exception( "Exception non gérée: " ~ e.msg ); } finally{ f.close(); } } void main(){ écrire( "Bonjour" ); écrire( "" ); writeln( "fin" ); }
Il est possible de créer vos propres Exception
. Pou cela on
utilise l'héritage:
import std.exception; class MonException : Exception{ this( string message, string f = __FILE__, size_t l = __LINE__ ){ super( message, f, l ); } }
Et voilà vous avez votre exception à vous.
FILE
et LINE
sont
des varaibles spéciales elles informes le nom du fichier et la ligne courante.
Ces variables permetent d'informer à l'utilisateurs l'endroit où s'est produit
l'exception.
Maintenant que vous savez comment créer vos exceptions nous allons voir comment attraper les exceptions selon le type d'instance d'exception.
import std.stdio; class EmptyPhraseException : Exception{ this( string msg = "est vide", string f = __FILE__, size_t l = __LINE__ ){ super( msg, f, l ); } } class MonException : Exception{ this( string message, string f = __FILE__, size_t l = __LINE__ ){ super( message, f, l ); } } void écrire( string phrase ){ File f; try{ f = File( "monFichier.txt", "a" ); if( phrase == "" ) throw new EmptyPhraseException( ); else f.write( phrase ); f.close(); } catch( MonException e ){ throw new Exception( "Exception non gérée: " ~ e.msg ); } catch( EmptyPhraseException e ){ écrire( " " ); } catch( Exception e ){ throw new Exception( "Exception non gérée: " ~ e.msg ); } finally{ f.close(); } } void main(){ écrire( "Bonjour" ); écrire( "" ); writeln( "fin" ); }
Dans cet exemple ont met en évidence le système de capture des exceptions.
Lorsque les exceptions sont levée elles seront capturés par le premier bloc
catch
approprié. Donc le bloc catch( Exception e )
est le bloc le plus généraliste car toutes les exceptions dérive de cette
dernière. Ce capture toute les exceptions pour cette rason on
le place en dernier. À la manière d'un switch
, l'exception levé
sera testé avec chaque blooc catch tant qu'elle n'a pas trouvé un bloc
compatible elle passe au bloc catch suivant. Soit dans le code précédant
l'exception sera capturé par le bloc catch( EmptyPhraseException e
)
.
La bibliothèque standard Phobos fournit un module pour gérer les exceptions. Ce module permet de capturer les erreurs avec moins de ligne de code. Je vous conseil de jeter un œil à ça documentation.
Le transtypage (cast)
En langage D, il existe 2 manières d'effectuer une conversion de type :
- avec le mot clé
cast
- avec le module
std.conv
provenant de la bibliothèque standard
La première façon de faire est générique et permet de caster (convertir) tout et n'importe quoi. C'est l'équivalent du dynamic_cast pour les connaisseurs du C++. La seconde manière est restreinte au type primitif cité, mais est plus sûre.
Exemples :
import module std.conv; short a = 1; float b = 0.5; int c = to!int(a); // on utilise le template nommé "to" provenant du module std.conv et on spécifie le type int int d = cast(int)a; double e = to!double(b); // on utilise le template nommé "to" provenant du module std.conv et on spécifie le type double double f = cast(double)(b);
On importe le module std.conv
uniquement pour utiliser la
fonction template to
(le mot clé cast
n'a besoin
d'aucun module).
Les structures de données et les classes
Il est fréquent de vouloir représenter une donnée ne s'exprimant pas simplement par une valeur ou un tableau. Par exemple si vous devez simuler un Humain l'utilisation des tructures de données est d'une grande aide. Pour celà il faut définir ce qu'est un Humain. Un humain est un mammifère avec 2 jambes, 2 bras, un nom et un âge, soit:
struct Humain{ string genre; string nom; size_t nbJambes; size_t nbBras; size_t âge; }
Après avoir définis ce qu'est un Humain
on va pouvoir créer
plusieurs instances ( personne ) d'Humain
. Par convention les
structures de donnée ont leurs première lettre en majuscule.
Humain personne; // Crée une instance de type Humain personne.genre = "mammifère"; // assigne "mammifère" à l'attribut genre de l'instance personne personne.nom = "Roger"; personne.nbJambes = 2; personne.nbBras = 2; personne.âge = 28;
Voilà vous avez votre premier petit homme. Comme vous pouvez le remarquer
pour accéder aux attributs ( genre
, nom
,
nbJambes
, nbBras
, âge
) de la la
structure Humain
on utilise .
.
Les attributs
Améliorons notre structure de donnée. Précédement pour avoir une définition
complète il fallait assigner les valeurs une à un aux attributs. Afin de
pouvroir créer plus simplement des Humains on ajoute à la définition de la
structure de donnée un constructeur . Pour celà on utilise le
mot clé this
désignant l'instance courante.
struct Humain{ string genre; string nom; size_t nbJambes; size_t nbBras; size_t âge; this( string genre, string nom, size_t nbJambes, size_t nbBras, size_t âge ){ this.genre = genre; this.nom = nom; this.nbJambes = nbJambes; this.nbBras = nbBras; this.âge = âge; } }
Le moth clé this
désigne l'instance elle même de la structure
de donnée. Ainsi this.genre
représente l'attribut de la structure
Humain et genre
est la variable provenant du constructeur. Pour
construire une instance provenant de cette définition on procède comme
suit:
Humain personne = Humain( "mammifère", "Roger", 2, 2, 28 );
On construit une instance de type Humain en une instruction au lieu de six
précédement. Pour accéder aux attributs il suffit de faire <nom de
l'instance>.<nom de l'attribut>
. Donc pour imprimer sur la
sortie standard le nom de la personne:
writeln( personne.nom );
Les méthodes
Une méthode n'est rien de plus qu'une fonction définit à l'instérieur d'une
structure de données. L'intérêt des méthodes et de permettre de maipuler
l'ensemble des données definit. Par exemple si on ajoute les attributs
x
et y
afin de pouvoir positionner notre personne
dans un espace en deux dimensions. On voudras certainement vouloir faire bouger
cette personne.
Commençons par définir cet structure de donnée:
struct Humain{ string nom; size_t x; size_t y; this( string nom, size_t x = 0, size_t y = 0 ){ this.nom = nom; this.x = x; this.y = y; } void déplace( size_t x, size_t y ){ this.x += x; this.y += y; } }
Nous avons définis un constructeur prenant le nom de la personne a crée puis ça position. Si la position n'est pas donnée lors de la construction la personne sera placé au coordonné x:0, y:0 .
La méthode déplace permet de modifier les attributs x et y de la personne.
Humain roger = Humain( "Roger" ); // Roger est en x:0, y:0 Humain jacques = Humain( "Jacques", 5, 2 ); // Jacques est en x:5, y:2 roger.déplace( 5, 2 ); // Roger se déplace en x:5, y:2
Afin d'éviter des déplacements suréaliste, on va ajouter les méthodes
avance
, recule
qui utilisera en interne la méthode
déplace
.
struct Humain{ string nom; size_t x; size_t y; this( string nom, size_t x = 0, size_t y = 0 ){ this.nom = nom; this.x = x; this.y = y; } void déplace( size_t x, size_t y ){ this.x += x; this.y += y; } void avance(){ this.déplace( 1, 0); // avance sur l'axe des x de 1 pas } void recule(){ this.déplace( -1, 0); // recule sur l'axe des x de 1 pas } }
Humain roger = Humain( "Roger" ); // Roger est en x:0, y:0 Humain jacques = Humain( "Jacques", 5, 2 ); // Jacques est en x:5, y:2 roger.avance( ); // Roger avance en x:1, y:0 jacques.recule( ); // Jacques recule en x:4 y:2
Modificateur d'accessibilité
Lorsque l'on rearde lé définition précédente de notre Humain
on
remarque que rien interdit à une personne d'utiliser la méthode
déplace
. Permettant des déplacements non autorisé. Pour celà on
utilise les modificateurs d'accessibilités. Il en existe deux :
public
, private
.
Mot clé | Description |
---|---|
public | Les méthodes sont par défaut en mode public. Tout le monde y a accès. |
private | Seulement la structure de donnée peut y accèder |
Ainsi on va pouvoir interdire l'accès à la méthode déplacer de l'exterieur
grâce à private
struct Humain{ string nom; size_t x; size_t y; this( string nom, size_t x = 0, size_t y = 0 ){ this.nom = nom; this.x = x; this.y = y; } private void déplace( size_t x, size_t y ){ this.x += x; this.y += y; } void avance(){ this.déplace( 1, 0); // avance sur l'axe des x de 1 pas } void recule(){ this.déplace( -1, 0); // recule sur l'axe des x de 1 pas } }
Humain roger = Humain( "Roger" ); // Roger est en x:0, y:0 Humain jacques = Humain( "Jacques", 5, 2 ); // Jacques est en x:5, y:2 jacques.avance(); // Jacques se déplace en x:66, y:2 roger._déplace( 15, 2 ); // erreur
Roger ne pourra pas «tricher» comme la méthode déplace
est
privé un appel extérieur provoquera une erreur.
Nous avons interdit à roger de tricher en utilisant la méthode
déplace
mais ce petit malin utilise maintenant directement les
attributs x et y pour «tricher».
roger.x = 15;
Pour l'interdire de tricher vous aller me dire il suffit de mettre ces attribut en mode privée comme ça roger ne pourra plus accéder.
Oui vous avez raison mais ce n'est qu'une partie de la réponse. En effet une fois les atributs en mode privé on ne pourra plus connaître de l'exterieur la positions des personnes
Pour répondre à cette problématique on donner l'accès à la lecture des
atributs mais interdire leur modifications ( écriture ) . Par convention on
préfix les attributs privées par _
.
struct Humain{ private: string _nom; size_t _x; size_t _y; void déplace( size_t x, size_t y ){ this._x += x; this._y += y; } void x( size_t valeur ){ this._x = valeur; } void y( size_t valeur ){ this._y = valeur; } void nom( size_t valeur ){ this._nom = valeur; } public: this( string nom, size_t x = 0, size_t y = 0 ){ this.nom( nom ); this.x( x ); this.y( y ); } void avance(){ this.déplace( 1, 0); // avance sur l'axe des x de 1 pas } void recule(){ this.déplace( -1, 0); // recule sur l'axe des x de 1 pas } void x( ) const { return _x ; } void y( ) const { return _y; } void nom( ) const { return _nom; } }
Humain roger = Humain( "Roger" ); // Roger est en x:0, y:0 roger.x( 15 ); // erreur
Remarquez que j'ai définit une liste d'attributs et méthodes en mode privée
et publique en utilisant priavte
ou public
suivit de
:
. Ceci évite de le mettre pour chaque déclaration. souvenez vous
l'informaticien et feignant .
Pourquoi les méthodes void x( )
, void y( )
,
void nom( )
sont suivis du mot clé const
Très bonne question, on utilise le mot clé const pour indiquez que ces méthodes ne modifie pas les valeurs des attributs de la structure. On dit qu'il n'y a pas de modification d'état. Ceci est optionnel si vous l'oubliez ça ne changera rien à votre programme. Simplement ça permet au compilateur d'effectuer des optimisations et ainsi avoir au final un programme plus rapide.
Comme je vous l'ai dit l'informaticien est feignant il n'est pas nécessaire
de préciser this.<attribut>
ou
this.<méthode>
. S'il n'y a pas d'ambiguité on peut utiliser
directement <attribut>
et <méthode>
. Il y
a ambiguité lorsque un paramètre de la fonction à le même nom qu'un attribut ou
un méthode. S'il y a ambiguité alors la priorité et donné au paramètre et donc
pour accéder à l'attribut ou à la méthode il faut la préfixer de
this.
;
struct Humain{ private: string _nom; size_t _x; size_t _y; public: this( string nom, size_t x = 0, size_t y = 0 ){ _nom = nom; _x = x; _y = y ; }
this( string _nom, size_t _x = 0, size_t _y = 0 ){ _nom = _nom; // ambiguité _x = _x; // ambiguité _y = _y ; // ambiguité }
Dans cet exemple les attributs ne sont jamais modifié car il y a ambiguité
et donc les vraiables sont celles provenant de la fonction. _nom =
_nom;
cette déclaration donne à la variale provenant de la fonction la
valeur donnée en paramêtre à la fonction. Donc en faite ça fait rien du tout
De manière énérale on s'arrangera toujours pour qu'il n'y ait pas d'ambiguité.
Les propriétés
Maintenant que nous devons utiliser une méthodes pour accéder à la valeur de
l'attribut on va «émuler» que l'on utilise un attribut. Et donc faire
foo.x
auy lieu de foo.x()
et faire foo.x =
5
auy lieu de foo.x(5)
. Pour ceci on utiliser les
propriétés. Ce n'est pas compliqué en pratique il suffit de rajouter @property
sur les méthodes protégeant les attributs.
struct Humain{ private: string _nom; size_t _x; size_t _y; void déplace( size_t coordX, size_t coordY ){ x = x + coordX; y = y + coordY; } @property void x( size_t valeur ){ _x = valeur; } @property void y( size_t valeur ){ _y = valeur; } @property void nom( size_t valeur ){ _nom = valeur; } public: this( string cNom, size_t coordX = 0, size_t coordY = 0 ){ nom = cNom; x = coordX; y = coordY; } void avance(){ this.déplace( 1, 0); // avance sur l'axe des x de 1 pas } void recule(){ this.déplace( -1, 0); // recule sur l'axe des x de 1 pas } @property void x( ) const { return _x ; } @property void y( ) const { return _y; } @property void nom( ) const { return _nom; } }
Pour appelé la méthode x
les parenthèses sont pas nécessaires
et pour assigner une nouvelle valeur à l'attribut _x
on peut le
faire de façon transparente avec le symbôle =
.
Humain roger = Humain( "Roger" ); // Roger est en x:0, y:0 writefln( "%s se trouve à la position %u", roger.nom, roger.x );
La méthode toString
Afin de pouvoir représenter une structure de donnée parun chaine de chaine
de carctère, il existe une méythode spécial: toString
. Par
exemple pour avoir un comportement comme cela:
Je suis un Humain, je m'appelle Roger et me trouve en x:5 y:2
On implémente la méthode toString
qui représentera l'état
courant de l'instance Roger.
import std.stdio; import std.format : format; struct Humain{ private: string _nom; size_t _x; size_t _y; public: this( string cNom, size_t coordX = 0, size_t coordY = 0 ){ _nom = cNom; _x = coordX; _y = coordY; } string toString() const{ return "Je suis un Humain, je m'appelle %s et me trouve en x:%d y:%d".format( _nom, _x, _y ); } } void main( ){ Humain roger = Humain( "Roger", 5, 2 ); writeln( roger ); // écrit sur le terminal }
Je suis un Humain, je m'appelle Roger et me trouve en x:5 y:2
La fonction writeln
demande une chaine de caractère, recherche
si la méthode toString
est présente, appel implicitement la
méthode toString
représentant l'état de l'instance
roger
. La déclaration de la fonction toString
se
termine par const car elle ne modifie pas l'état de roger
.
La surcharge d'opérateurs Les templates
Les templates
Les fonctions templates Les structures et les classes templates Les macros
Range
InputRange OutputRange
Interfrace graphique en GTK avec GTKD
Langage D de A à Z by Langage D de A à Z is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 France License.
Based on a work at http://blog.fedora-fr.org/bioinfornatics/post/Language-D-de-A-à-Z.
Note
[1] Si vous n'êtes pas familier avec les pointeurs, vous pourrez en apprendre plus dans La mémoire, une question d'adresse sur le site du zéro