Accueil > Le langage Ruby > Ruby - Documentation de référence

Ruby - Documentation de référence

Le langage Ruby - Sommaire

Guide utilisateur du langage ruby en VO / Pour en savoir plus : Ruby-lang.org
Pour télécharger Ruby :

Introduction

Ruby est un langage orienté objet simple. Il peut paraître un peu déroutant au début, mais il est conçu pour être facilement lu et écrit. Ce guide de l’utilisateur vous aidera à démarrer en Ruby, et vous fournira des éléments que vous ne trouverez pas dans le manuel de référence.

Table des matières

Historique de ce document

  • Version originale japonaise : matz. (l’auteur du langage ruby).
  • Première traduction anglaise par GOTO Kentaro & Julian Fondren.
  • Traductions suivantes et révisions par Mark Slagell.  La présente version est datée 16-02-2001. Traduction française 04-2001 : Alain Feler.
  • Akinori Musha, Laurent Julliard, Manpreet Singh et Robert Gustavsson ont aidé à repérer les erreurs et à améliorer le formatage et la présentation.
    Merci à eux !

Quid Ruby ?

Ruby est “un langage de script interprété pour une programmation orientée-objet rapide et facile”; ça veut dire quoi ?

Langage de script interprété :

  • capable de faire des appels système directement
  • avec de puissantes fonctions de traitement des chaînes de caractères et des expressions régulières
  • forte interactivité du développement

Rapide et facile :

  • pas de déclaration des variables
  • les variables ne sont pas typées
  • la syntaxe est simple et cohérente
  • gestion automatique de la mémoire

Programmation orientée-objet :

  • tout est un objet
  • classes, héritage, méthodes, etc.
  • méthodes singleton
  • mixin des modules
  • itérateurs et enclosures

et aussi :

  • entiers à précision illimitée
  • gestion des exceptions
  • chargement dynamique
  • threads

Si certains des termes ci-dessus ne vous sont pas familiers, ne vous inquiétez pas, et continuez de lire.  Les arcanes de ruby sont aisées et vite acquises.

Pour commencer

Pour commencer, vérifiez que ruby est installé.  Depuis la ligne de commande (notée ici “%“, - ne tapez pas le %), tapez:

 % ruby -v

(-v demande à l’interpréteur d’afficher la version de ruby), puis appuyez sur la touche Entrée.  Si ruby est installé, vous verrez un message du genre:

% ruby -v
ruby 1.6.1 (2000-09-27) [i586-linux]

Si ruby n’est pas installé, demandez à votre administrateur de le faire, ou faites-le vous même, puisque ruby est un logiciel gratuit sans restriction quand à son installation ou son usage.

Maintenant, jouons un peu avec ruby.  Vous pouvez passez un programme ruby directement depuis la ligne de commande, en utilisant l’option -e :

% ruby -e 'print "hello world\n"'
hello world

Plus classiquement, un programme ruby peut être stocké dans un fichier.

% cat > test.rb
print "hello world\n"
^D
% cat test.rb
print "hello world\n"
% ruby test.rb
hello world

^D est control-D.  Ce qui précède vaut sous UNIX.  Sous DOS, essayez ceci :

C:ruby> copy con: test.rb
print "hello world\n"
^Z
C:ruby> type test.rb
print "hello world\n"
C:ruby> ruby test.rb
hello world

Pour écrire des programmes plus consistants, vous préférerez sûrement utiliser un éditeur de texte !

On peut faire des choses étonnamment complexes et utiles avec des programmes miniatures tenant sur la ligne de commande. Par exemple, celui-ci remplace la chaîne foo par bar dans tous les sources C et fichiers header du répertoire courant, et sauve les fichiers originaux avec l’extension “.bak”:

% ruby -i.bak -pe 'sub "foo", "bar"' *.[ch]

Ce programme marche comme la commande UNIX cat (mais moins vite…):

% ruby -pe 0 file

Des exemples simples

Ecrivons une fonction pour calculer des factorielles.  La définition mathématique de la factorielle de n est :

n! = 1                (n==0)
   = n * (n-1)!       (Sinon)

En ruby cela peut s’écrire :

def fact(n)
  if n == 0
    1
  else
    n * fact(n-1)
  end
end

Remarquez les deux end.  Ruby a été qualifié de langage de la famille d’Algol à cause de ça.  (En fait, la syntaxe de ruby imite plutôt celle du langage Eiffel.) Vous remarquerez aussi l’absence d’une instruction return . Elle est superflue parce qu’en ruby, une fonction retourne la dernière chose qu’elle a évaluée. On peut utiliser un return ici, mais ça n’est pas nécessaire.

Essayons notre fonction factorielle.  Il suffit d’ajouter une ligne de code pour avoir un programme utilisable :

Programme donnant la factorielle d'un nombre
# Sauver ce source en fact.rb

def fact(n)
  if n == 0
    1
  else
    n * fact(n-1)
  end
end

print fact(ARGV[0].to_i), "\n"

Ici, ARGV est un tableau qui contient les arguments de la ligne de commande, et to_i convertit une chaîne de caractères en un entier.

% ruby fact.rb 1
1
% ruby fact.rb 5
120

Est-ce que ça marche si on passe 40 en argument ? Cela devrait donner un overflow…

% ruby fact.rb 40
815915283247897734345611269596115894272000000000

Ca marche.  En fait, ruby peut affronter n’importe quel entier, dans les limites de la mémoire. Par exemple 400! Peut être calculé :

% ruby fact.rb 400
64034522846623895262347970319503005850702583026002959458684
44594280239716918683143627847864746326467629435057503585681
08482981628835174352289619886468029979373416541508381624264
61942352307046244325015114448670890662773914918117331955996
44070954967134529047702032243491121079759328079510154537266
72516278778900093497637657103263503315339653498683868313393
52024373788157786791506311858702618270169819740062983025308
59129834616227230455833952075961150530223608681043329725519
48526744322324386699484224042325998055516106359423769613992
31917134063858996537970147827206606320217379472010321356624
61380907794230459736069956759583609615871512991382228657857
95493616176544804532220078258184008484364155912294542753848
03558374518022675900061399560145595206127211192918105032491
00800000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000

On ne peut pas affirmer au premier coup d’oeil que c’est bon, mais ça devrait l’être :-)

La boucle de saisie/évaluation

Quand on appelle ruby sans argument, il lit les commandes depuis l’entrée standard, et les exécute à la fin de la saisie (ctrl-D sous UNIX, ctrl-Z sous DOS):

% ruby
print "hello world\n"
print "good-bye world\n"
^D
hello world
good-bye world

Ruby nous vient aussi avec un programme appelé eval.rb qui permet de saisir du code au clavier en boucle, en montrant le résultat au fur et à mesure. Ce programme sera utilisé tout au long de ce tutorial.

Si vous avez un terminal ANSI (ce qui est sûrement vrai sous UNIX; sous DOS, il faut avoir chargé ANSI.SYS ou ANSI.COM), vous devriez utiliser cet eval.rb amélioré qui offre indentation, messages d’avertissement, et coloration syntaxique.  Sinon, regardez dans le répertoire sample de la distribution ruby où se trouve la version non-ANSI qui marche sur n’importe quel type de terminal.  Voici une petite session en eval.rb :

% ruby eval.rb
ruby> print "Hello, world.\n"
Hello, world.
   nil
ruby> exit

hello world est produit par print.  La ligne suivante, en l’occurrence nil, se rapporte à ce qui a été évalué en dernier, ruby ne distinguant pas entre instructions et expressions, évaluer un bout de programme signifiant à peu près la même chose que l’exécuter.  Ici, nil indique que print ne rend pas une valeur ayant un sens.  Noter que l’on quitte cette boucle d’interprétation en tapant exit, quoique ^D marche aussi.

Tout au long de ce guide, “ruby>” correspond à l’invite de saisie de notre petit programme eval.rb.

Les chaînes de caractères

Ruby traite les chaînes de caractères aussi bien que les nombres.
Une chaîne peut être entre guillemets (”…”) ou entre apostrophes
(’…’).

ruby> "abc"
   "abc"
ruby> 'abc'
   "abc"

Guillemets et apostrophes ont des effets différents dans certains cas.  Une chaîne entre guillemets permet d’utiliser des séquences d’échappement en mettant un anti-slash devant les caractères voulus, et l’évaluation d’expressions incluses dans la chaîne et entourées de #{}.  Une chaîne entre apostrophes ne permet pas tout ça et ne dit rien que ce qu’elle montre. Exemples :

ruby> print "anbnc","\n"
a
b
c
   nil
ruby> print 'anbnc',"\n"
anbnc
   nil
ruby> "n"
   "n"
ruby> 'n'
   "n"
ruby> "01"
   "01"
ruby> '01'
   "01"
ruby> "abcd #{5*3} efg"
   "abcd 15 efg"
ruby> var = " abc "
   " abc "
ruby> "1234#{var}5678"
   "1234 abc 5678"

Le traitement des chaînes par Ruby est plus intelligent et plus intuitif que celui de C. Par exemple, on peut concaténer des chaînes avec + et répéter une chaîne n fois avec *:

ruby> "foo" + "bar"
   "foobar"
ruby> "foo" * 2
   "foofoo"

Concaténer des chaînes est beaucoup plus pénible en C parce qu’il faut gérer explicitement l’allocation mémoire correspondante :

char *s = malloc(strlen(s1)+strlen(s2)+1);
strcpy(s, s1);
strcat(s, s2);
/* ... */
free(s);

Au contraire, avec ruby, on n’a pas à se soucier de la place occupée par une chaîne. On ne s’occupe pas du tout de la gestion de la mémoire.

Voici des choses qu’on peut faire avec les chaînes.

Concaténation :

ruby> word = "fo" + "o"
   "foo"

Répétition :

ruby> word = word * 2
   "foofoo"

Extraction de caractères (notez que les caractères sont des
entiers pour ruby) :

ruby> word[0]
   102            # 102 est le code ASCII pour `f' 
ruby> word[-1]
   111            # 111 est le code ASCII pour `o'

(Les indices positifs indiquent des positions à partir du début de la chaîne, les indices négatifs à partir de la fin).

Extraction de sous-chaînes :

ruby> herb = "parsley"
   "parsley"
ruby> herb[0,1]
   "p"
ruby> herb[-2,2]
   "ey"
ruby> herb[0..3]
   "pars"
ruby> herb[-5..-2]
   "rsle"

Test d’égalité :

ruby> "foo" == "foo"
   true
ruby> "foo" == "bar"
   false

Note : En ruby 1.0, les résultats précédents seraient en majuscules, par exemple TRUE.

Maintenant, mettons ces éléments au travail.  Voici un jeu “Quel mot est-ce ?” - mais le mot jeu est peut-être un peu excessif pour ce qui suit ;-)

# sauver ceci en guess.rb
words = ['foobar', 'baz', 'quux']
secret = words[rand(3)]

print "Devine? "
while guess = STDIN.gets
  guess.chop!
  if guess == secret
    print "Tu gagnes!\n"
    break
  else
    print "Dommage, tu perds.\n"
  end
  print "Devine? "
end
print "Le mot était ", secret, "\n"

Pour l’instant ne vous souciez pas trop du détail du code. Voici ce que produit une exécution.

% ruby guess.rb
Devine? foobar
Dommage, tu perds.
Devine? quux
Dommage, tu perds.
Devine? ^D
Le mot était baz.

(J’aurais pu faire mieux, vu la probabilité de 1/3…)

Expressions régulières

Nous allons maintenant construire un programme plus intéressant.  Cette fois, nous testerons si une chaîne de caractères correspond à une certaine description, qu’on appelle une forme (a pattern).

Certains caractères et combinaisons de caractères ont une signification spéciale dans les formes, notamment :

[] spécification d’intervalle (par ex: [a-z]
indique une lettre dans l’intervalle a à z)
w lettre ou chiffre; équivaut à [0-9A-Za-z]
W ni lettre ni chiffre
s caractère espace; équivaut à[ tnrf]
S caractère non espace
d chiffre; équivaut à [0-9]
D non-chiffre
b backspace (0×08) (seulement dans un intervalle)
b limite de mot (sauf dans un intervalle)
B limite autre que de mot
* zero, 1 ou n occurrences de ce qui précède
+ 1 ou n occurrences de ce qui précède
{m,n} au moins m et au plus n occurrences de ce qui précède
? Au plus une occurrence de ce qui précède; équivaut
à {0,1}
| alternative: soit ce qui précède soit ce qui suit
() groupe

Le nom générique pour les formes qui utilisent ce vocabulaire bizarre est expressions régulières. En ruby, comme en Perl, elles sont généralement entourées par des slashs (/) plutôt que par des guillemets. Si vous n’avez jamais utilisé les expressions régulières auparavant, elles vous paraîtront sûrement tout sauf régulières (ndt: jeu de mot sur regular = ordinaire), mais vous avez tout intérêt à passer quelques temps à les apprivoiser. Elles ont un pouvoir d’expression concise qui vous évitera des maux de têtes et beaucoup de lignes de code, que vous ayez à faire de la reconnaissance de forme sur chaînes de caractères, des recherches de chaînes, et toutes autres manipulations de chaînes.

Par exemple, supposons que nous voulions tester si une chaîne correspond à la description suivante: “Commence par un f minuscule, suivi immédiatement par exactement une majuscule, suivie par n’importe quoi sauf des minuscules.”  Si vous êtes un programmeur C expérimenté, vous avez sûrement déjà écrit mentalement une douzaine de lignes, pas vrai ?  Reconnaissez-le, vous n’avez pas pu vous en empêcher.  Mais en ruby vous n’avez qu’à tester votre chaîne contre l’expression régulière suivante: /^f[A-Z][^a-z]*$/.

Et si c’était “Contient un nombre hexadecimal entouré par des crochets” ? Pas de problème.

ruby> def chab (s)   # "contient un hexa entre crochets"
    |    (s =~ /<0[Xx][dA-Fa-f]+>/) != nil
    | end
  nil
ruby> chab "Pas ça."
  false
ruby> chab "Peut-être ça? {0x35}"    # parenthèses, pas crochets
  false
ruby> chab "Ou ça? <0x38z7e>"    # hexa incorrect
  false
ruby> chab "Celui-ci, oui: <0xfc0004>."
  true

Quoique les expressions régulières soient déroutantes au début, vous tirerez vite des satisfactions de pouvoir vous exprimer avec une telle concision.

Voici un petit programme pour vous aider à pratiquer avec les expressions régulières.  Sauvez-le en regx.rb et lancez-le en tapant “ruby regx.rb”.

# Requiert un terminal ANSI !

st = "33[7m"
en = "33[m"

while TRUE
  print "str> "
  STDOUT.flush
  str = gets
  break if not str
  str.chop!
  print "pat> "
  STDOUT.flush
  re = gets
  break if not re
  re.chop!
  str.gsub! re, "#{st}&#{en}"
  print str, "\n"
end
print "\n"

Ce programme demande deux entrées, une pour une chaîne de caractères, et une pour l'expression régulière. La chaîne est confrontée à l'expression, puis affichée avec les parties qui répondent à l'expression en vidéo inverse. Ne vous occupez pas des détails du programme maintenant, nous l'analyserons bientôt.

str> foobar
pat> ^fo+
foobar
~~~

Ce que vous voyez en rouge ci-dessus apparaîtra en video inverse à l'exécution. Le "~~~" est pour ceux qui utilisent des browsers internet en mode caractère.

Essayons d'autres saisies.

str> abc012dbcd555
pat> d
abc012dbcd555
   ~~~    ~~~

Si ceci vous étonne, regardez à nouveau le tableau ci-dessus: d n'a rien à voir avec le caractère d, mais correspond à un chiffre quelconque.

Et s'il y a plusieurs façon de correspondre à la forme indiquée?

str> foozboozer
pat> f.*z
foozboozer
~~~~~~~~

foozbooz est retenu au lieu de seulement fooz, parce qu'une expression régulière renvoie la sous-chaîne la plus longue possible.

Voici une forme pour isoler une heure avec des : comme séparateurs.

str> Wed Feb  7 08:58:04 JST 1996
pat> [0-9]+:[0-9]+(:[0-9]+)?
Wed Feb  7 08:58:04 JST 1996
           ~~~~~~~~

=~” est un opérateur de correspondance qui s’applique aux expressions régulières. Il retourne la position du début de la correspondance dans la chaîne, ou nil si la forme n’a pas été trouvée.

ruby> "abcdef" =~ /d/
   3
ruby> "aaaaaa" =~ /d/
   nil

Les tableaux

Vous pouvez créer un tableau en énumérant divers éléments entre des crochets ([ ]), en les séparant par des virgules. Les tableaux de ruby peuvent contenir des objets de divers types.

ruby> ary = [1, 2, "3"]
   [1, 2, "3"]

Les tableaux peuvent être concaténés ou répétés
exactement comme des chaînes de caractères.

ruby> ary + ["foo", "bar"]
   [1, 2, "3", "foo", "bar"]
ruby> ary * 2
   [1, 2, "3", 1, 2, "3"]

On peut utiliser des indices pour se référer aux parties d’un tableau.

ruby> ary[0]
   1
ruby> ary[0,2]
   [1, 2]
ruby> ary[0..1]
   [1, 2]
ruby> ary[-2]
   2
ruby> ary[-2,2]
   [2, "3"]
ruby> ary[-2..-1]
   [2, "3"]

(Les indices négatifs indiquent des déplacements depuis la fin du tableau.)

Les tableaux peuvent être convertis depuis et vers des chaînes de caractères, en utilisant split et join respectivement:

ruby> str = ary.join(":")
   "1:2:3"
ruby> str.split(":")
   ["1", "2", "3"]

Hashes (tableau associatif)

Un tableau associatif a des éléments auxquels on accède non plus par des indices numériques croissant séquentiellement, mais par des clés qui peuvent avoir n’importe quelle valeur. Ce type de tableau est parfois appelé hash ou dictionnaire; dans le monde ruby, on préfère le mot hash.  Un hash peut être construit par des paires de valeurs entre accolades ({}).  On utilise une clé pour trouver quelque chose dans un hash, de la même façon qu’un indice pour trouver quelque chose dans un tableau.

ruby> h = {1 => 2, "2" => "4"}
   {1=>2, "2"=>"4"}
ruby> h[1]
   2
ruby> h["2"]
   "4"
ruby> h[5]
   nil
ruby> h[5] = 10     # ajoute une valeur
   10
ruby> h
   {5=>10, 1=>2, "2"=>"4"}
ruby> h[1] = nil    # supprime une valeur
   nil
ruby> h[1]
   nil
ruby> h
   {5=>10, "2"=>"4"}

Retour sur les exemples

Maintenant, nous allons revoir le code de certains de nos programmes exemples. Pour nous y référer plus commodément, nous numérotons les lignes.

Factorielles

Ce qui suit apparaissait dans le chapitre : Des exemples simples.

01  def fact(n)
02    if n == 0
03      1
04    else
05      n * fact(n-1)
06    end
07  end
08  print fact(ARGV[0].to_i), "\n"

Parce que c’est la première fois, nous allons examiner chaque ligne.

01  def fact(n)

Dans la première ligne, def est une instruction pour définir une fonction (ou, plus précisément, une méthode; mais nous parlerons plus en détail des méthodes plus loin).  Ici, elle spécifie que la fonction fact reçoit un unique argument, nommé n.

 02    if n == 0

Ce if (si) teste une condition. Quand la condition est remplie, la prochaine ligne est évaluée, sinon, c’est ce qui suit le else (sinon) qui est évalué.

 03      1

La valeur de if est 1 si la condition est remplie.

 04    else

Si la condition n’est pas vérifiée, le code depuis else jusqu’à end est évalué.

 05      n * fact(n-1)

Si la condition n’est pas satisfaite, la valeur de if est n fois fact(n-1).

 06    end

Ce premier end ferme l’nstruction if.

 07  end

Le second end ferme l’instruction def.

 08  print fact(ARGV[0].to_i), "\n"

Ceci invoque notre fonction fact() en utilisant une valeur fournie par la ligne de commande, et imprime le résultat.

ARGV est un tableau qui contient les arguments de la ligne de commande. Les éléments de ARGV sont des chaînes de caractères, aussi devrons-nous les convertir en nombres entiers par to_i.  Ruby ne convertit pas automatiquement les chaînes en entiers comme Perl le fait.

Hmmm… Qu’arriverait-il si nous fournissions à ce programme un nombre négatif?  Voyez-vous le problème?  Sauriez-vous le résoudre?

Strings

Maintenant examinons le programme de devinette du chapitre sur les Chaînes de caractères.

01  words = ['foobar', 'baz', 'quux']
02  secret = words[rand(3)]
03
04  print "guess? "
05  while guess = STDIN.gets
06    guess.chop!
07    if guess == secret
08      print "you win\n"
09      break
10    else
11      print "you lose.\n"
12    end
13    print "guess? "
14  end
15  print "the word is ", secret, "\n"

Dans ce programme, une nouvelle structure de contrôle, while (tant que), est présentée.  Le code entre while et son end sera exécuté répétitivement tant qu’une certaine condition restera vraie.

rand(3) en ligne 2 retourne un nombre aléatoire dans la tranche de valeur 0 à 2.  Ce nombre aléatoire est utilisé pour extraire un des éléments du tableau words.

En ligne 5 nous lisons une ligne depuis l’entrée standard par la méthode STDIN.gets.  Si EOF (end of file - fin de fichier) survient pendant qu’on essaye de recueillir la ligne, gets rend nil (rien).  Donc le code associé à ce while se répétera jusqu’à ce qu’il rencontre un ^D (ou ^Z sous DOS), qui signifie la fin de l’entrée.

guess.chop! en ligne 6 ôte le dernier caractère de guess; qui dans ce cas sera toujours un caractère newline (nouvelle ligne).

En ligne 15 on imprime le mot secret.  Nous l’avons écrit en une instruction qui est un print avec trois arguments (qui sont imprimés l’un après l’autre), mais nous aurions aussi bien pu le faire avec un unique argument, en écrivant secret sous la forme #{secret} pour rendre manifeste que c’est une variable à évaluer, et non un littéral à imprimer tel quel:

 print "the word is #{secret}\n"

Expressions régulières

Enfin, examinons le programme du chapitre sur les Expressions régulières.

01  st = "33[7m"
02  en = "33[m"
03
04  while TRUE
05    print "str> "
06    STDOUT.flush
07    str = gets
08    break if not str
09    str.chop!
10    print "pat> "
11    STDOUT.flush
12    re = gets
13    break if not re
14    re.chop!
15    str.gsub! re, "#{st}&#{en}"
16    print str, "\n"
17  end
18  print "\n"

En ligne 4, la condition pour le while est forcée à true (vrai), ce qui forme ce qui pourrait être une boucle infinie. Mais nous mettons des instructions break (rupture) en _ème et 13ème lignes pour sortir de la boucle.  Ces deux breaks sont aussi des exemples de if modifiers.  Un "if modifier" exécute sa partie gauche si et seulement si la condition spécifiée est satisfaite.

Il y plus à dire sur le chop! (tronque) ( lignes 9 et 14).  En ruby, on attache conventionnellement un '!' ou un '?' à la fin de certains noms de méthodes.  Le point d'exclamation (!, qu'on prononce parfois "bang!") indique qu'il y a là quelque chose de potentiellement destructeur, c'est-à-dire quelque chose qui peut changer la valeur de ce qu'il touche. chop! modifie directement la chaîne concernée, tandis que chop sans point d'exclamation travaille sur une copie. Voici une illustration de la différence :

ruby> s1 = "forth"
  "forth"
ruby> s1.chop!       # Ceci change s1.
  "fort"
ruby> s2 = s1.chop   # Ceci met une copie modifiée de s1 dans s2,
  "for"
ruby> s1             # ... sans changer s1.
  "fort"

Vous rencontrerez plus loin des noms de méthodes se terminant par un point d'interrogation (?, quelquefois prononcé "huh?" - ndt "hein?"); ceci indique une méthode de type "prédicat", c'est-à-dire qui retourne soit true (vrai) soit false (faux).

La ligne 15 mérite toute votre attention. D'abord notez que gsub! est aussi une méthode destructive. Elle change str en y remplaçant tout ce qui correspond à la forme re (sub signifie substitue, et le g du début siginfie global, c'est-à-dire, remplace toutes les parties de la chaîne qui correspondent, pas juste la première).  Tout ça est très bien, mais on remplace les parties trouvées par quoi ? st et en ont été définies en lignes 1-2 comme les séquences ANSI qui mettent la couleur du texte en inverse et normal, respectivement.  En ligne 15 elles sont entourées de #{} pour forcer leur interprétation (et que ce ne soit pas simplement le nom des variables qui soit imprimé).  Entre elles nous voyons "&".  Ceci est un peu tortueux. Du fait que la chaîne de remplacement est entre guillemets, la paire d'antislashs sera interprétée comme un antislash simple; que gsub! verra comme "&", qui se trouve être un code spécial qui désigne "ce qui a été trouvé par application de la forme précédente". Donc la nouvelle chaîne imprimée sera identique à la chaîne initiale, sauf que les parties trouvées par la forme (l'expression régulière) seront en inverse vidéo.

Structures de contrôles

Ce chapitre poursuit l'exploration des structures de contrôle en ruby.

case

On utilise l'instruction case pour tester une suite de conditions.  Elle est en gros semblable au switch de C et Java mais beaucoup plus puissante, comme nous allons le voir.

ruby> i=8
ruby> case i
    | when 1, 2..5
    |   print "1..5\n"
    | when 6..10
    |   print "6..10\n"
    | end
6..10
   nil

2..5 est une expression qui désigne la tranche de valeurs (range) entre 2 et 5, bornes incluses. L'expression suivante teste si la valeur de i est dans cet intervalle:

 (2..5) === i

case utilise en interne l'opérateur relationnel === pour tester plusieurs conditions en même temps.  En restant dans l'orientation objet de ruby, === est interprété selon la nature de l'objet qui apparaît dans le when.  Par exemple, le code suivant teste l'égalité de chaînes de caractères dans le premier when, et la réussite d'une expression régulière dans le second.

ruby> case 'abcdef'
    | when 'aaa', 'bbb'
    |   print "aaa or bbb\n"
    | when /def/
    |   print "includes /def/\n"
    | end
includes /def/
   nil

while

Ruby fournit des moyens commodes de construire des boucles, encore que vous allez voir au prochain chapitre qu'en apprenant à utiliser des itérateurs vous n'aurez plus si souvent à écrire des boucles explicites.

Un while est un if répété. Nous l'avons utilisé dans notre programme de devinette et dans le programme avec les expressions régulières (voir le chapitre sur ce sujet) ; où il prenait la forme while condition ... end entourant un bloc de code à répéter tant que condition était vrai.  Mais while et if peuvent aussi bien s'appliquer à des instructions individuelles:

ruby> i = 0
  0
ruby> print "C'est un zéro\n" if i==0
C'est un zéro.
  nil
ruby> print "C'est un négatif\n" if i<0
  nil
ruby> print "#{i+=1}\n" while i<3
1
2
3
   nil

Il arrive qu'on veuille tester une condition négative.  Un unless (sauf si) est un if négatif, et un until (jusqu'à ce que) est un while (tant que)négatif.  Nous vous laissons le soin de les essayer.

Il y a quatre moyens de rompre une boucle.  Premièrement, break fait, comme en C, quitter la boucle complètement.  Deuxièmement, next saute au début de la prochaine itération de la boucle (cf le continue du C).  Troisièmement, ruby a le redo, qui répète l'itération courante.  Ce qui suit est du code C qui illustre break, next, et redo:

while (condition) {
 label_redo:
   goto label_next        /* next */
   goto label_break       /* break */
   goto label_redo        /* redo */
   ;
   ;
 label_next:
}
label_break:
;

La quatrième façon de rompre une boucle est return.  Une évaluation return fait sortir non seulement de la boucle mais aussi de la méthode qui contient la boucle.  Si un argument est fourni, il sera retourné à l'appelant de la méthode, sinon c'est un nil qui sera rendu.

for

Les programmeurs C doivent se demander maintenant comment on fait une boucle "for".  Le for de ruby est un peu plus intéressant que vous ne le croyez peut-être.  La boucle ci-dessous s'exécute une fois pour chaque élément de la collection:

for elt in collection
  ...
end

La collection peut être un intervalle de valeurs (c'est à cela que pensent la plupart des gens en parlant de boucles for):

ruby> for num in (4..6)
    |    print num,"\n"
    | end
4
5
6
   4..6

Elle peut aussi être n'importe quelle autre sorte de collection, comme un tableau:

ruby> for elt in [100,-9.6,"pickle"]
    |    print "#{elt}t(#{elt.type})\n"
    | end
100    (Fixnum)
-9.6   (Float)
pickle (String)
   [100, -9.6, "pickle"]

Mais nous anticipons un peu, car for est en somme une autre façon de dire each (pour chaque), qui, comme par hasard, est notre premier exemple d’un itérateur.  Les deux formes ci-après sont équivalentes:

# Si vous connaissz C ou Java, vous préférerez peut-être ceci:
for i in collection
  ...
end

# Un programmeur Smalltalk , quand à lui, préférera:
collection.each {|i|
  ...
}

Les itérateurs peuvent souvent remplacer les boucles conventionnelles, et quand on y est habitué, on les trouve plus faciles.  Alors, allons-y et voyons en plus sur eux.

Itérateurs

Les itérateurs ne sont pas un concept propre à ruby.  Ils sont courants dans les langages orientés objet..  Ils sont aussi utilisés en Lisp, bien qu’ils ne s’y appellent pas comme cela.  Cependant le concept d’itérateur est sûrement peu familier pour beaucoup d’entre vous, aussi allons nous l’expliquer plus en détail.

Le verbe itérer signifie faire la même chose plusieurs fois, vous le savez; donc un itérateur est quelque chose qui fait la même chose plusieurs fois.

Quand on écrit des programmes, on a besoin de boucles dans diverses situations.  En C, on les code avec des for ou des while. Par exemple,

char *str;
for (str = "abcdefg"; *str != ''; str++) {
  /* ici on traite un caractère */
}

La syntaxe C for(…) fournit une abstraction pour aider à la création d’une boucle, mais le test de *str vis à vis d’un caractère nul requiert du programmeur une connaissance de la représentation interne des chaînes.  C’est ce qui fait percevoir le C comme un langage de bas niveau.  Les langages de plus haut niveau sont caractérisés par leur support plus souple des itérations. Considérons le script shell sh suivant:

#!/bin/sh

for i in *.[ch]; do
  # ... ici on ferait quelque chose sur chaque fichier
done

Tous les fichiers sources C et header du répertoire courant sont traités, et la commande shell traite les détails relatifs à la collecte et à la substitution des noms de fichiers les uns après les autres.  Voilà quelque chose de plus haut niveau que le C, non ?

Mais il y a plus: c’est bien de trouver dans un langage de quoi faire des itérations sur les types d’objets prédéfinis, mais c’est décevant s’il faut revenir aux boucles de bas niveau pour ses propres types d’objets.  En programmation orientée objet, les développeurs définissent souvent les objets à la pelle, aussi le problème y devient-il sérieux.

C’est pourquoi tous les langages orientés objet fournissent ce qu’il faut pour les itérations. Certains fournissent une classe spéciale pour cela, ruby quand à lui permet de définir des itérateurs directement.

Le type string (chaîne) en ruby offre des itérateurs très utiles:

ruby> "abc".each_byte{|c| printf "<%c>", c}; print "\n"
<a><b><c>
   nil

each_byte est un itérateur pour chacun des caractères de la chaîne.  Chaque caractère est placé successivement dans la variable locale c.  Ceci peut être traduit par quelque chose qui ressemble beaucoup à du C …

ruby> s="abc";i=0
   0
ruby> while i<s.length
    |    printf "<%c>", s[i]; i+=1
    | end; print "\n"
<a><b><c>
   nil

… mais l’itérateur each_byte est à la fois conceptuellement plus simple et plus enclin à continuer de fonctionner même si la classe String devait être radicalement modifiée dans le futur.  Un des bénéfices des itérateurs est qu’ils tendent à être robustes face à de tels changements, ce qui est d’ailleurs une caractéristique du code sain en général.  (Patience, on va bientôt parlez des classes.)

Un autre itérateur de String est each_line.

ruby> "anbncn".each_line{|l| print l}
a
b
c
   nil

Tout ce qui ferait le gros du travail en C (chercher les délimiteurs de ligne, générer des sous-chaînes, etc.) est facilement réalisé avec les itérateurs.

L’instruction for qui apparaissait au chapitre précédent accomplit l’itération à l’aide d’un each. Les each sur les chaînes travaillent de la même façon que each_line, alors réécrivons l’exemple ci-dessus avec for:

ruby> for l in "anbncn"
    |   print l 
    | end
a
b
c
   nil

Nous pouvons utiliser une structure de contrôle retry en conjonction avec une boucle, et elle réessayera l’itération courante depuis le début de la boucle.

ruby> c=0
   0
ruby> for i in 0..4
    |   print i
    |   if i == 2 and c == 0
    |     c = 1
    |     print "n"
    |     retry
    |   end
    | end; print "\n"
012
01234
   nil

yield (fournir) apparaît parfois dans la définition d’un itérateur. yield passe le contrôle au bloc de code qui est associé à l’itérateur (on verra ça plus en détail au chapitre sur les Objets procédures.  L’exemple suivant définit un itérateur repeat, qui répète un bloc de code le nombre de fois indiqué en argument.

ruby> def repeat(num)
    |   while num > 0
    |     yield
    |     num -= 1
    |   end
    | end
   nil
ruby> repeat(3) { print "foo\n" }
foo
foo
foo
   nil

Avec retry, on peut définir un itérateur qui fonctionne comme while, mais c’est très lent.

ruby> def WHILE(cond)
    |   return if not cond
    |   yield
    |   retry
    | end
   nil
ruby> i=0; WHILE(i<3) { print i; i+=1 }
012   nil

Comprenez-vous maintenant ce qu’est un itérateur? Il y a quelques restrictions, mais on peut écrire son propre itérateur, et en fait, à chaque fois qu’on définit un nouveau type de donnée, il est généralement commode de définir les itérateurs qui vont avec.  Sous cet angle, les exemples ci-dessus ne sont pas très utiles.  Nous parlerons d’itérateurs vraiment utiles lorsque nous aurons une meilleure compréhension de ce que sont les classes.

Penser Orienté Objet

Orienté objet est une formule accrocheuse. Appeler n’importe quoi “orienté objet” vous donne l’air intelligent.  Ruby se proclame un langage de script orienté objet, mais que veut dire au juste “orienté objet” ?

Il y a eu des tas de réponses à cette question, qui se ramènent probablement à la même chose.  Plutôt que résumer le tout à la hâte, réfléchissons un instant au paradigme de la programmation traditionnelle.

Classiquement, un problème de programmation est attaqué à l’aide de structures de données , et de procédures qui manipulent ces données.  Dans ce modèle, les données sont inertes, passives, et, sans défense, elles gisent à la merci du corps du programme qui est actif, logique et tout-puissant.

Le problème avec cette approche est que les programmes sont écrits par des programmeurs, qui ne sont que des humains, et ne peuvent avoir clairement en tête à un instant donné qu’une quantité limitée de détails. Au fur et à mesure qu’un projet grossit, son noyau procédural grossit jusqu’au point où il est difficile de se rappeler comment le tout marche. De petits trous de mémoire et des fautes de frappe tournent en bugs bien cachés. Des intéractions complexes et imprévues commencent à apparaitre dans le noyau procédural, et le maintenir commence à ressembler à la tentative de maîtriser un gros calmar en colère sans qu’un tentacule ne vous touche la figure.  Il y a des règles de programmation qui vous aident à minimiser et à localiser les bugs dans cette approche classique, mais il y a une meilleure solution, qui impose cependant de changer radicalement notre façon de travailler.

Ce que la programmation orientée objet fait est qu’elle nous laisse déléguer une grande partie du travail fastidieux et répétitif aux données elles-mêmes, ce qui change le statut de la donnée de passive en active. Autrement dit,

  • On arrête de traiter les données comme des boites au couvercle grand ouvert, qui nous laissent les atteindre et en faire n’importe quoi.
  • On commence à traiter les données comme des machines avec un capot bien fermé, des boutons et des écrans de commande et de contrôle.

Ce que nous appelons ci-dessus “machine” peut être très simple ou très complexe à l’intérieur; on ne peut pas le savoir de l’extérieur, et nous nous interdirons d’ouvrir la machine (sauf quand nous sommes absolument sûrs que quelque chose ne va pas dedans), si bien que nous devons agir dessus par l’intermédiaire des boutons et des écrans. Une fois que la machine est construite, nous n’avons plus à penser à la façon dont elle fonctionne.

Vous pensez peut-être que nous ne faisons que nous rajouter du travail, mais cette approche tend à s’avérer une bonne façon de prévenir toutes sortes d’ennuis.

Commençons par un exemple trop simple pour être d’intérêt pratique, mais qui illustrera au moins une partie du concept. Votre voiture a un compteur journalier. Son rôle est de garder trace de la distance parcourue depuis la dernière fois qu’il a été réinitialisé par une pression sur le bouton. Comment modéliserions nous cela dans un langage de programmation ? En C, le compteur journalier serait juste une variable numérique, peut-être du type float.  Le programme manipulerait cette variable en augmentant sa valeur de petits incréments, avec des remises à zéro quand il faudrait.  Qu’est-ce qui ne va pas là-dedans? Un bug dans le programme pourrait affecter - pour quelque raison imprévue - une valeur incorrecte à la variable. Quiconque a programmé en C sait ce que c’est que de passer des heures ou des jours à traquer ce genre de bug dont la cause semble ridiculement simple une fois qu’on l’a trouvée (le moment de la découverte se manifeste généralement par le bruit d’une forte tape sur le front).

Le même problème serait attaqué sous un tout autre angle dans un contexte orienté objet. La première chose qu’un programmeur demande alors quand il définit le compteur journalier n’est pas “Quel est le type de donnée qui ressemble le plus à ce truc ?” mais “Comment ce truc est-il sensé agir?” La différence s’avère profonde.  Il est nécessaire de passer un peu de temps à décider exactement ce qu’un compteur est, et comment le monde extérieur s’ attend à interagir avec lui.  Nous décidons de construire une petite machine qui contrôle ce qui nous permettra de l’incrémenter, de le réinitialiser, de lire sa valeur, et rien d’autre.

Nous ne fournirons pas de moyen d’assigner au compteur une valeur quelconque. Pourquoi? Parce que nous savons tous que les compteurs ne marchent pas comme ça. Il n’y a que peu de choses qu’on peut faire à un compteur, et ce sont celles-là que nous permettrons. Alors, si quelque chose d’autre dans le programme essaie à tort de placer une autre valeur (par exemple, la température voulue pour la climatisation de la voiture) dans le compteur, eh bien il y aura aussitôt une indication que cela ne va pas. On nous dira à l’exécution du programme (ou peut-être dès la compilation, cela dépend des langages) que nous n’avons pas le droit de mettre une valeur dans les objets de type Compteur.  Le message ne sera peut-être pas aussi clair que ça, mais il s’en rapprochera plus ou moins.  Cela n’évitera pas l’erreur, à vrai dire. Mais cela nous en indiquera rapidement la direction. Voici une des nombreuses choses qui font que la programmation orientée objet peut nous faire gagner beaucoup de temps.

On va généralement au-delà dans l’abstraction, parce qu’il s’avère qu’il est aussi facile de fabriquer des usines à faire des machines que de faire les machines elles-mêmes. Il est donc vraisemblable qu’au lieu de fabriquer directement un compteur, on va plutôt s’arranger pour pouvoir dériver toutes sortes de compteurs d’un seul cadre général. Ce cadre - si on veut, l’usine à compteurs - est ce qu’on appelle une classe, et les compteurs individuels générés d’après ce modèle général (fabriqués dans l’usine) sont ce qu’on appelle les objets. La plupart des langages orientés objet imposent qu’une classe soit définie avant qu’on puisse créer des objets d’une nouvelle sorte, mais ça n’est pas le cas de ruby.

Ici, il convient de noter que l’usage d’un langage orienté objet n’a pas automatiquement pour effet une bonne conception orientée objet. A vrai dire il est possible en n’import quel langage d’écrire du code peu clair, mal fichu, buggé et instable. Ce que ruby fait pour vous, (au contraire de C++ en particulier) est de rendre la pratique de la programmation orientée objet assez naturelle pour que, même quand vous travaillez sur de petites choses, vous ne ressentiez pas le besoin de recourir à du code hideux pour éviter des efforts. On discutera de la façon dont ruby produit ce merveilleux résultat en avançant dans ce guide; le prochain sujet étant relatif au “boutons et cadrans” (les méthodes) et de là nous passerons aux “usines” (les classes). Vous êtes encore là ?

Les méthodes

Qu’est-ce qu’une méthode ? En programmation orientée objet, on n’opère pas directement sur les données de l’extérieur d’un objet; au lieu de cela, on donne aux objet une espèce de compréhension de la façon dont ils doivent opérer sur eux-mêmes (quand on le leur demande gentiment). On peut dire qu’on passe des messages à un objet, et que ces messages vont généralement susciter quelque action ou réponse de la part de l’objet. Ceci doit survenir sans que nous ayons nécessairement la connaissance ou le souci de ce qui se passe dans l’objet pour produire cet effet. Les tâches que nous avons le droit de demander à un objet d’accomplir (ou - c’est équivalent - les messages qu’il comprend) sont les méthodes de cet objet.

En ruby, on invoque une méthode d’un objet avec une notation par point (.) comme en C++ ou en Java. L’objet auquel on s’adresse est nommé à gauche du point.

ruby> "abcdef".length
   6

Pour parler d’une façon très intuitive on “demande” à cet objet sa longueur.  Pour parler plus technique, on invoque la méthode length de l’objet “abcdef”.

D’autres objets pourraient avoir une autre interprétation de length, ou même aucune.  Les décisions sur la façon de répondre aux messages sont faites à la volée, pendant l’exécution du programme, et l’action résultante peut varier selon ce à quoi une variable se réfère.

ruby> foo = "abc"
   "abc"
ruby> foo.length
   3
ruby> foo = ["abcde", "fghij"]
   ["abcde", "fghij"]
ruby> foo.length
   2

Ce que nous entendons par length peut varier selon l’objet dont nous parlons. La première fois que nous demandons à foo quelle est sa longueur, dans l’exemple ci-dessus, il s’agit d’une simple chaîne de caractères, et il n’y a qu’une seule réponse raisonnable. La seconde fois, foo se réfère à un tableau, et nous pouvons raisonnablement imaginer que la réponse soit 2, 5, ou 10; mais la réponse la plus courante sera bien sûr 2 (les autres réponses peuvent être obtenues si on veut).

ruby> foo[0].length
   5
ruby> foo[0].length + foo[1].length
   10

La chose à remarquer ici est qu’un tableau “sait” quelque chose à propose de ce que c’est que d’être un tableau. Les données en ruby portent en elles des connaissances de ce type, si bien que les demandes qu’on leur fait peuvent être automatiquement satisfaites de diverses façons. Ceci soulage le programmeur de la charge de mémoriser un grand nombre de noms de fonctions parce qu’un nombre relativement petit de noms de méthodes, correspondant aux concepts du langage naturel, peuvent s’appliquer à différents types de données avec pour chacun les résultats appropriés. Cette caractéristique des langages orientés objets (que, malheureusement, Java a faiblement exploité) est appelée le polymorphisme.

Quand un objet reçoit un message qu’il ne comprend pas, une erreur est
“soulevée” :

ruby> foo = 5
   5
ruby> foo.length
ERR: (eval):1: undefined method `length' for 5(Fixnum)

Il est donc nécessaire de savoir quelles sont les méthodes acceptables pour un objet, bien qu’il ne soit pas indispensable de savoir comment ces méthodes sont exécutées.

Si des arguments sont fournis à une méthode, ils sont généralement entourés de parenthèses,

 object.method(arg1, arg2)

mais elles peuvent être omises si cela ne crée pas d’ambiguïté.

 object.method arg1, arg2

Il y a en ruby une variable spéciale: self , qui se réfère à tout objet qui appelle la méthode.  Ceci arrive si souvent que, par commodité, le “self.” peut être omis pour les appels d’un objet à ses propres méthodes:

 self.method_name(args...)

équivaut donc à

 method_name(args...)

Cette forme qui ressemble à ce qui serait un appel de fonction en programmation classique, est en ruby une façon abrégée de noter les invocations de méthodes par self.  Ceci fait de ruby un langage purement orienté objet. Ceci dit, les méthodes se comportent en ruby à peu près comme les fonctions d’autres langages de programmation, à l’avantage de ceux qui ne digèrent pas le fait que, en ruby, les “appels de fonction” sont vraiment des invocations de méthodes.  Vous pouvez donc parler des fonctions comme si elles n’étaient pas vraiment des méthodes d’objets si vous voulez.

Les Classes

Le monde réel est plein d’objets, et non pouvons classifier ceux-ci. Par exemple, un très petit enfant dira peut-être “Ouah Ouah” en voyant un chien, quelle que soit la race; et nous voyons naturellement le monde à travers ce genre de catégories.

Dans la terminologie de la programmation orientée objet, une catégorie d’objets comme les “chiens” est appelée une classe, et les objets appartenant à une classe sont appelés des instances de cette classe.

En général, pour fabriquer un objet en ruby ou dans les autres langages orientées objet, on commence par définir les caractéristiques d’une classe, puis on crée une instance. Pour illustrer cette démarche, nous allons définir une classe Chien toute simple.

ruby> class Chien
    |   def parle
    |     print "Ouah Ouah\n"
    |   end
    | end
   nil

En ruby, une définition de classe est une portion de code comprise en les mots-clés class et end.  Un def dans cette zone commence la définition d’une méthode de la classe, qui, comme on l’a vu au chapitre précédent, correspond à la spécification d’un comportement particulier aux objets de cette classe.

Maintenant que nous avons défini la classe Chien, nous pouvons nous en servir pour fabriquer un chien:

ruby> pochi = Chien.new
   #<Chien:0xbcb90>

Nous avons créé une instance de la classe Chien, et nous lui avons donné le nom pochi.  La méthode new (nouveau) de toute classe en crée une nouvelle instance.  Du fait que pochi est un Chien conformément à notre définition de classe, il a toutes les propriétés que nous avons décidé d’attribuer aux objets de type Chien.  Notre idée de la Chien-néité étant très sommaire, il n’y a qu’une chose que nous puissions demander à pochi de faire.

ruby> pochi.parle
Ouah Ouah
   nil

Fabriquer une nouvelle instance d’une classe est quelquefois appelé instancier cette classe.  Nous devons avoir un chien avant de jouir des plaisirs de sa conversation. Nous ne pouvons pas demander à la classe Chien d’aboyer pour nous.

ruby> Chien.parle
ERR: (eval):1: undefined method `parle' for Chien:class

Ca n’a pas plus de sens que d’essayer de manger le concept d’un sandwich.

D’un autre côté, si nous voulons entendre le son de la voix d’un chien sans risquer de trop nous attacher à lui, nous pouvons toujours créer (instancier) un chien éphémère, temporaire, et tirer de lui un peu de bruit avant qu’il ne disparaisse.

ruby> (Chien.new).parle   # ou, plus couramment, Chien.new.parle
Ouah Ouah
   nil

“Attendez un peu,” direz-vous, “qu’est-ce-que c’est que cette histoire de la pauvre bête disparaissant comme ça ?” C’est vrai: si on ne prend pas la peine de lui donner un nom (comme nous l’avions fait pour pochi), le ramasse-miettes (garbage collector) automatique de ruby décide que c’est un chien abandonné, et l’élimine sans pitié.  En fait, ça n’est pas gênant, vous savez, parce qu’on peut faire autant de chiens qu’on veut.

Héritage

Notre classification des objets dans la vie quotidienne est naturellement hiérarchique. Nous savons que les tous les chats sont des mammifères et que tous les mammifères sont des animaux. Les classes plus étroites héritent des caractéristiques des classes plus larges auxquelles elles appartiennent. Si les mammifères respirent, alors les chats respirent.

Nous pouvons exprimer ce concept en ruby:

ruby> class Mammifere
    |   def respire
    |     print "inhale et exhale\n"
    |   end
    | end
   nil
ruby> class Chat<Mammifere
    |   def parle
    |     print "Miaou\n"
    |   end
    | end
   nil

Bien que nous n’ayons pas précisé qu’un chat doive respirer, tout chat héritera ce comportement de la classe Mammifere puisque Chat a été défini comme une sous-classe de Mammifere.  (En terminologie objet, la classe la plus étroite est une sous-classe, et la classe la plus large une super-classe.) En conséquence, du point de vue du programmeur, les chats obtiennent la capacité à respirer gratuitement, et après qu’on leur aie donné une méthode parle, ils respirent et parlent pour le même prix.

ruby> tama = Chat.new
   #<Chat:0xbd80e8>
ruby> tama.respire
inhale et exhale
   nil
ruby> tama.parle
Miaou
   nil

Il y aura des situations où certaines propriétés de la super-classe ne devraient pas être héritées par une sous-classe particulière. Quoique les oiseaux sachent généralement voler, les pingouins sont une sous-classe d’oiseaux ne volant pas.

ruby> class Oiseau
    |   def toilette
    |     print "Je nettoie mes plumes."
    |   end
    |   def vole
    |     print "Je vole."
    |   end
    | end
   nil
ruby> class Pigouin<Oiseau
    |   def vole
    |     fail "Désolé, je préfère nager."
    |   end
    | end
   nil

Plutôt que définir exhaustivement toutes les caractéristiques de chaque nouvelle classe, on n’a besoin que d’ajouter ou redéfinir les différences entre chaque sous-classe et sa super-classe. Cette utilisation de l’héritage est appelée quelquefois programmation par différence, ou différentielle. C’est un des bénéfices de la programmation orientée objet.

Redéfinition des Méthodes

Dans une sous-classe, on peut changer le comportement des instances en redéfinissant les méthodes de la super-classe.

ruby> class Humain
    |   def identifie
    |     print "Je suis une personne.\n"
    |   end
    |   def tarif_train(age)
    |     if age < 12
    |       print "Tarif réduit.\n";
    |     else
    |       print "Tarif normal.\n";
    |     end
    |   end
    | end
   nil
ruby> Humain.new.identifie
Je suis une personne.
   nil
ruby> class Etudiant1<Humain
    |   def identifie
    |     print "Je suis un étudiant.\n"
    |   end
    | end
   nil
ruby> Etudiant1.new.identifie
Je suis un étudiant.
   nil

Supposons que nous voulions améliorer la méthode identifie de la super-classe plutôt que la remplacer complètement. Pour cela nous pouvons utiliser super.

ruby> class Etudiant2<Humain
    |   def identifie
    |     super
    |     print "Je suis aussi un étudiant.\n"
    |   end
    | end
   nil
ruby> Student2.new.identifie
Je suis un humain.
Je suis aussi un étudiant.
   nil

super nous permet de passer des arguments à la méthode originale. On dit quelquefois qu’il y a deux sortes de personnes…

ruby> class Malhonnete<Humain
    |   def tarif_train(age)
    |     super(11) # on veut un tarif réduit.
    |   end
    | end
   nil
ruby> Malhonnete.new.tarif_train(25)
Tarif réduit.
   nil

ruby> class Honnete<Humain
    |   def tarif_train(age)
    |     super(age) # passe l'argument reçu
    |   end
    | end
   nil
ruby> Honnete.new.tarif_train(25)
Tarif normal.
   nil

Contrôles d’accès

Plus haut, nous avons dit que ruby n’a pas de fonctions, seulement des méthodes. Oui, mais il y a plusieurs sortes de méthodes. Dans ce chapitre, nous introduisons les contrôles d’accès.

Considérons ce qui arrive quand nous définissons une méthode au niveau le plus élevé, pas dans une définition de classe. On peut penser à une telle méthode comme analogue à une fonction telle qu’on en définit dans des langages plus traditionnels tels que C.

ruby> def carré(n)
    |   n * n
    | end
   nil
ruby> carré(5)
   25

Notre nouvelle méthode pourrait apparaître comme n’appartenant à aucune classe, mais en fait ruby l’affecte à la classe Object, qui est super-classe de toutes les classes. En conséquence, tout objet pourrait alors utiliser cette méthode. Et c’est vrai, mais avec une petite restriction: c’est une méthode privée de toute classe. On verra plus bas ce que cela signifie, mais une des conséquences est que cette méthode ne peut être invoquée qu’à la façon d’une fonction, comme ici:

ruby> class Foo
    |   def quatrième_puissance de (x)
    |     carré(x) * carré(x)
    |   end
    | end
  nil
ruby> Foo.new.quatrième_puissance_de 10
  10000

On n’a pas le droit d’appliquer explicitement la méthode à un objet:

ruby> "fish".carré(5)
ERR: (eval):1: private method `carré' called for "fish":String

Ceci préserve intelligemment la nature purement orientée objet de ruby (les fonctions sont encore des méthodes, mais le receveur est implicitement self) tout en fournissant des fonctions qui peuvent être écrites comme dans un langage classique.

Une discipline mentale courante en programmation orientée objet, que nous avons rencontrée plus tôt, concerne la séparation de la spécification et de l’ implémentation, autrement dit du quoi et du comment: quelles taches un objet est supposé accomplir et comment il les accomplit.  Le fonctionnement interne d’un objet devrait généralement être caché aux yeux de ses utilisateurs, qui ne devraient avoir à s’occuper que de ce qui entre et sort de l’objet, et se fier à l’objet pour savoir ce qu’il a à faire en interne.  De ce fait, il est souvent utile que des classes aient des méthodes que le monde extérieur ne voit pas, mais qui sont utilisées en interne (et peuvent être améliorées par le programmeur de la méthode autant qu’il veut, sans affecter la façon dont les utilisateurs voient les objets de cette classe).  Dans l’exemple trivial ci-dessous, imaginez que moteur est ce qui travaille dans la classe sans être vu.

ruby> class Test
    |   def fois_deux(a)
    |     print a," fois deux font ",moteur(a),"\n"
    |   end
    |   def moteur(b)
    |     b*2
    |   end
    |   private:moteur  # ceci cache le moteur aux yeux des
    |                   # utilisateurs
    | end
   Test
ruby> test = Test.new
   #<Test:0x4017181c>
ruby> test.moteur(6)
ERR: (eval):1: private method `moteur' called for #<Test:0x4017181c>
ruby> test.fois_deux(6)
6 fois deux font 12.
   nil

On aurait peut-être attendu que test.moteur(6) rende 12, mais au lieu de ça on apprend que engine est inaccessible quand on agit comme utilisateur d’un objet de type Test.  Seules les autres méthodes Test comme fois_deux, ont le droit d’utiliser engine.  Nous devons passer par l’interface publique, qui consiste en la méthode fois_deux.  Le programmeur qui a la charge de cette classe peut changer moteur librement (ici, par exemple, en remplaçant b*2 par b+b, par exemple pour augmenter les performances) sans affecter la façon dont l’utilisateur interagira avec les objets de type Test.  Cet exemple est évidemment trop simpliste pour être utile, et les bénéfices des contrôles d’accès n’apparaîtront clairement que lorsque nous commencerons à créer des classes plus compliquées et plus intéressantes.

Les méthodes Singleton

Le comportement d’une instance est déterminé par sa classe, mais il y a des fois où nous savons qu’une instance particulière devrait avoir un comportement spécial.  Dans la plupart des langages, il faut se donner la peine de définir une autre classe, qu’on aura à instancier une seule fois. En ruby, on peut donner à n’importe quel objet ses propres méthodes.

ruby> class SingletonTest
    |   def taille
    |     print "25\n"
    |   end
    | end
   nil
ruby> test1 = SingletonTest.new
   #<SingletonTest:0xbc468>
ruby> test2 = SingletonTest.new
   #<SingletonTest:0xbae20>
ruby> def test2.taille
    |   print "10\n"
    | end
   nil
ruby> test1.taille
25
   nil
ruby> test2.taille
10
   nil

Dans cet exemple, test1 et test2 appartiennent à la même classe, mais test2 a une méthode taille redéfinie, et donc ils se comportent différemment.  Une méthode donnée seulement a un objet est appelée une méthode singleton.

Les méthodes singletons sont souvent utilisées pour les éléments de l’interface graphique (GUI - graphic user interface), où différentes actions doivent survenir lorsque différents boutons sont pressés.

Les méthodes singletons ne sont pas propres à ruby, car elles apparaissent aussi dans CLOS, Dylan, etc.  Même, d’autres langages, comme par exemple, Self et NewtonScript, n’ont que ça.  On appelle ces langages des langages à prototypes.

Les Modules

Les modules de ruby sont semblables à des classes, sauf que:

  • Un module ne peut avoir d’instances.
  • Un module ne peut avoir de sous-classes.
  • Un module est défini par module … end.

En fait…la classe Module des modules est la super-classe de la classe Classe des classes. Compris ? Non ? Bon, on continue.

Il y a deux usages typiques des modules. L’un est de réunir des constantes et des méthodes en rapport les unes avec les autres dans un lieu commun.  Le module Math dans la librairie standard joue un tel rôle :

ruby> Math.sqrt(2)
   1.41421
ruby> Math::PI
   3.14159

L’opérateur :: indique à l’interpréteur ruby quel module il devrait consulter pour trouver la valeur d’une constante (on pourrait avoir d’autres modules que Math qui donne un sens à PI). Si nous voulons nous référer aux méthodes ou constantes d’un module directement, sans utiliser le ::, nous pouvons include (inclure) ce module:

ruby> include Math
   Object
ruby> sqrt(2)
   1.41421
ruby> PI
   3.14159

Un autre usage des modules est appelé mixin (mélange).  Certains langages orientés objet, dont C++, permettent l’héritage multiple, c’est-à-dire le fait d’avoir plusieurs super-classes directes pour une classe donnée. Un exemple concret d’héritage multiple est le réveil-matin: vous pouvez ranger les réveils-matin dans la classe des horloges, mais aussi dans la classe des trucs qui font du bruit.

Ruby - à dessein - n’offre pas le véritable héritage multiple, mais propose la technique du mixin (prononcez mixine - ndt) comme une alternative avantageuse. Souvenez-vous qu’un module ne peut être instancié ni sous-classé; mais si on include (inclut) un module dans la définition d’une classe, ses méthodes sont effectivement ajoutées (ou “mélangées”) à la classe.

Les mixin peuvent être compris comme une façon d’obtenir les propriétés particulières qu’on veut avoir.  Par exemple, si une classe a une méthode each, y mêler (par include) le module Enumerable de la librairie standard nous offre “gratis” les méthodes sort (trie) et find (cherche).

Cette utilisation des modules nous offre les fonctionnalités de base de l’héritage multiple, mais nous permet de représenter par un simple arbre les relations de classe, ce qui simplifie considérablement l’implémentation du langage (un choix semblable a été fait par les concepteurs de Java).

L’Objet Procédure

Il est souvent souhaitable de pouvoir spécifier les réponses à des événements inattendus. Il s’avère que c’est plus facile si nous pouvons passer des blocs de code en argument à d’autres méthodes, ce qui veut dire être capables de traiter du code comme si c’était de la donnée.

Un nouvel objet procédure est formé en utilisant proc:

ruby> quux = proc {
    |   print "QUUXQUUXQUUX!!!\n"
    | }
   #<Proc:0x4017357c>

Ce à quoi quux se réfère est un objet, et comme la plupart des objets, il a un comportement qui peut être invoqué. En particulier, nous pouvons lui demander de s’exécuter, via sa méthode call :

ruby> quux.call
QUUXQUUXQUUX!!!
   nil

Bon, et avec tout ça, est-ce que quux peut être utilisé comme argument d’une méthode ? Bien sûr.

ruby> def run( p )
    |   print "Je suis sur le point d'appeler une procédure...\n"
    |   p.call
    |   print "Ca y est, c'est fait.\n"
    | end
   nil
ruby> run quux
Je suis sur le point d'appeler une procédure...
QUUXQUUXQUUX!!!
Ca y est, c'est fait.
   nil

La méthode trap nous permet d’assigner la réponse de notre choix à n’importe quel signal du système.

ruby> gestion_d_interruption = proc{ print "On a tapé Ctrl-C.\n" }
   #<Proc:0x401730a4>
ruby> trap "SIGINT", gestion_d_interruption
   #<Proc:0x401735e0>

Normalement, quand on tape Ctrl-C, on sort de l’interpréteur. Maintenant, un message apparaît et l’interpréteur continue de tourner, si bien que vous ne perdez pas le travail en cours.  (Vous n’êtes pas coincé dans l’interpréteur à vie, vous pouvez encore sortir en tapant exit ou Ctrl-D.)

Un dernier mot avant de passer à un autre sujet: il n’est pas strictement nécessaire de donner un nom à une procédure avant de l’associer à un signal. Un objet procédure anonyme pourrait ressembler à

ruby> trap "SIGINT", proc{ print "On a tapé Ctrl-C.\n" }
   nil

ou même encore plus compact:

ruby> trap "SIGINT", 'print "On a tapé Ctrl-C.\n"'
   nil

Cette forme abrégée est commode et lisible pour écrire de très petites procédures anonymes.

Variables

Ruby a 3 sortes de variables, 1 sorte de constantes, et exactement 2 pseudo-variables.  Les variables et constantes ne sont pas typées.  Les variables non typées ont quelques inconvénients, mais elles ont encore bien plus d’avantages, et s’accordent bien avec la philosophie de ruby : vite et facile.

Les variables doivent être déclarées dans la plupart des langages de programmation, de façon à spécifier leur type, leur modifiabilité (càd constantes ou pas), et leur portée. Comme le type n’est pas un problème, et que le reste est évident - comme on va le voir bientôt - dès qu’on voit le nom de la variable, eh bien nous n’avons pas besoin de déclarations en ruby.

La premier caractère du nom d’une variable suffit à la caractériser au premier coup d’oeil:

$ variable globale
@ variable d’instance
[a-z] ou _ variable locale
[A-Z] constante

Les seules exceptions sont les deux pseudo-variables de ruby: self, qui désigne toujours l’objet en cours d’exécution, et nil, qui est la valeur sans signification affectée aux variables non initialisées.  Les deux ont des noms de variables locales, mais self est une variable globale maintenue par l’interpréteur, et nil est en fait une constante.  Comme ce sont les deux seules exceptions, elles ne rendent pas les choses trop confuses.

Vous ne pouvez pas affecter de valeurs à self ou nil.

main, comme valeur de self, se réfère à l’objet de plus haut niveau :

ruby> self
   main
ruby> nil
   nil

Variables Globales

Une variable est globale si son nom commence par $.  On peut en user depuis n’importe où dans un programme. Avant initialisation, une variable globale a la valeur spéciale nil (rien).

ruby> $foo
   nil
ruby> $foo = 5
   5
ruby> $foo
   5

Les variables globales devraient être utilisées avec parcimonie. Elles sont dangereuses parce qu’on peut écrire dedans depuis n’importe où.  Un abus des variables globales peut rendre la recherche des bugs difficile; il tend aussi à indiquer que le programme n’a pas été bien pensé. A chaque fois que vous estimez nécessaire d’utiliser une variable globale, assurez vous de lui donner un nom explicite et qui ne risque pas d’être utilisé par inadvertance pour quelque chose d’autre ailleurs (l’appeler $foo comme dans l’exemple ci-dessus est probablement une mauvaise idée).

Un aspect sympathique des variables globales est qu’on peut les tracer; vous pouvez définir une procédure qui sera invoquée à chaque fois que la valeur de la variable sera modifiée.

ruby> trace_var :$x, proc{print "$x vaut maintenant ", $x, "\n"}
   nil
ruby> $x = 5
$x vaut maintenant 5
   5

Quand une variable globale est utilisée pour fonctionner comme un trigger (une gâchette) qui invoque une procédure à chaque fois qu’elle change, on l’appelle parfois une variable active.  Par exemple, cela sert pour tenir à jour l’affichage d’une interface graphique.

Il y a une collection de variables spéciales dont le nom est constitué d’un signe dollar ($) suivi d’un unique caractère. Par exemple, $$ contient le process id (identifiant de processus) de l’interprète ruby, et est en lecture seule. Voici les principales variables système et leur signification (voir le Manuel de référence de ruby pour les détails) :

$! Dernier message d’erreur
$@ Lieu de l’erreur
$_ Dernière chaîne lue par gets
$. Numéro de la dernière ligne lue par l’interpréteur
$& Dernière chaîne trouvée par regexp
$~ Dernière trouvaille de regexp, en tableau
$n La nième trouvaille du dernier regexp (équivaut
à $~[n])
$= Flag de sensibilité à la casse (minuscules/majuscules)
des recherches par regexp
$/ Séparateur des articles en entrée
$ Séparateur des articles en sortie
$0 Le nom du script ruby
$* Les arguments de la ligne de commande
$$ L’identifiant du process de l’interpréteur
$? Code retour du dernier processus fils

Dans ce qui précède, $_ et $~ ont une portée locale. Leurs noms suggèrent qu’elles sont globales, mais elles sont beaucoup plus utiles en étant locales, et ces noms sont justifiés par des raisons historiques.

Variables d’instance

Une variable est d’instance si son nom commence par @, et sa portée est limitée aux objets auxquels se réfère self. Deux objets différents, même appartenant à la même classe, peuvent avoir des valeurs différentes dans leurs variables d’instance respectives. De l’extérieur de l’objet, les variables d’instance ne peuvent être ni altérées ni même vues (autrement dit les variables d’instance ne sont jamais publiques), sauf par des méthodes explicitement fournies par le programmeur. Comme les variables globales, les variables d’instance ont la valeur nil jusqu’à ce qu’elles soient initialisées.

Les variables d’instance n’ont pas à être déclarées, en ruby. Ceci donne une structure d’objets très souple. En fait, chaque variable d’instance est dynamiquement ajoutée à l’objet au moment de sa première invocation.

ruby> class InstTest
    |   def set_foo(n)
    |     @foo = n
    |   end
    |   def set_bar(n)
    |     @bar = n
    |   end
    | end
   nil
ruby> i = InstTest.new
   #<InstTest:0x83678>
ruby> i.set_foo(2)
   2
ruby> i
   #<InstTest:0x83678 @foo=2>
ruby> i.set_bar(4)
   4
ruby> i
   #<InstTest:0x83678 @foo=2, @bar=4>

Remarquez que ci-dessus i n’indique une valeur pour @bar qu’après que la méthode set_bar ait été invoquée.

Variables locales

Une variable est locale si son nom commence par une minuscule ou un blanc souligné (_). Les variables locales n’ont pas, contrairement aux variables d’instance ou globales, la valeur nil avant leur initialisation :

ruby> $foo
   nil
ruby> @foo
   nil
ruby> foo
ERR: (eval):1: undefined local variable or method `foo' for
main(Object)

La première affectation que l’on fait à une variable locale agit un peu comme une déclaration. Si vous vous référez à une variable locale non initialisée, l’interpréteur ruby croit à une tentative d’invoquer un méthode de ce nom, d’où le message d’erreur de l’exemple ci-dessus.

En général, la portée d’une variable locale est dans :

  • proc{}
  • loop{}
  • defend
  • classend
  • moduleend
  • …le programme entier si aucun des cas précédents ne s’est
    appliqué.

Dans l’exemple suivant, defined? est un opérateur qui vérifie si un identificateur est défini. Il rend une description de l’identificateur s’il est défini, et nil sinon. Comme vous voyez, la portée de bar est locale à la boucle; et quand on quitte la boucle, bar devient indéfini.

ruby> foo = 44; print foo, "\n"; defined? foo
44
   "local-variable"
ruby> loop{bar=45; print bar, "\n"; break}; defined? bar
45
   nil

Les objets procédures qui “vivent” dans la même portée partagent toutes les variables locales qui appartiennent aussi à cette portée.  Ici la variable locale bar est partagée par main et par les objets procédures p1 et p2:

ruby> bar=0
   0
ruby> p1 = proc{|n| bar=n}
   #<Proc:0x8deb0>
ruby> p2 = proc{bar}
   #<Proc:0x8dce8>
ruby> p1.call(5)
   5
ruby> bar
   5
ruby> p2.call
   5

Notez que le “bar=0” du début ne saurait être omis: cette affectation a pour effet que la portée de bar comprendra p1 et p2.  Sans elle p1 et p2 auraient chacune sa propre variable locale bar, et appeler p2 aurait produit une erreur “undefined local variable or method” (variable locale ou méthode non définie).

Une caractéristique puissante des objets procédures vient de leur capacité à être passés en arguments: les variables locales partagées restent valides même quand elles sont ainsi transmises hors de leur portée initiale.

ruby> def boite
    |   contenu = 15
    |   get = proc{contenu}
    |   set = proc{|n| contenu = n}
    |   return get, set
    | end
   nil
ruby> reader, writer = boite
   [#<Proc:0x40170fc0>, #<Proc:0x40170fac>] 
ruby> reader.call
   15
ruby> writer.call(2)
   2
ruby> reader.call
   2

Ruby est fort sur les portées. Il est évident d’après notre exemple, que la variable contenu est partagée entre reader et writer. Mais nous pouvons aussi fabriquer de multiples paires reader-writer en utilisant boite comme elle est définie dans l’exemple; chaque paire partage une variable locale contenu, et les paires n’interfèrent pas entre elles.

ruby> reader_1, writer_1 = boite
   [#<Proc:0x40172820>, #<Proc:0x4017280c>]
ruby> reader_2, writer_2 = boite
   [#<Proc:0x40172668>, #<Proc:0x40172654>]
ruby> writer_1.call(99)
   99
ruby> reader_1.call
   99
ruby> reader_2.call
   15

Constantes de Classe

Le nom d’une constante commence par une majuscule. Elle doit recevoir une valeur au moins une fois. Dans l’implémentation actuelle de ruby, l’affectation d’une nouvelle valeur à une constante génère un avertissement, mais pas une erreur (la version non-ANSI de eval.rb ne rapporte même pas l’avertissement) :

ruby>fluid=30
   30
ruby>fluid=31
   31
ruby>Solid=32
   32
ruby>Solid=33
   (eval):1: warning: already initialized constant Solid
   33

Les constantes peuvent être définies au sein des classes, mais contrairement aux variables d’instance, elles sont accessibles en dehors de la classe.

ruby> class ConstClass
    |   C1=101
    |   C2=102
    |   C3=103
    |   def show
    |     print C1," ",C2," ",C3,"\n"
    |   end
    | end
   nil
ruby> C1
ERR: (eval):1: uninitialized constant C1
ruby> ConstClass::C1
   101
ruby> ConstClass.new.show
101 102 103
   nil

Les constantes peuvent aussi être définies dans des modules.

ruby> module ConstModule
    |   C1=101
    |   C2=102
    |   C3=103
    |   def showConstants
    |     print C1," ",C2," ",C3,"\n"
    |   end
    | end
   nil
ruby> C1
ERR: (eval):1: uninitialized constant C1
ruby> include ConstModule
   Object
ruby> C1
   101
ruby> showConstants
101 102 103
   nil
ruby> C1=99  # ceci n'est pas une bonne idée
   99
ruby> C1
   99
ruby> ConstModule::C1  # la constante du module reste inchangée... 
   101
ruby> ConstModule::C1=99 
ERR: (eval):1: compile error
(eval):1: parse error
ConstModule::C1=99
                ^
ruby> ConstModule::C1  # ... malgré tout ce qu'on tente de lui faire
   101

Traitement des exceptions : rescue

Un programme en cours d’exécution peut rencontrer des problèmes inattendus. Tel fichier qu’il veut lire n’existe pas; le disque est plein au moment où il veut sauver des données; les saisies de l’utilisateur sont inadéquates…

ruby> fichier = open("tel_fichier")
ERR: (eval):1:in `open': No such file or directory - tel_fichier

Un programme robuste gérera ces situations avec bon sens et amabilité. Atteindre cet objectif peut être une tâche éprouvante. Les programmeurs C sont supposés vérifier le résultat de tout appel système qui pourrait éventuellement échouer, et décider sur le champ ce qui soit être fait:

FILE *file = fopen("tel_fichier "r");
if (file == NULL) {
  fprintf( stderr, "Ficher absent.\n" );
  exit(1);
}
bytes_read = fread( buf, 1, bytes_desired, file );
if (bytes_read != bytes_desired ) {
  /* ici, un traitement d'erreur ... */
}
...

C’est une tâche de programmation si ennuyeuse que les programmeurs tendent à devenir imprudents et à la négliger, et le résultat est un programme qui ne gère pas bien les erreurs.  D’un autre côté, faire ce qu’il faut peut rendre les programmes difficiles à lire, parce que beaucoup de code de gestion d’erreur noie le code directement utile.

En ruby, comme en beaucoup de langages modernes, on peut gérer les exceptions relatives à un bloc de code d’un façon compartimentée, ce qui permet de faire face aux surprises d’une façon efficace, sans trop charger ni le programmeur ni quiconque aura à relire le source. Le bloc de code commençant par begin s’exécute jusqu’à ce que survienne une exception, qui provoque le transfert du contrôle vers le bloc de gestion des exceptions, qui suit le mot rescue (secours).  Si aucune exception ne survient, le code de secoursn’est pas exécuté.  La méthode qui suit retourne la première ligne d’un fichier texte, ou nil si une exception survient :

def première_ligne( nom_de_fichier )
  begin
    file = open("un_fichier")
    info = file.gets
    file.close
    info  # La dernière chose évaluée est la valeur retournée
  rescue
    nil   # On ne peut pas lire le fichier ? Alors retourne nil
  end
end

Il y a des circonstances où on voudrait pouvoir contourner un problème d’une façon constructive. Ici, si le fichier est indisponible, on essaie l’entrée standard à sa place… :

begin
  file = open("un_fichier")
rescue
  file = STDIN
end

begin
  # ... traite l'entrée ...
rescue
  # ... et traite les autres exceptions ici.
end

retry (réessaie) peut être utilisé dans le code de secours pour tenter de réexécuter ce qui a échoué le coup d’avant.  Ceci nous permet de réécrire l’exemple précédent d’une façon un peu plus compacte :

fname = "un_fichier"
begin
  file = open(fname)
  # ... traite l'entrée ...
rescue
  fname = "STDIN"
  retry
end

Seulement, il y a une faille. Si le fichier n’existe pas on va boucler indéfiniment. Il faut penser à éviter ce genre d’écueils si vous utilisez retry pour gérer les exceptions.

Les librairies de ruby soulèvent des exceptions si une erreur survient, et vous pouvez aussi déclencher volontairement une exception dans votre propre code. Pour soulever une exception, utilisez raise (soulever).  On peut lui passer un argument, qui est une chaîne décrivant l’exception. Cet argument est optionnel mais ne devrait pas être omis. On y a accès via la variable globale spéciale $!.

ruby> raise "erreur test"
   test error
ruby> begin
    |   raise "test 2"
    | rescue
    |   print "Une erreur est survenue: ",$!, "\n"
    | end
Une erreur est survenue: test 2
   nil

Traitement des exceptions : ensure

Quand une méthode s’achève il peut y avoir du travail de nettoyage à effectuer à coup sûr.  Peut-être qu’un fichier ouvert doit être refermé, que de la donnée bufferisée doit être transmise, etc. S’il y avait toujours un seul point de sortie pour chaque méthode, nous pourrions avec confiance placer notre code de nettoyage à cet endroit-là et savoir qu’il serait exécuté; mais une méthode a souvent plusieurs sorties, et notre code de nettoyage pourrait être sauté à cause d’une exception.

begin
  file = open("/tmp/some_file", "w")
  # ... écrit dans le fichier ...
  file.close
end

Ci-dessus, qu’une exception survienne pendant que la section de code où nous écrivons dans le fichier, et le fichier restera ouvert. Et nous préférerions éviter le genre de redondance qui suit:

begin
  file = open("/tmp/some_file", "w")
  # ... écrit dans le fichier ...
  file.close
rescue
  file.close
  fail # soulève une exception
end

C’est lourd, et devient ingérable lorsque le code se complique parce que nous avons à gérer un tas de return et break.

Pour cette raison, nous pouvons ajouter un autre mot-clé à la séquence “begin…rescue…end“, et c’est ensure (garantit que).  Le bloc ensure s’exécute, que le bloc begin réussisse ou pas.

begin
  file = open("/tmp/some_file", "w")
  # ... écrit dans le fichier ...
rescue
  # ... gère les exceptions ...
ensure
  file.close   # ... et ceci arrive dans tous les cas.
end

Il est possible d’utiliser ensure sans rescue, ou le contraire, mais s’il sont utilisés dans le même bloc begin…end, alors le rescue doit précéder le ensure.

Les Accesseurs

Qu’est-ce qu’un accesseur ?

Nous avons brièvement discuté les variables d’instance dans un chapitre précédent, mais nous n’en avons pas fait grand-chose jusqu’à maintenant. Les variables d’instance d’un objet sont ses attributs, les choses qui le distinguent des autres objets de la même classe. Il est important d’être capable de lire et d’écrire ces attributs, et nous avons besoin pour ce faire de méthodes appelées accesseurs d’attributs. Nous verrons dans un moment que nous n’avons pas toujours à écrire ces méthodes explicitement, mais voyons le reste du film d’abord. Les deux sortes d’accesseurs sont les writers (écriveurs) et les readers (lecteurs).

ruby> class Fruit
    |   def set_kind(k)  # un écriveur
    |     @kind = k
    |   end
    |   def get_kind     # un lecteur
    |     @kind
    |   end
    | end
   nil
ruby> f1 = Fruit.new
   #<Fruit:0xfd7e7c8c>
ruby> f1.set_kind("peach")  # utilise l'écriveur
   "peach"
ruby> f1.get_kind           # utilise le lecteur
   "peach"
ruby> f1                    # inspecte l'objet
   #<Fruit:0xfd7e7c8c @kind="peach">

C’est simple : nous pouvons stocker et récupérer l’information sur la sorte de fruits que nous manipulons. Mais nos noms de méthode sont un peu verbeux. Ce qui suit est plus bref, et plus conventionnel en ruby :

ruby> class Fruit
    |   def kind=(k)
    |     @kind = k
    |   end
    |   def kind
    |     @kind
    |   end
    | end
   nil
ruby> f2 = Fruit.new
   #<Fruit:0xfd7e7c8c>
ruby> f2.kind = "banane"
   "banane"
ruby> f2.kind
   "banane"

La méthode inspect

Il faut faire ici une petite digression. Vous aurez remarqué maintenant que lorsque nous essayons de regarder un objet directement, on nous montre un truc abscons du genre #<anObject:0×83678>.  Ceci est simplement le comportement par défaut, et nous sommes libres de le changer. Tout ce dont nous avons besoin est d’ajouter une méthode nommée inspect.  Elle doit retourner une chaîne qui décrit l’objet d’une façon qui nous plaise mieux, par exemple en donnant la valeur de tout ou partie de ses variables d’instance.

ruby> class Fruit
    |   def inspect
    |     "Un fruit de la variété " + @kind 
    |   end
    | end
   nil
ruby> f2
   "Un fruit de la variété banane"

Une méthode proche est to_s (convertir en chaîne), qui est utilisée quand on imprime un objet.  En général, vous pouvez penser à inspect comme à un outil qui sert quand on écrit et débogue un programme, et à to_s comme un moyen de raffiner les sorties d’un programme. eval.rb utilise inspect pour tous ses affichages.  Vous pouvez utiliser la méthode p pour obtenir facilement des affichages d’aide à la mise au point dans vos programmes.

# Ces deux lignes sont équivalentes:
p anObject
print anObject.inspect, "\n"

Faire facilement ses accesseurs

Du fait qu’on a constamment besoin d’accesseurs pour ses variables d’instance, ruby fournit des raccourcis plus commodes que les formes standards.

Raccourci Equivaut à
attr_reader :v def v; @v; end
attr_writer :v def v=(value); @v=value; end
attr_accessor :v attr_reader :v; attr_writer :v
attr_accessor :v, :w attr_accessor :v; attr_accessor :w

Tirons parti de cela et ajoutons un indicateur de fraîcheur ànos fruits.  D’abord nous générons automatiquement un accesseur en lecture et en écriture, puis nous incorporons la nouvelle information dans inspect :

ruby> class Fruit
    |   attr_accessor :condition
    |   def inspect
    |     "Une " + @kind + @état"
    |   end
    | end
   nil
ruby> f2.condition = "mûre"
   "mûre"
ruby> f2
   "Une banane mûre"

On continue avec nos fruits

Si personne ne mange nos fruits mûrs, nous pourrions peut-être laisser le temps prélever son tribut.

ruby> class Fruit
    |   def le_temps_passe
    |     @condition = "pourrie"
    |   end
    | end
   nil
ruby> f2
   "Une banane mûre"
ruby> f2.le_temps_passe
   "pourrie"
ruby> f2
   "Une banane pourrie"

Mais en jouant comme cela, nous avons introduit un petit problème. Que se passe-t-il si nous essayons de créer maintenant un troisième fruit ? Souvenez-vous que les variables d’instance n’existent pas tant qu’on ne leur a pas assigné une valeur…

ruby> f3 = Fruit.new
ERR: failed to convert nil into String

C’est la méthode inspect qui se plaint ici, et à bon droit. Nous lui avons demandé de rendre compte de la sorte (kind) et de l’état (condition) d’un fruit, mais pour l’instant, f3 n’a reçu aucun attribut.  Si nous voulions, nous pourrions réécrire la méthode inspect pour qu’elle teste les variables d’instance en utilisant la méthode defined? et ne rende compte d’elles que si elles existent, mais ça n’est peut-être pas très utile, vu que tout fruit réel a une variété et un état, et que nous devrions plutôt faire en sorte que ces attributs soient toujours valorisés d’une façon ou d’une autre. C’est le sujet de la section suivante.

Initialisation des objets

Notre classe Fruit du chapitre précédent avait deux variables d’instance, une pour décrire la sorte de fruits (kind) et l’autre pour décrire son état (condition). C’est seulement après avoir écrit une méthode inspect pour cette classe que nous avons réalisé que ça n’avait pas de sens pour un fruit de n’avoir aucune valeur pour ces attributs.  Heureusement, ruby fournit un moyen de s’assurer que des variables d’instance sont toujours initialisées.

La méthode initialize

A chaque fois que ruby crée un nouvel objet, il regarde s’il a une méthode initialize et si oui, il l’exécute.  Ainsi nous n’avons qu’à utiliser une méthode initialize pour donner des valeurs par défaut à toutes les variables d’instance, et la méthode inspect aura quelque chose à dire.

ruby> class Fruit
    |   def initialize
    |     @kind = "pomme"
    |     @condition = "mûre"
    |   end
    | end
   nil
ruby> f4 = Fruit.new
   "Une pomme mûre"

Changer les présupposés à volonté

Il y a des cas où une valeur par défaut ne veut pas dire grand-chose. Est-ce qu’une variété de fruit par défaut veut vraiment dire quelque chose ? Il peut être préférable de spécifier que tout fruit doit avoir sa variété précisée au moment de sa création. Pour ce faire, nous devrions ajouter un paramètre à la méthode initialize.  Pour des raisons que nous ne préciserons pas ici, les arguments que vous passez à new sont repassés à initialize.

ruby> class Fruit
    |   def initialize( k )
    |     @kind = k
    |     @condition = "mûre"
    |   end
    | end
   nil
ruby> f5 = Fruit.new "mangue"
   "Une mangue mûre"
ruby> f6 = Fruit.new
ERR: (eval):1:in `initialize': wrong # of arguments(0 for 1)
(mauvais nombre d'argument - 0 au lieu d'1)

Initialisation élastique

Ci-dessus nous voyons qu’une fois qu’un argument a été associé à la méthode initialize , il ne peut plus être omis sans générer une erreur.  Si nous voulons être plus circonstanciés, nous pouvons faire que l’argument soit utilisé s’il est fourni, et qu’on revienne aux valeurs par défaut sinon.

ruby> class Fruit
    |   def initialize( k="pomme" )
    |     @kind = k
    |     @condition = "mûre"
    |   end
    | end
   nil
ruby> f5 = Fruit.new "mangue"
   "Une mangue mûre"
ruby> f6 = Fruit.new
   "Une pomme mûre"

Vous pouvez utiliser des arguments par défaut pour toutes les méthodes, pas seulement initialize.  La liste des arguments doit être ordonnée de façon que ceux ayant des valeurs par défaut soient en dernier.

Quelquefois il est utile de fournir plusieurs moyens d’ initialiser un objet. Bien que ce soit au-delà de la portée de ce tutorial, ruby offre la réflexion d’objet et les listes d’arguments de longueur variable, qui, ensemble, permettent la surcharge de méthode.

De choses et d’autres

Ce chapitre traite de divers problèmes pratiques.

Délimiteurs d’instruction

Certains langages requièrent l’emploi d’une certaine ponctuation, souvent un point-virgule (;), pour terminer toutes les instructions d’un programme.  Au lieu de cela, ruby suit la convention des shells unix comme sh et csh.  Les instructions multiples sur une même ligne doivent être séparées par des point-virgules, mais ils ne sont pas imposés en fin de ligne, une fin de ligne étant traitée comme un point-virgule. Si une ligne se termine sur un antislash (), la fin de ligne qui suit est ignorée, ce qui vous permet d’avoir une unique ligne logique étalée sur plusieurs lignes physiques.

Les commentaires

Pourquoi écrire des commentaires ? Un code bien écrit tend à être auto-documenté, mais il est souvent utile de mettre des annotations dans les marges, et cela peut être une erreur de croire que les autres seront capables de regarder votre code et de tout suite voir les choses comme vous. En plus de cela, vous-mêmes, pour des raisons évidentes, serez une autre personne dans quelques jours… Qui de nous n’a pas dit, revenant après quelques temps sur un programme qu’il a écrit, “je sais que c’est moi qui ai écrit ça, mais bon dieu, qu’est-ce que ça peut bien vouloir dire ?”

Certains programmeurs expérimentés signaleront, assez justement, que des commentaires en contradiction avec le code, ou en retard sur lui peuvent être pires que pas de commentaires du tout. Sans doute, les commentaires ne devraient pas se substituer à un code lisible; si votre code est peu clair, il est probablement aussi buggé. Vous éprouverez peut-être aussi plus le besoin de commenter pendant que vous apprendrez ruby, et puis de moins en moins au fur et à mesure que vous apprendrez à exprimer vos idées d’une façon simple, élégante et lisible.

Ruby suit une convention commune aux langages de script, qui est d’utiliser un signe dièse (#) pour indiquer le début d’un commentaire. Tout ce qui suit un # non entouré de guillemets ou d’apostrophes, jusqu’à la fin de la ligne, est ignoré par l’interpréteur.

De plus, pour faciliter la mise en commentaire de grands blocs, l’interpréteur ignore aussi tout ce qui est entre une ligne commençant par “=begin” et une autre ligne commençant par “=end“.

#!/usr/local/bin/ruby

=begin
********************************************************************
 Ceci est un bloc de commentaire, quelque chose que vous écrivez au
profit d'un lecteur humain (y compris vous-même). L'interpréteur
l'ignore. Pas besoin de '#' au début de ces lignes.
********************************************************************
=end

Organiser votre code

L’interpréteur ruby traite le code au fur et à mesure qu’il le rencontre. Il n’y a pas de phase de compilation; si quelque chose n’a pas été lu, ce quelque chose est tout simplement indéfini.

# ceci provoque une erreur "undefined method" (méthode non définie):

print successor(3),"\n"

def successor(x)
  x + 1
end

Ceci ne vous force pas pour autant, contrairement à ce que vous pourriez penser d’abord, à organiser votre code strictement de haut en bas. Quand l’interpréteur rencontre une définition de méthode, celle-ci peut sans inconvénient contenir des références non résolues, pourvu qu’elles le soient au moment où la méthode sera effectivement utilisée:

# Conversion de fahrenheit en celsius, décomposée en deux étapes

def f_to_c(f)
  rapport (f - 32.0)  # Voici une référence anticipée (forward
		      # reference), mais ça va quand même.
end

def rapport(x)
  x * 5.0 / 9.0
end

printf "%.1f est une température confortable.\n", f_to_c( 72.3 )

Bien que ceci puisse vous paraître moins commode que ce que à quoi vous êtes peut-être habitué en Perl ou Java, c’est moins restrictif que d’essayer d’écrire du C sans prototypes (ce qui impose de conserver toujours un ordre relatif entre quoi appelle quoi). Vous pouvez toujours mettre le code de plus haut niveau à la fin de votre source. Et même c’est moins ennuyeux qu’il n’y parait d’abord. Une façon raisonnable et sans effort d’obtenir le comportement que vous voulez est de définir une fonction main au début de votre source, et de l’appeler de la fin .

#!/usr/local/bin/ruby

def main
  # ici vous mettez votre code de plus haut niveau
end

# ... ici le code de réalisation, organisé comme vous voulez

main # ... et l'exécution commence ici

Il est d’un grand secours que ruby vous offre des outils pour décomposer des programmes compliqués en morceaux lisibles, réutilisables, logiquement reliés. Nous avons déjà vu l’utilisation de include pour accéder aux modules. Vous découvrirez aussi que load et require sont utiles pour cela. Load marche comme si le fichier mentionné était copié et collé dans le source (un peu comme les directive de préprocesseur #include en C). require est un peu plus sophistiqué, car il fait que le code mentionné est chargé au plus une fois, et seulement si on en a besoin. Il y a d’autres différences entre load et require; référez vous au manuel du langage ou à la FAQ pour plus d’informations.

Et voilà…

Ce tutorial devrait suffire pour commencer à écrire des programmes en ruby. Lorsque d’autres questions surgiront, vous pourrez vous plonger dans le Manuel de référence pour en apprendre plus sur ruby. La FAQ et la Library reference sont aussi des ressources importantes.

Bonne chance, et heureuse programmation !

© Novembre 2002 - Alain Feler pour Project:Omega

afeler Le langage Ruby ,

  1. 17/05/2009 à 20:32 | #1

    ooops, en fait il manque le caractère “\” devant le “n” pour former le caractère de nouvelle ligne “\n”.
    En reessayant ce devrait être mieux là.

    Merci pour la remarque.

  2. 17/05/2009 à 16:20 | #2

    Bonjour! Merci beaucoup pour ce post, vraiment très instructif. Toutefois rien qu’au début j’ai rencontré des différences avec le résultat que vous obtenez.

    J’exécute sous Mac. Dans le terminal, le code suivant:
    % Ruby
    Print “Hello Worldn”
    Print “Goodbye Worldn”
    CMD+D

    me donne:

    hello worldngoodbye worldnmacbook-de-XXXXXXX:~ XXXXXXX$

    Je ne comprend donc pas pourquoi vous ajoutez des “n” à la fin des mots, et pourquoi j’obtiens un résultat aussi bâclé que ça.

  3. Thierry
    13/05/2009 à 19:10 | #3

    Très bien cet article, j’y ai appris beaucoup.

    Bravo !

  1. Pas encore de trackbacks
S'abonner aux commentaires de cet article