Как Ruby возвращает два значения?

95

Всякий раз, когда я меняю местами значения в массиве, я убеждаюсь, что сохранил одно из значений в ссылочной переменной. Но я обнаружил, что Ruby может возвращать два значения, а также автоматически менять местами два значения. Например,

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

Мне было интересно, как это делает Руби.

Пит
источник
9
Технически Ruby не возвращает два значения. Он может возвращать один массив, который, в свою очередь, присваивается двум переменным.
Чарльз Колдуэлл,

Ответы:

166

В отличие от других языков, возвращаемое значение любого вызова метода в Ruby всегда является объектом. Это возможно, потому что, как и все в Ruby, nilсамо по себе является объектом.

Вы увидите три основных шаблона. Не возвращая конкретного значения:

def nothing
end

nothing
# => nil

Возврат единственного числа:

def single
  1
end

x = single
# => 1

Это соответствует тому, что вы ожидаете от других языков программирования.

При работе с несколькими возвращаемыми значениями все становится немного иначе. Их необходимо указать явно:

def multiple
  return 1, 2
end

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

При вызове, который возвращает несколько значений, вы можете разбить их на независимые переменные:

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

Эта стратегия также работает с заменами, о которых вы говорите:

a, b = 1, 2
# => [1, 2]
a, b = b, a
# => [2, 1]
a
# => 2
b
# => 1
тадман
источник
8
Вы можете явно вернуть массив, [1, 2]и это будет работать так же, как и в приведенных выше примерах.
Hauleth
6
@hauleth Хорошее наблюдение. Я должен был прояснить, что 1,2сам по себе недействителен, но либо работает, return 1,2либо [1,2]работает.
tadman
51

Нет, на самом деле Ruby не поддерживает возврат двух объектов. (Кстати: вы возвращаете объекты, а не переменные. Точнее, вы возвращаете указатели на объекты.)

Однако он поддерживает параллельное присвоение. Если у вас есть более одного объекта в правой части назначения, объекты собираются в Array:

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

Если у вас есть более одной «цели» (переменная или метод установки) в левой части присваивания, переменные привязываются к элементам Arrayв правой части:

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

Если правая часть не является Array, она будет преобразована в одну с помощью to_aryметода

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]

И если мы сложим их вместе, мы получим

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

С этим связан оператор splat в левой части присваивания. Это означает "взять все оставшиеся элементы в Arrayправой части":

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

И последнее, но не менее важное: параллельные назначения можно вкладывать в круглые скобки:

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]

Когда вы переходите returnиз метода nextили breakблока, Ruby будет рассматривать это как правую часть присваивания, поэтому

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

Кстати, это также работает в списках параметров методов и блоков (с более строгими методами и менее строгими блоками):

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

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

Например, «менее строгие» блоки делают Hash#eachработу. Это на самом деле yieldса одного из двух элементов Arrayключа и значения в блок, но мы обычно пишем

some_hash.each {|k, v| }

вместо того

some_hash.each {|(k, v)| }
Йорг В. Миттаг
источник
17

Тадман и Йорг В. Миттаг знают Руби лучше меня, и их ответы не ошибочны, но я не думаю, что они отвечают на то, что хотел знать OP. Я думаю, что вопрос был непонятен. Насколько я понимаю, то, что OP хотел спросить, не имеет ничего общего с возвратом нескольких значений.


Настоящий вопрос заключается в том, что когда вы хотите переключить значения двух переменных aи b(или двух позиций в массиве, как в исходном вопросе), почему нет необходимости использовать временную переменную, tempнапример:

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

но это можно сделать напрямую, например:

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

Ответ заключается в том, что при множественном присваивании вся правая часть оценивается до назначения всей левой части, и это не выполняется по одному. Так a, b = b, aне эквивалентно a = b; b = a.

Первая оценка всей правой части перед назначением является необходимостью, которая следует из корректировки, когда обе стороны =имеют разное количество терминов, и описание Йорга Миттага может быть косвенно связано с этим, но это не главная проблема.

сава
источник
8

Массивы - хороший вариант, если у вас всего несколько значений. Если вам нужно несколько возвращаемых значений без необходимости знать (и вас смущает) порядок результатов, альтернативой может быть возврат хеша, который содержит любые именованные значения, которые вы хотите.

например

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
пронуб
источник