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 ?

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:

  1. Rapidité d'éxécution
  2. Productivité
  3. 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

geany config ldc compilateur

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:

  1. import std.stdio déclarant que nous allons utiliser des fonctionnalités définis dans le module stdio du package std . ( la fonction writeln provient de ce module )
  2. 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

  1. les chiffres entiers (sans virgule)
  2. les chiffres décimaux (également apellé flottant)

Parmis les chiffres entiers on différencie également:

  1. les entiers pouvant être uniquement positif ( ex: uint )
  2. 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 :D .

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 que y :?:

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 1break;
    case 2: // cas où la valeur est 2break;
    case 3: // cas où la valeur est 3break;
    case 4: // cas où la valeur est 4break;
    default: // cas où la valeur est différente}

Pourquoi retrouve-t-on break à chaque fois :?: Admettons, pour imager, 2 choses :

  1. que la variable a vaut 3
  2. 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 1case 2: // cas où la valeur est 2case 3: // cas où la valeur est 3break
    case 4: // cas où la valeur est 4break
    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:

  • wchar
  • wchar
  • dchar
  • dchar

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

dot_product

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 des tableaux dynamiques
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 des tableaux statiques
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 des tableaux associatifs
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
.val­ues Retourne un tableau dynamique, dans lequel les éléments du tableau sont les valeurs du tableau associatif
.re­hash 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
.by­Value() Retourne un delegate pour l'utiliser dans une boucle foreach, itérant sur les valeurs du tableau associatifs
.get(Key key, lazy Value de­fault­Value) 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
  1. On assigne le résultat de la fonction double( 4 ) à la variable i
    1. la fonction double reçoit la valeur 4 qui est attribué à la variable x au sein de la fonction
    2. la fonction returne le résultat de l'opération 4 * 2
  2. On écrit sur la sortie standard le double du chiffre 4 est 8
  3. On assigne la valeur de 6 à la variable x
  4. On écrit sur la sortie standard le double du chiffre x est double( x )
    1. la valeur de x est évalué ( 6 )
    2. la fonction double( 6 ) est évalué
      1. la fonction double reçoit la valeur 6 qui est attribué à la variable x au sein de la fonction
      2. la fonction returne le résultat de l'opération 6 * 2
  5. 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 :-D .

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. :-D 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 signifie line new soit nouvelle ligne, les fonctions writeln et writefln rajoute une nouvelle ligne à la fin de la phrase.
  • le suffix f signifie format 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 mot Coucou sera suivis d'une valeur de type string car la lettre utilisée est s . 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 lettre d 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 pointeur NULL 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 disant if (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 :-D dans un fichier nommer monFichier.txt . Lors du premier passage dans la fonction écrire .

  1. l'argument phrase vaut "Bonjour"
  2. on entre dans le bloc de capture des exceptions ( try )
  3. Ouverture du fichier en mode ajout
  4. phrase n'est pas vide
  5. écrire dans le fichier la phrase Bonjour
  6. fermeture du fichier

Lors du second passage dans la fonction écrire .

  1. l'argument phrase vaut "" ( vide )
  2. on entre dans le bloc de capture des exceptions ( try )
  3. Ouverture du fichier en mode ajout
  4. phrase est vide
  5. On lève une exception ayant le message "est vide"
  6. L'exception est capturé par le bloc catch
  7. L'exception capturé est gérée comme un objet de type Exception et la variable s'appel e
  8. 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 .

  1. l'argument phrase vaut "" ( vide )
  2. on entre dans le bloc de capture des exceptions ( try )
  3. Ouverture du fichier en mode ajout
  4. phrase est pas vide
  5. On lève une exception ayant le message "est vide"
  6. L'exception est capturé par le bloc catch
  7. L'exception capturé est gérée comme un objet de type Exception et la variable s'appel e
  8. le message de l'exception étant "est vide" on relance la fonction écrire avec un argument ayant pour valeur un espace
  9. 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
Modificateur d'accessibilité
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 :D 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 :D .

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

Creative Commons License
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

Vus : 984
Publié par MERCIER Jonathan : 20