Сегодня речь пойдет о блоках в руби. Долго думал, как бы понятнее сформулировать что же такое блок, но это слишком абстрактная вещь. На данном этапе давайте условимcя, что блок - это код, который не исполняется в том месте где определен, а сохраняется на будущее. Они очень похожи на методы, но есть ключевые различия, о которых потом.
И так, как же создать блок? Самый простой способ - передать его во время вызова методу.
say('hello') { 'world' } #методу say мы передали аргумент и блок, ограниченный { }
say 'hello' do #тут блок ограничен ключевыми словами do end
'world'
end
И в первом же примере мы видим некоторые проблемы - обратите внимания, что в первом вызове метода say я заключил аргумент в круглые скобки, во втором - нет. Это потому, что если бы мы в первом случае оставили бы вызов метода без скобок - интерпретатор подумал бы, что мы ему передаем второй агрумент - хэш в {} и выдал бы ошибку, что хэш не валидный. За гибкость нужно платить, время от времени.
В руби есть негласное правило - когда передаешь в метод однострочный блок - используй синтаксис с {}, когда в блоке - несколько строк - то с do - end.
Передали мы блок, как его вызвать?
a) При помощи yield:
def say(first)
second = yield
puts "#{first} #{second}"
end
say('hello') { 'world' } # выведет 'hello world'
b) Получить блок в аргумент
def say(first, &block) #мы можем попросить руби погрузить блок в аргумент
#для этого заведем новый аргумент и перед его именем поставим символ &
#такие аргументы должны идти последними!
#если методу передали блок, то он сохранится в block
#если не передали - в block будет nil
second = block.call #исполняем блок, получем в переменную second значение последнего в нем выражения
puts "#{first} #{second}"
end
Приведенный выше код выдаст ошибку, если мы вызовем say без блока (мы ведь вызываем yield без блока или пытаемся вызвать call у nil). Для того, чтобы узнать, передали ли блок методу можно использовать block_given?
def say(first)
second = block_given? ? yield : ''
puts "#{first} #{second}"
end
#если же вы использовали аргумент с &, то можно сделать так (block_given? все еще будет работать!)
def say(first, &block)
second = block.nil? ? '' : block.call
puts "#{first} #{second}"
end
Кстати метод, который использует блоки в руби называется итератором.
В блок можно передавать аргументы
def say
yield 'hello'
end
#или
def say(&block)
block.call 'hello'
end
#а вот так мы принимаем аргумент внутри блока
say { |word| puts "hello #{word}" }
say do |word|
puts "hello #{word}"
end
Можно использовать аргументы по умолчанию
def say
yield 'hello'
end
say { |a, b = 'world'| puts "#{a} #{b}" }
Или опускать скобки в последнем аргументе - хэше
def say
yield 'hello', a: 'world'
end
say { |a, b| puts "#{a} #{b[:a]}" }
Или сгрузить оставшиеся аргументы в массив
def say
yield 'hello', 'w', 'o', 'r', 'l', 'd'
end
say { |a, *chars| puts "#{a} #{chars.join}" }
Этим блоки похожи на методы. Но только похожи - на самом деле тут используются правила для параллельного присваивания переменных, а не для методов.
def say
yield 'a'
end
say { |a, b| } #в переменную b поместится nil
def say
yield [1, 2]
end
say { |a, b| } #в a будет 1, в b будет 2
Теперь мы можем сами посмотреть, что же такое блок - давайте вернем блок из метода и посмотрим что же это такое
def say(&block)
block
end
block = say { 'hello' }
block.class #=> Proc
Так вот, блоки, которые мы передаем методам - на самом деле объекты класса Proc. Раз есть класс - значит мы можем инициализировать блок напрямую, не возвращая его из метода.
block = Proc.new { 'hello' } #Это конечно смущает, но чтобы получить объект блока, мы должны передать в Proc.initialize блок
#вот такое масло масляное
block = proc { 'hello' } #тоже самое, что и сверху, короткий синтаксис.
Теперь мы можем этот объект исполнить напрямую
block.call #=> 'hello' #в call можно передавать аргументы для блока
И мы можем передавать его методам
say(&block) #мы вызываем метод say, передаем ему block, чтобы указать что его нужно использовать как блок, а не как обычный аргумент, мы используем символ &
Последнее можно использовать, когда у вас несколько вызовов методов с одними и теми же блоками
У символа & есть еще одно полезное применение. Допустим в блок передается объект, если все что делает блок - вызывает один единственный метод у этого объекта, то можно написать так
arr = %w(one two three for five) #это такой короткий синтаксис для массива строк
arr.map(&:length) #мы хотим у каждого элмента вызвать метод length
arr.map { |string| string.length } #это то же самое, что и в предыдущем примере.
Proc объекты конечно же могут принимать аргументы
arr = [1,2,3,4,5]
block = proc { |i| i % 2 } #так мы указываем аргументы для proc, совсем как для блоков
arr.min_by(&block)
arr.max_by(&block)
arr.find(&block)
Помните мы говорили о аргументах для блоков? Все это действует и на proc. Но есть более строгая форма Proc - lambda, которая работает с аргументами совсем как метод.
l = lambda { puts 'hello world' }
l.call #выведет 'hello world', это почти тот же proc
l = lambda { |a, b| }
l.call 1 #ошибка
l.call #ошибка
l.call 1, 2 #lambda строго следит за количеством аргументов!
В общем мое мнение - lambda немного логичниее чем proc, но чаще все же используют последний. С этими кстати пытаются бороться создатели руби. В новом синтаксисе (в мейкере он тоже работает) возвращается именно lambda, а не proc
-> { 'hello world' } #тоже самое, что и lambda { 'hello world' }
->(a, b) { } #тоже самое, что и lambda { |a, b| }
Так же lambda от proc отличается тем, как в них работает return, если вызвать такой proc или lambda внутри метода. Lambda просто вернет значение, proc же тоже вернет значение... из метода, прервав его дальнейшее исполнение. В общем об этом полезно знать, но лучше стараться не использовать.
Блоки очень похожи на методы, но есть очень важные различия по поводу локальных перменных - блоки видят локальные переменные, методы - нет. Это достигается путем привязки блока к binding объекту в том месте, где блок был создан.
a = 1
def say
a
end
p = proc { a }
say() #=> выдаст ошибку
p.call #=> вернет 1
Я этим очень часто пользуюсь, зачастую даже не замечая этого. Есть еще одна очень важна особенность о который нужно
всегда помнить - блок всегда привязан к тому контексту, где был определен. Если вы к примеру, определите proc, а в нем вызываете локальные переменные, инстанс переменные и т.п., потом передадите этот proc не важно куда, в другой класс в тридцатом модуле, он все равно при вызове будет искать все эти переменные в том месте, где было определен.
a = 1
p = proc { a }
module A
module B
module C
def say(block)
a = 2
block.call
end
end
end
end
class My
include A::B::C
end
My.new.say(p) #=> 1
Думаю на этом все, надеюсь вам было интересно!