Le langage D partie 2

De Magazine fedora-fr
Aller à : navigation, rechercher

Le langage D - partie 2

Dans le précédent article, nous avons étudié les différentes composantes de bases du langage D telles que les type de bases, comment réaliser des boucles, des fonctions, etc. Toutefois depuis cette article une version majeure du langage est apparu. Cette nouvelle version ce veut mieux adapté au défis de l'avenir, avec notamment une utilisation simple des flux de processus, support du style fonctionnelle et de la metaprogrammation. Liens utiles:

Cette article va principalement traiter de la notion d'objets dans le langage D. Comme pour le précédent article, certains concepts fondamentaux seront rappelés et d'autres non.

Cependant, avant de commencer à traiter des objets, des classes et autre jouets, nous allons continuer d'introduire certains éléments de bases que sont :

* Les conversions de type (cast)
* La lecture des entrées clavier
* La manière dont il faut procéder pour recueillir et traiter des arguments depuis la ligne de commande.

Nous vous souhaitons une très bonne lecture et beaucoup de plaisir dans la suite de votre apprentissage du D.

-- Notions de bases

Les conversions de type (cast)

En langage D, il existe 2 manières d'effectuer une conversion de type :

  • avec le mot clé cast
  • avec le module tango.util.Convert

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 cité dans un chapitre précédent mais est plus sûre.

Les exemples suivants présentes quelques cas pratiques de cast :

import module std.con;
short a = 1;
float b = 0.5;
int c = to!(int)(a); // on utilise le template nommé "to" provenant du module Convert 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 Convert et on spécifie le type double
double f = cast(double)(b);

On l'importation du module std.conv permet l'utilisation du template "to" (le mot clé cast n'a besoin d'aucun module).

Lire les entrées clavier

Pour commente lire des entrées claviers, commençons par un cas pratique.

Le code suivant montre un petit exemple d'application D qui récupère le nom et l'âge d'une personne pour ensuite les réafficher.

import std.stdio;
import std.string;
void main(){
    writeln("Entrer votre nom: ")();
    string nom = stdin.readln;
    writeln("Salut ", nom);
}

Demander le nom et l'âge:

import std.stdio;
import std..conv;
import std.string;

void main() {
    writeln("Entrer votre nom: ");
    string nom = stdin.readln();
    writeln("Entrer votre age: ");
    uint age = to!(uint)(stdin.readln());
    writefln("Salut %s tu as donc %d ans", nom , age);    
}

La récupération des données se trouvant dans le tampon d'entrée clavier les flux standard est réalisée à l'aide de la fonction stdin.readln() (la méthode readln() de la structure stdin).

La ligne uint age = to!(uint)(stdin.readln()); présente une conversion de ce qui a été récupéré dans le tampon d'entrée (saisi pas l'utilisateur) en valeur numérique (int). Dans le cas présent, une chaine de caractère (l'âge) en int.

Maintenant que nous savons récupérer des données depuis l'entrée standard, intéressons nous au passage d'arguments.

Parser les argument de la ligne de commande

En D, le traitement des arguments passer en ligne de commande (mon_application argument1, argument2, etc.) est en général réalisé avec l'aide de fonctionnalités proposées par la libraire phobos.

Les explications données dans cette article diffère un peut des exemples qu'il est possible de trouver sur internet. Cependant, les lecteurs intéressé trouveront sur le site officiel tutoriel d'introduction au traitement des arguments un très bon tutoriel qui introduit les bases du traitement des arguments en D.

L'exemple ci-dessous présente au travers un petit programme comment nous proposons de parser et de gérer des arguments donnés en ligne de commande. Cette exemple permet aussi de poser les base d'une application proposant un début d'aide contextuel.

import std.getopt;
import std.stdio;
import std.string;
import std.path;
import std.c.process;

// Quelques variables global au module (pas taper !)
uint verbosity                 = 0;
const float numVersion = 0.1;
string inputFile              = "";
string outputFile            = "";

// Mode verbeux de l'application
void verboseMode(string[] message) {
    if( verbosity > 0 )
        writetfln("\033[0;31m'%s'\033[0;0m",message);
}

// Imprimer sur la sortie standard  des information à propos de cette application
void about(){
    writetln("monApplication version \033[0;31m'{}'\033[0;0m", numVersion);
    writetln("monApplication est un super programme écrit en D.");
}

// Programme principal
void main (string args) {
    // Imprime sur la sortie standard une aide contextuel
    void usage( ) {
        writetln("\033[0;31mUsage:\033[0;0m appName  [options] );
        writetln("Options:");
        writetln("--input  -i      Fichier à lire" );
        writetln("--output -o    Fichiier de résultat" );
        writetln("--help -h        Écris ce menu d'aide" );
        writetln("--verbose -v  Augmente la verbosité du programme" );
        writetln("--quiet -q      Annule la verbosité du programme" );
        writetln("--version       Affiche la verson du programme" );
        exit(0);
    }
   // Assigne la verbosité au niveau demandé
   void verbose( string option ){
       switch( option ){
           case("v"):
           case("verbose"):
               verbosity += 1;
               break;
           case("quiet"):
               verbosity = 0;
               break;
           default:
               verbosity = 1;
               break;
       }
   }
    // Définit les arguments du programme puis parse la ligne de commande
    getopt(
        args,
        "input|i",     &inputFile,
        "output|o", &outputFile,
        "help|h",     &usage ,
        "version",   &about,
        "verbose",  &verbose ,
        "v",             &verbose,
        "quiet",        &verbose 
    }
    // Affiche un message d'erreur et quitte le programme si l'argument --input n'est pas presente
    else if( inputFile == "" ){
        writeln("\033[0;31mWarning:\033[0;0m option --input absent!");
        usage();
        scope(failure) writeln("Programme arrêté subitement");
    }
    else{
        // Stocke le nom du fichier soumis par l'option --input
        verboseMode("Input file: "~inputFile);

        // Stocke le nom du fichier soumis par l'option  --output
        if( outputFile == "" ) // Si l'option --output est absente, il prend le nom du fichier "input" et remplace l'extention par .out
           outputFile =stripExtension(inputFile) ~ ".out";
        verboseMode("Output file: "~outputFile);         
    }
}


Prenons maintenant le temps de découper ce qui a été réalisé dans cette exemple.

En début de programme, nous définition à l'aide les différents arguments qui peuvent être traité ainsi que leur versions raccourcie et leur description pour l'utilisateur. Avec la fonction getopt , nous parsons les différents arguments passé lors de l'appel de notre application.

Comme par exemple :

"help|h",     &usage ,

Ici l'argument "help" peut aussi être utilisé avec "h" et la fonction usage est apellé si le paramètre est invoqué via la ligne de commande.

La verbosité du programme est par défaut à 1 elle peut être augmenté en ajoutant via la lige de commande une ou plusieurs fois le paramèrtre verbose. La verbosité augmente autant de fois qu'est présent ce paramètre. Le paramètre quiet désactive la verbosité. Ainsi dans le programme on peut sélectionner les messages à imprimer selon le nivau de verbosité.

if( verbosity > 1) ...
if( verbosity > 2) ...
...

Le reste de l'application (commentée) est laissée à titre d'exercice au lecteur de ce document.

Maintenant que les bases du language ont été posée, il est possible de commencer la notion objet dans le langage D.

La notion objet dans le langage D

Le langage D permet de faire de la programmation orientée objet. Pour cela, il faut passer par l'utilisation des classes. Profitons de l'occasion pour faire un petit rappel de la notion de classe et d'objet.

En programmation objet on distingue 2 choses : la classe et l'objet. Par exemple, un humain peut être une classe et Roger un objet. En fait, la classe est la définition, soit dans l'exemple donné ici la définition d'un humain. Un humain est un mammifère avec 2 jambes, 2 bras, un nom et un âge. On dit que Roger est une instance de humain. Donc, un objet et l'instance d'une classe. Une instance est un élément (Thread, mémoire allouée. etc) qui est manipulable de manière effective.

Pour rendre les chose plus claire, nous allons continuer avec notre exemple de l'humain.

Les classes

L'exemple ci dessous présente comment déclarer une classe avec le mot clé "class":

class Humain{
…
}

Par convention le nom d'une classe s'écrit avec la 1ère lettre en majuscule.

Si on reprend l'exemple précédent, les attributs de la classe humain sont :

  • le nombre de jambes
  • le nombre de bras
  • le nom
  • l'âge

Soit :

class Humain {
 private:
     uint _nbJambes;
     uint _nbBras;
     char[] _nom;
     uint _age;
…
}

Vous remarquerez l'utilisation du mot "private" qui signifie que l'on ne peut accéder à tout ce qui suit depuis l'extérieur de l'objet, par opposition à public. Regardez l'exemple qui suit :

class Humain  {
 private:
     uint _nbJambes;
     uint _nbBras;
     char[] _nom;
     uint _age;
 public:
     getNbJambes(){
         return _nbJambes;
     }
…
}

Humain robert = new Humain(); // création d'un objet robert de type Humain
uint nbJambes = robert._nbJambes; // Erreur: ceci ne peut pas fonctionner car _nbJambes est un attribut privé. On ne peut pas accéder à cette valeur.
uint nbJambes = robert.getNbJambes(); // renvoie le nombre de jambes de l'objet robert

Par convention, on préfixe les attributs par un tiret bas (underscore) : "_".

Etre capable de gérer un humain c'est bien, pouvoir interagir avec lui et lui permettre d'agir c'est mieux. Voyant comment lui donner les capacités d'interagir avec nous.

Les méthodes

Les méthodes d'une classe correspondent aux actions. Par exemple, pour notre cas, imaginons que nous deviens connaitre le nombre de jambes de notre humain et qu'il se trouve sur une grille pour se déplacer. Dans notre cas, on peut implémenter deux méthode, la méthode "avancer" et "getNbJambres".

Pour être capable de déplacer notre humain, il faut aussi lui donné le moyen de connaitre sa position. En plus des méthode nous allons donc lui définir deux aributs pour sauvegarder sa position.

class Humain {
 private:
     uint _nbJambes;
     uint _nbBras;
     char[] _nom;
     uint _age;
     int _x;
     int _y;
 public:
     getNbJambes(){
         return _nbJambes;
     }
     avancer(int x, int y){
         _x += x;
         _y += y;
     }
…
}

Humain robert = new Humain(); // création d'un objet robert de type Humain
robert.avancer(2,3);

Dans cet exemple, il y a 2 méthodes :

  1. getNbJambes
  2. avancer

On fait avancer robert de +2 et +3 sur la grille (abstrait).

A la suite de ce cours exemple nous avons vu comment créer de manière simple une classe et d'en instencié sont objet. Passons maintenant à quelques point clés important en programmation objet.

Constructeur et Destructeur

Les constructeurs et les destructeurs sont des méthodes particulières.

Constructeurs

Le constructeur permet de construire un objet. La méthode utilisée s'appelle "this". Si l'on reprend l'exemple précédent :

class Humain {
 private:
     uint _nbJambes;
     uint _nbBras;
     char[] _nom;
     uint _age;
     int _x;
     int _y;
 public:
     getNbJambes() {
         return _nbJambes;
     }
     avancer(int x, int y) {
         _x += x;
         _y += y;
     }
     this(nbJambes, nbBras, nom, age, x, y) {
         _nbJambes = nbJambes;
         _nbBras = nbBras;
         _nom = nom;
         _age = age;
         _x = x;
         _y = y;
     }
…
}

Humain robert = new Humain(2, 2, "robert", 28, 3, 5); // création d'un objet robert de type Humain

Dans l'exemple précédent, on créé un objet robert de type Humain en précisant qu'il a 2 jambes, 2 bras, que son nom est robert, qu'il a 28 ans et se trouve en x:3 y:5.

Destructeur

Le destructeur est appelé par le ramasse-miette (« garbage collector ») pour libérer la mémoire et supprimer l'objet. On peut explicitement demander la destruction de l'objet avec le mot clé delete. Le nom de la méthode destructeur est ~this. Soit:

class Humain {
 private:
     uint _nbJambes;
     uint _nbBras;
     char[] _nom;
     uint _age;
     int _x;
     int _y;
 public:
     getNbJambes() {
         return _nbJambes;
     }
     avancer(int x, int y){
         _x += x;
         _y += y;
     }
     this(nbJambes, nbBras, nom, age, x, y) {
         _nbJambes = nbJambes;
         _nbBras = nbBras;
         _nom = nom;
         _age = age;
         _x = x;
         _y = y;
     }
     ~this(){
     }
…
}

Humain robert = new Humain(2, 2, "robert", 28, 3, 5); // création d'un objet robert de type Humain
delete robert;

Si on ne supprime pas l'objet robert explicitement avec le mot clé delete , le ramasse-miette (garbage collector) le supprimera tout seul quand l'objet ne sera plus utilisé. On peut donc, selon nos choix, gérer (où l'on a) la mémoire.

Vous avez remarqué ? Le destructeur est vide. En effet aucune variable n'a été créée par le mot clé new . Par conséquent, le ramasse-miette pourra faire son travail quand il sera appelé.

L'héritage

L'héritage consiste à spécialiser une classe : par exemple, un magicien est un humain. Afin d'éviter de réécrire le code de la classe humain, on réutilise le code et on effectue un héritage de la manière suivante :

class Magicien : Humain {
private:
    uint _mana;
public:
    this(uint mana,char[] nom, uint age, x, y) {
        super(2, 2, nom, age, x, y); // construction de l'humain
        _mana = mana;
    }
}
Magicien gandalf = new(200, "gandalf", 7000, 156, -54);

Vous remarquerez l'utilisation du mot-clé super . Celui-ci permet d'appeler le constructeur de la classe mère (c'est à dire ici le constructeur Humain). Pour construire un magicien, on doit lui donner une quantité de mana (pouvoirs magiques), un nom, un âge et sa position dans l'espace.

Les Classes Abstraites

Une classe abstraite est une classe que l'on ne peut pas instancier (c'est à dire dont on ne peut créer un objet). Par exemple, on peut décider que la classe mammifère est abstraite. En effet, un mammifère en tant que tel n'existe pas (par exemple, l'Homme est un mammifère et n'existe qu'en tant que tel).

La mise en place d'une classe abstraite se fait de la manière suivante :

abstract class Mammifere {
    private:
        uint _temperatureCorporelle;
        uint _taille;
        char[] _espece;
    public:
        this(uint temperatureCorporelle, uint taille, char[] espece) {
            _temperatureCorporelle = temperatureCorporelle;
            _taille = taille;
            _espece = espece;
        }
        getTemperatureCorporelle() {
            return _temperatureCorporelle;
        }
        getTaille() {
            return _taille;
        }
        getEspece() {
            return _espece;
        }
}

Ainsi, on ne pourra pas écrire :

Mammifere homme = new Mammifere(37, 170, "Homo sapiens");

Mais on devra créer une classe Homme qui héritera de la classe abstraite Mammifère.

Les interfaces

Le langage D ne supporte pas l'héritage multiple. Pour contourner cela, on utilise les interfaces. Une interface liste des fonctions que doit obligatoirement utiliser une classe. Reprenons la classe Humain et demandons à celle-ci d'implémenter les méthodes suivantes:

  • seReposer
  • esquiver
  • marcher
  • courir

Vous en conviendrez, ces méthodes peuvent être utilisées par d'autres classes que les Humains mais par l'utilisation d'une interface, on standardise le nom de ces méthodes et on évitera ainsi de se retrouver avec du code comme :

  • repos se_reposer regenere
  • esquiver evite
  • marcher marche
  • courir seDepecher

On déclare une interface comme cela :

interface actions{
    void seReposer();
    void esquiver();
    void marcher();
    void courir();
}

Et on l'implémente à une classe ainsi :

class Humain : actions, Mammifere  {
 private:
     uint _nbJambes;
     uint _nbBras;
     char[] _nom;
     uint _age;
     int _x;
     int _y;
 public:
     getNbJambes()  {
         return _nbJambes;
     }
     avancer(int x, int y) {
         _x += x;
         _y += y;
     }
     this(nbJambes, nbBras, nom, age, x, y) {
         super(37, 170, "Homo sapiens");
         _nbJambes = nbJambes;
         _nbBras = nbBras;
         _nom = nom;
         _age = age;
         _x = x;
         _y = y;
     }
    void seReposer(){
    …
    }
    void esquiver(){
    …
    }
    void marcher(){
    …
    }
    void courir(){
    …
    }
…
}

Je vous invite également à lire le tutoriel traitant de la notion d'interface sur le Site du Zéro (Bien que le tutoriel en question soit rédigé pour Java, la syntaxe est proche et l'utilisation des interfaces est identique).

Conclusions

Nous arrivons ici au terme de cette introduction à la programmation objet en D. Ne vous inquiétez pas , d'autres articles irons plus loin dans certains de ses concepts (Thread, programmation par contrat, etc.).

Le prochain article parlera du traitement des erreurs, de la généricité, de la documentation et de quelques autre mots clé important dans le language D.

En attendent. nous vous souhaitons une très bonne pratique du D et nous vous encourageons à essayer ce langage.

Auteur : SébastienPasche, JonathanMercier