Comment Ruby renvoie-t-il deux valeurs?

94

Chaque fois que j'échange des valeurs dans un tableau, je m'assure que j'ai stocké l'une des valeurs dans une variable de référence. Mais j'ai trouvé que Ruby peut renvoyer deux valeurs et échanger automatiquement deux valeurs. Par exemple,

array = [1, 3, 5 , 6 ,7]
array[0], array[1] = array[1] , array[0] #=> [3, 1] 

Je me demandais comment Ruby faisait cela.

Pete
la source
9
Techniquement, Ruby ne renvoie pas deux valeurs. Il peut renvoyer un tableau qui à son tour est affecté à deux variables.
Charles Caldwell

Réponses:

164

Contrairement à d'autres langages, la valeur de retour de tout appel de méthode dans Ruby est toujours un objet. Cela est possible car, comme tout dans Ruby, nillui-même est un objet.

Vous verrez trois modèles de base. Ne renvoyant aucune valeur particulière:

def nothing
end

nothing
# => nil

Renvoyer une valeur singulière:

def single
  1
end

x = single
# => 1

Cela correspond à ce que vous attendez des autres langages de programmation.

Les choses deviennent un peu différentes lorsqu'il s'agit de plusieurs valeurs de retour. Ceux-ci doivent être spécifiés explicitement:

def multiple
  return 1, 2
end

x = multiple
# => [ 1, 2 ]
x
# => [ 1, 2 ]

Lorsque vous effectuez un appel qui renvoie plusieurs valeurs, vous pouvez les diviser en variables indépendantes:

x, y = multiple
# => [ 1, 2 ]
x
# => 1
y
# => 2

Cette stratégie fonctionne également pour les types de substitution dont vous parlez:

a, b = 1, 2
# => [1, 2]
a, b = b, a
# => [2, 1]
a
# => 2
b
# => 1
tadman
la source
8
Vous pouvez renvoyer explicitement un tableau [1, 2]et cela fonctionnera de la même manière que vos exemples ci-dessus.
Hauleth
6
@hauleth Une bonne observation. J'aurais dû préciser que 1,2par lui-même est invalide, mais soit de return 1,2ou de [1,2]travail.
tadman
50

Non, Ruby ne prend pas en charge le retour de deux objets. (BTW: vous renvoyez des objets, pas des variables. Plus précisément, vous renvoyez des pointeurs vers des objets.)

Cependant, il prend en charge l'affectation parallèle. Si vous avez plusieurs objets sur le côté droit d'une affectation, les objets sont rassemblés dans un Array:

foo = 1, 2, 3
# is the same as
foo = [1, 2, 3]

Si vous avez plus d'une "cible" (variable ou méthode de définition) sur le côté gauche d'une affectation, les variables sont liées aux éléments d'un Arraysur le côté droit:

a, b, c = ary
# is the same as
a = ary[0]
b = ary[1]
c = ary[2]

Si le côté droit n'est pas un Array, il sera converti en un à l'aide de la to_aryméthode

a, b, c = not_an_ary
# is the same as
ary = not_an_ary.to_ary
a = ary[0]
b = ary[1]
c = ary[2]

Et si nous mettons les deux ensemble, nous obtenons cela

a, b, c = d, e, f
# is the same as
ary = [d, e, f]
a = ary[0]
b = ary[1]
c = ary[2]

L'opérateur splat sur le côté gauche d'une affectation est lié à cela. Cela signifie "prendre tous les éléments de gauche sur le Arraycôté droit":

a, b, *c = ary
# is the same as
a = ary[0]
b = ary[1]
c = ary.drop(2) # i.e. the rest of the Array

Enfin, les affectations parallèles peuvent être imbriquées en utilisant des parenthèses:

a, (b, c), d = ary
# is the same as
a = ary[0]
b, c = ary[1]
d = ary[2]
# which is the same as
a = ary[0]
b = ary[1][0]
c = ary[1][1]
d = ary[2]

Lorsque vous returnutilisez une méthode ou nextou breakun bloc, Ruby traitera ce genre de chose comme le côté droit d'une affectation, donc

return 1, 2
next 1, 2
break 1, 2
# is the same as
return [1, 2]
next [1, 2]
break [1, 2]

À propos, cela fonctionne également dans les listes de paramètres de méthodes et de blocs (les méthodes étant plus strictes et les blocs moins stricts):

def foo(a, (b, c), d) p a, b, c, d end

bar {|a, (b, c), d| p a, b, c, d }

Les blocs étant "moins stricts", c'est par exemple ce qui fait Hash#eachfonctionner. Il s'agit en fait d' yieldun seul élément à deux éléments Arrayclé et valeur du bloc, mais nous écrivons généralement

some_hash.each {|k, v| }

au lieu de

some_hash.each {|(k, v)| }
Jörg W Mittag
la source
16

tadman et Jörg W Mittag connaissent Ruby mieux que moi, et leurs réponses ne sont pas fausses, mais je ne pense pas qu'ils répondent à ce que OP voulait savoir. Je pense que la question n’était pas claire cependant. À mon avis, ce que OP voulait demander n'a rien à voir avec le renvoi de plusieurs valeurs.


La vraie question est, lorsque vous voulez changer les valeurs de deux variables aet b(ou deux positions dans un tableau comme dans la question d'origine), pourquoi n'est-il pas nécessaire d'utiliser une variable temporelle tempcomme:

a, b = :foo, :bar
temp = a
a = b
b = temp

mais peut être fait directement comme:

a, b = :foo, :bar
a, b = b, a

La réponse est que dans l'assignation multiple, tout le côté droit est évalué avant l'assignation de tout le côté gauche, et cela ne se fait pas un par un. Donc a, b = b, an'est pas équivalent à a = b; b = a.

Évaluer d'abord tout le côté droit avant l'assignation est une nécessité qui découle de l'ajustement lorsque les deux côtés =ont des nombres de termes différents, et la description de Jörg W Mittag peut être indirectement liée à cela, mais ce n'est pas le problème principal.

Sawa
la source
8

Les tableaux sont une bonne option si vous n'avez que quelques valeurs. Si vous voulez plusieurs valeurs de retour sans avoir à connaître (et être confondu par) l'ordre des résultats, une alternative serait de retourner un Hash qui contient les valeurs nommées que vous voulez.

par exemple

def make_hash
  x = 1
  y = 2
  {x: x, y: y}
end

hash = make_hash
# => {:x=>1, :y=>2}
hash[:x]
# => 1
hash[:y]
# => 2
Pronoob
la source