Retour sur les variables

C’est parti pour un nouveau chapitre. Ici, nous reviendrons aux variables en approfondissant ce que nous avons vu dans le deuxième chapitre.

Une histoire de références

Qu’est-ce qu’une variable ?

Nous les utilisons depuis le premier chapitre de ce tutoriel et nous nous n’avons jamais vraiment répondu à cette question ? Bon, cela doit changer, voyons un peu ce qui se passe quand on déclare une variable.

Dans d’autres langages, on peut apprendre qu’une variable est une boîte. Une simple boîte dans laquelle on range une valeur. Pour accéder à cette valeur, on ouvre la boîte. Pour changer la valeur d’une variable, on change le contenu de cette boîte. Simple, non ?

Mais cette idée est à supprimer. En Ruby, une variable n’est rien d’autre qu’un nom.

Quoi, un nom ? Dans ce cas, pourquoi ce nom représente une valeur ?

Le verbe utilisé dans la question est intéressant. Le nom représente une valeur. C’est exactement ça. Une variable n’est pas une boîte qui contient quelque chose, c’est juste un nom qui représente une valeur.

Bien sûr, pour cela, la variable doit contenir quelque chose pour savoir ce qu’elle est censée représenter.

Une représentation plus juste des variables pourrait être les pointeurs dans les langages comme le C (à un plus haut niveau). Un pointeur est une variable qui contient une adresse mémoire. Si deux pointeurs sont égaux, alors non seulement la valeur pointée est la même, mais en plus, l’adresse mémoire est la même.

En Ruby, cela se vérifie ainsi, si a = 4 et b = 4, alors non seulement les valeurs a et b sont égales, mais en plus, elles représentent le même objet en mémoire (on ne peut pas vérifier leur adresse mémoire, mais on peut vérifier leur identifiant). Il n’y a pas un objet créé pour a et un autre pour b, leur identifiant est le même. Ce sont des références au même objet.

Une histoire d’identifiant

Mettons maintenant un nom sur ce dont nous parlons. Nous avons dit qu’une variable servait juste à représenter une valeur. Puis nous avons parlé d’identifiant. Le mot « identifiant » est le bon. D’ailleurs, il existe une méthode qu’on peut utiliser avec tout en Ruby, la méthode object_id. Cette méthode donne l’identifiant d’une variable.

Utilisons cette méthode pour vérifier ce que l’on a dit dans la partie précédente.

a = 4
b = 4
puts a.object_id
puts b.object_id

Comme nous l’avons dit, les deux variables ont le même identifiant.

Nous pouvons même aller encore plus loin.

a = 4
b = 4
puts a.object_id
puts b.object_id
puts 4.object_id

Bien sûr, en affectant a à b, ils ont également le même identifiant.

a = 4
b = a
puts a.object_id
puts b.object_id
Tableaux, chaînes de caractères…

Cependant, l’affaire est différente pour les tableaux, chaînes de caractères ou encore hachages. Lorsque nous créons deux fois le même tableau, par exemple, ils n’ont pas le même identifiant. Regardons ce code.

a = [122, 32]
b = [122, 32]
puts a.object_id
puts b.object_id
puts [122, 32].object_id

Comme nous pouvons le remarquer en exécutant ce code, les trois identifiants sont différents. Mais ne nous inquiétons pas, c’est dû à la manière dont nous pouvons modifier ces variables (et donc ce sera traité dans la partie qui suit).

Modification de variables

Dans cette partie, nous allons tenter de répondre à une question simple.

Que se passe-t-il lorsque l’on modifie une variable ?

La question est simple, mais nous allons voir que la réponse ne l’est pas forcément.

Commençons par le cas de variables simples. Regardons le résultat de ce code.

a = 12
puts a.object_id
a = 15
puts a.object_id

Ce code conforte notre idée précédente : a ne fait plus référence au même objet, son identifiant n’est donc plus le même.

Maintenant que nous avons vu ce qui se passe lors de la modification de variable, nous pouvons passer à la partie suivante. Ah, non, il nous reste le cas des tableaux, des chaînes de caractères et des hachages qui comme tout à l’heure sont particuliers.

a = []
puts a.object_id
a = [1, 2]
puts a.object_id

Les identifiants sont différents comme tout à l’heure.

Quoi ? Pourquoi ? Pourtant, nous avons dit que ce cas était différent ?

Oui, mais le fait est qu’ici on change de tableau. Au départ, a faisait référence au tableau [], après la seconde affectation, il fait référence au tableau [1, 2].

Cependant, lorsque nous modifions le tableau directement (donc sans affectation), la variable fait toujours référence au même tableau. Aucun nouveau tableau n’a été créé, c’est l’ancien tableau qui a été modifié.

a = []
puts a.object_id
a << 1
a << 2
a[0] = 23
puts a.object_id

Dans ce code, nous ne réaffectons pas notre variable a. Elle fait toujours référence au même tableau, tableau qui en revanche a été beaucoup modifié. L’identifiant reste néanmoins le même.

Et ceci nous permet de répondre à la question que nous nous posions à propos de la différence entre les tableaux et les variables plus simples : si en déclarant deux fois le même tableau, les deux variables avaient le même identifiant, alors en modifiant l’un on modifierait l’autre, ce qui n’est pas trop voulu. Voilà ce que l’on veut (et c’est ce qui se passe).

a = []
b = [] # L’identifiant de b est différent de celui de a.
a << 1 # a est modifié, cela ne change pas b.
c = a  # c = a donc c et a ont le même identifiant.
c << 2 # On modifie c, a est également modifié, car ils font référence au même tableau.
a = [] # On donne une nouvelle valeur à a ; c garde son ancienne valeur, par contre.

Cette partie était remplie de nouvelles informations, alors il faut prendre le temps de bien tout assimiler et faire des tests avant de passer à la suite.

Les variables globales

Un problème : passage de variables aux méthodes

Nous allons maintenant nous intéresser à un problème : on passe une variable en paramètre à une méthode, comment faire pour que les modifications effectuées sur la variable de la méthode affectent la variable que l’on a passée en argument. Un exemple de code pour voir le problème.

def increment(a)
  a = a + 1
end

x = 6
increment(x)
print x

On aimerait que la valeur de x soit 7 après l’appel de la méthode increment, or elle a gardé son ancienne valeur 6.

On pourrait alors se dire que c’est normal, qu’il suffit d’incrémenter x plutôt que a dans la méthode increment (et dans ce cas, l’argument n’est plus nécessaire).

def increment
  x = x + 1
end

x = 6
increment
print x

Et ce code ne fonctionne pas, et nous obtenons une erreur. Cette erreur est due à ce que l’on appelle la portée des variables. Il s’agit de définir dans quelle partie du code une variable existe. Il faut donc savoir que les variables n’existent que dans le bloc dans lequel elles ont été déclarées. Ainsi, dans notre code précédent, les deux variables x sont différentes :

  • la variable x de la méthode increment n’existe que dans la méthode increment et pas en dehors ;
  • la variable x du reste du code n’existe qu’en dehors de la méthode increment.

On dit que ce sont des variables locales.

Ainsi, dans la méthode increment, on essaie d’incrémenter la valeur de x, or x n’existe pas encore (et a donc la valeur nil) et l’opération nil + 1 ne peut pas être faite.

Pour mieux voir le phénomène de portée, testons des codes de ce genre.

def f(x)
  x = x + 1 # Fonctionne, car x existe, étant un paramètre.
  puts x
end

def g
  x = 3     # On est obligé de déclarer x avant.
  x = x + 1
  puts x
end

def h(x)
  x = x + 1 
  puts x 
  return x   # On retourne x.
end

x = 1
f(x)
puts x # x vaut toujours 1 en dehors de la méthode.
g(x)
puts x # x vaut toujours 1.
h(x)
puts x # x vaut toujours 1.
x = h(x)
puts x # x vaut maintenant 2, car on a récupéré la valeur retournée par la méthode h.

Ce code nous montre qu’il est possible de faire la méthode increment grâce au retour de la méthode (nous le savions déjà). Mais si notre méthode doit changer plusieurs valeurs, c’est déjà plus embêtant à faire.

En fait, en Ruby, les valeurs sont passées par référence (normal, puisqu’en Ruby, tout est référence). Ainsi, voici ce qui se passe.

def f(x)
  puts "Au début de la méthode, l’id de x est #{x.object_id}."
  x = x + 1
  puts "À la fin de la méthode, l’id de x est #{x.object_id}."
end

x = 2
puts "En dehors de la méthode avant son appel, l’id de x est #{x.object_id}."
f(x)
puts "En dehors de la méthode après son appel, l’id de x est #{x.object_id}."

On remarque que l’identifiant de x au début de la fonction est le même que celui du x extérieur (normal, puisqu’elles font référence au même objet et que le passage de paramètre se fait par référence). Par contre, une fois l’incrémentation effectuée, l’identifiant du x de la fonction a changé et puisqu’il ne s’agit pas du même x que celui à l’extérieur, alors la valeur du x extérieur n’a pas changé.

Cependant, cela nous permet de découvrir quelque chose : si on sait modifier un objet sans modifier son identifiant, alors on peut le modifier dans une fonction. En particulier, on sait modifier un tableau dans une fonction.

def f(tab)
  tab << 3
end

tab = [1, 2]
f(tab)
print tab
Les variables globales

Pour régler ce problème, nous pouvons utiliser une variable globale. Les variables globales sont des variables qui, contrairement aux variables locales, sont accessibles dans tout le programme. Pour déclarer une variable globale, il suffit de préfixer son nom du caractère $.

$x # x est une variable globale.

Les variables x et $x sont bien sûr différentes.

Donc, ce code fonctionne.

def print_x
  print $x
end

$x = 'Voici une variable globale.'
print_x

Nous sommes maintenant capables de faire une méthode qui incrémente la variable globale $x.

def increment
  $x = $x + 1
end

$x = 1
increment
print $x # Affiche 2.

Les variables globales semblent être une solution pertinente aux problèmes que nous pourrons avoir, et nous pourrions penser sûrement à les utiliser tout le temps. Pourtant, leur utilisation peut s’avérer dangereuse et ne conduit pas à une bonne conception du programme. On peut presque toujours se passer des variables globales et c’est ce que nous ferons.

Variables globales réservées

Il existe des variables globales dont le nom est réservé. Cela veut dire que nous ne pourrons pas utiliser ces noms de variable dans notre programme. Ces variables ont chacune leur utilité et nous pouvons les utiliser dans nos programmes. Certaines de ces variables sont utiles pour le débogage ou pour apporter d’autres fonctionnalités. D’autres ont des usages plus simples. Voyons les deux plus simples d’entre elles :

  • la variable __FILE__ est une chaîne de caractères qui représente le nom du fichier courant ;
  • la variable __LINE__ est un entier qui représente la ligne du fichier courant que l’interpréteur est en train d’exécuter.

On peut ainsi écrire ce petit script qui affiche juste le nom du fichier et la ligne à laquelle on se trouve.

puts "Le fichier interprété est le fichier #{__FILE__} et nous sommes actuellement à la ligne #{__LINE__}."
puts "Nous sommes maintenant à la ligne #{__LINE__}."

puts "Nous avons laissé une ligne vide dans le fichier, nous sommes à la ligne #{__LINE__}."

Nous avons dit que les variables globales étaient à éviter et c’est vrai. En fait, les seules variables globales dont l’utilisation est normale et tout à fait conseillée sont les variables globales réservées. Tout au long du tutoriel, nous en verrons d’autres, mais nous pouvons déjà nous renseigner sur elles et sur leur utilité.

Les symboles

Garder le même identifiant

Il peut arriver que l’on veuille attribuer un objet unique à une variable, c’est-à-dire garder le même identifiant. C’est le but des symboles. On peut alors voir les symboles comme un nom associé à un identifiant. Donc chaque fois qu’une variable aura pour valeur ce symbole, ce sera toujours le même identifiant qui lui sera associé. Un symbole en Ruby c’est un nom précédé du caractère :. Donc…

symbol = :symbol

C’est un symbole. Vérifions alors que deux symboles identiques ont le même identifiant.

x = :symbol
y = :symbol
print x.object_id == y.object_id

Ce code affiche True. Toutes les variables qui auront pour valeur :symbol sont le même objet. Que ce soit dans une méthode ou autre part.

def f
  y = :s
  puts y.object_id
end

def g
  z = :s
  puts z.object_id
end

x = :s
puts x.object_id
puts :s.object_id
f
g

Nous obtenons bien le même identifiant partout.

Mais quel est l’intérêt de cette démarche ?

Imaginons par exemple que nous devions utiliser un très grand nombre de variables avec le même contenu. En utilisant les symboles, nous faisons une économie de mémoire (un seul objet plutôt que plusieurs).

Nous pouvons également déclarer des symboles avec la syntaxe %s, mais conformément aux bonnes pratiques, nous allons préférer utiliser :. Si nous devions malgré tout utiliser %s, nous privilégierons son utilisation avec les parenthèses comme délimiteurs.

Les symboles et les chaînes de caractères

Les symboles sont surtout utilisés en lieu et place des chaînes de caractères. La question qui se pose alors est quand utiliser les symboles et quand au contraire utiliser des chaînes de caractères. La réponse est en rapport avec l’information qui est importante :

  • si c’est le contenu qui est important, il faut préférer une chaîne de caractères ;
  • si c’est l’identité qui importe, il faut préférer un identifiant.

Prenons l’exemple d’une application qui gère les nationalités de plusieurs individus. Ce qui nous intéresse, c’est l’information de la nationalité et non pas comment cette nationalité s’écrit. En fait, on pourrait tout aussi bien écrire français que France ou encore fr. Par contre, le nom de l’individu sera une chaîne de caractère. On va donc plutôt écrire ceci.

name1 = 'Nom'
nation1 = :fr
name_2 = 'Nom2'
nation2 = :en
name3 = 'Nom3'
nation3 = :it # it c’est Italie, pas informatique, on est bien d’accord…

Pour convertir un symbole en chaîne de caractères, on peut toujours utiliser la méthode to_s. Cependant, nous pouvons également utiliser la méthode id2name, plus idiomatique.

L’opération inverse (à savoir convertir une chaîne de caractères en symboles) est faite à l’aide de la méthode intern. Donc…

name = 'nom'
nation = :fr
x = name.intern     # x = :nom
y = nation.to_s    # y = "fr"
z = nation.id2name # z = "fr"

Nous avons déclaré un symbole pour chacune des nations que nous voulions représenter. Utiliser un tableau de symboles aurait été plus simple. Pour cela, nous pouvons déclarer notre tableau normalement, mais aussi utiliser la syntaxe %i. Ainsi, nous allons écrire nations = %i[fr en it]. La syntaxe %i est d’ailleurs à privilégier pour écrire un tableau de symboles (remarquons que là encore, puisqu’il s’agit d’un tableau, les crochets sont les délimiteurs à privilégier).

Les symboles et les hachages

Les symboles sont communément utilisés en tant que clés pour les hachages. Ainsi, il est courant de voir ceci.

hash = { :last_name  => 'Mon nom',
         :first_name => 'Mon prénom',
         :age        => 2015 }

En fait, les hachages sont généralement utilisés de cette manière et, dorénavant, c’est ce que nous allons faire. Ce choix est de plus parfaitement logique. En effet, ce qui compte pour une clé, c’est bien son identité, et non sa valeur. Le contenu de la clé nous importe peu, pourvu qu’il s’agisse de la bonne clé. Il existe même une syntaxe plus simple pour utiliser des symboles en tant que clés de hachages.

hash = { last_name: 'Mon nom',
         first_name: 'Mon prénom',
         age: 2015 }

C’est une bonne pratique en Ruby d’utiliser des symboles comme clés de hachages et c’est aussi une bonne pratique de privilégier la dernière syntaxe que nous avons vue pour cela.

Modifier nos variables dans des méthodes

Nous avons parlé des variables globales et des symboles. Pourtant, nous n’avons toujours pas répondu à la question originelle : comment modifier nos variables dans des méthodes ?

Utiliser le retour des méthodes

Il est possible de modifier directement une variable dans une méthode. Mais nous n’allons pas voir comment le faire et allons, pour le moment, nous contenter de renvoyer la valeur modifiée et de la récupérer. Par exemple, si l’on veut une méthode qui met une variable au carré, nous allons faire ce code.

def square(x)
  return x * x
end

print 'Entrez un nombre : '
number = gets.chomp.to_i
puts "Nous allons calculer le carré de #{number}."
number = square(number)
puts "Son carré est #{number}."

Ainsi, on détourne le problème. Plutôt que de chercher à modifier la variable dans la méthode, on renvoie sa valeur modifiée et on la récupère.

Retourner plusieurs variables ?

Maintenant que nous avons établi que nous allions utiliser la valeur retournée par les méthodes, une question peut nous venir à l’esprit.

Comment modifier plusieurs variables ?

Un exemple simple serait une méthode dite de swap, c’est-à-dire une méthode qui échange la valeur de deux variables. Comment peut-on faire cette méthode ?

En fait, nous allons utiliser ce que l’on appelle l’affectation multiple. Elle consiste, comme son nom l’indique, à faire plusieurs affectations en une seule fois. Voyons un exemple d’affectation multiple.

a, b = 2, 3 

Ici, a vaut 2 et b vaut 3. Et on peut alors échanger les valeurs de a et de b directement.

a, b = b, a

Pour plus de lisibilité, nous pouvons ajouter des crochets autour des valeurs assignées. On écrit alors notre méthode de swap.

def swap(a, b)
  return [b, a]
end

a, b = [1, 2]
a, b = swap(a, b)

Bien sûr, notre méthode swap n’a pas grande utilité, puisqu’il suffit d’écrire a, b = [b, a]. Cependant, elle illustre parfaitement le principe de retour multiple, puisque dans cette méthode, nous avons renvoyé deux valeurs.

Si les crochets nous rappellent les tableaux, ce n’est pas sans raison. En effet, nous pouvons de cette manière affecter les éléments d’un tableau à des variables.

tab = [1, 2, 3]
a, b, c = tab
print "a = #{a}, b = #{b} et c = #{c}."

En fait, c’est même plus que ça. Lorsqu’on écrit a, b = b, a (ou a, b = [b, a]) b, a et [b, a] sont des tableaux. Donc, pour renvoyer plusieurs valeurs, nous renvoyons un tableau.

Notons finalement que nous ne sommes pas obligés d’affecter tous les éléments du tableau à une variable. Si nous affectons moins de valeurs qu’il n’y a d’éléments dans le tableau, les valeurs sont affectées suivant l’ordre du tableau. Si nous en affectons plus, les variables qui sont en trop vaudront nil. Le code qui suit illustre ce comportement.

a, b, c = [1, 2, 3, 4]
puts "a = #{a}, b = #{b} et c = #{c}."
a, b, c = [5, 6]
puts "a = #{a}, b = #{b} et c = #{c}."

Cela signifie notamment que nous pouvons faire une méthode qui retourne un tableau, et ne récupérer que les éléments qui nous intéressent. Il faudra alors faire attention à la place des éléments dans notre tableau. Ceux que l’on voudra toujours récupérer seront placés en premier, et les optionnels en dernier (ce qui rappelle fortement le fonctionnement des arguments optionnels de méthodes).

Copier un objet

Si au contraire on veut copier un tableau par exemple, le signe égal ne fonctionne pas puisque la variable que l’on créera de cette manière fera référence au tableau d’origine. Dès lors, si on veut copier un tableau, il nous faut créer un tableau vide et copier les éléments du premier tableau dedans. Ceci est valable pour les tableaux, les hachages, les chaînes de caractères, etc.

def copy_tab(tab)
  copy = []
  tab.each { |e| copy << e }
  copy
end

tab = [1, 2, 3]
copy = copy_tab(tab)
copy[0] = 3
print tab
print copy

Ruby nous fournit la méthode dup qui nous permet de nous affranchir de tout ça.

def f(tab)
  tab << 2
  tab << 4
  tab[0] = 0
end

# Ne Fonctionne pas
tab = [1, 2, 3]
copy = tab
f(copy)
print tab
print copy

# Fonctionne

tab = [1, 2, 3]
copy = tab.dup
f(copy)
print tab
print copy

Ce chapitre très théorique est enfin fini. N’oublions pas que les variables globales sont à proscrire dans la plupart des cas, contrairement aux symboles qui sont très utiles et très utilisés, avec les hachages par exemple. Pour renvoyer plusieurs valeurs dans une méthode, nous renvoyons des tableaux et nous récupérons ensuite les valeurs voulues en faisant une affectation multiple.

  • Les variables sont passées par référence, et une variable passée en paramètre dans une fonction n’est pas modifiée.
  • Pour modifier des variables, nous allons utiliser les retours des fonctions.
  • Les variables globales sont à éviter (sauf celles qui sont réservées).
  • Les symboles permettent d’associer un objet unique à une variable. Ils sont souvent utilisés avec les hachages.