Блоки

Программист Ruby Коммерсант Проект года 1 место Учитель Проект месяца 1 место
Больше
11 года 8 мес. назад #73327 от Iren_Rin
Iren_Rin создал тему: Блоки
Сегодня речь пойдет о блоках в руби. Долго думал, как бы понятнее сформулировать что же такое блок, но это слишком абстрактная вещь. На данном этапе давайте условимcя, что блок - это код, который не исполняется в том месте где определен, а сохраняется на будущее. Они очень похожи на методы, но есть ключевые различия, о которых потом.
И так, как же создать блок? Самый простой способ - передать его во время вызова методу.
Code:
say('hello') { 'world' } #методу say мы передали аргумент и блок, ограниченный { } say 'hello' do #тут блок ограничен ключевыми словами do end 'world' end
И в первом же примере мы видим некоторые проблемы - обратите внимания, что в первом вызове метода say я заключил аргумент в круглые скобки, во втором - нет. Это потому, что если бы мы в первом случае оставили бы вызов метода без скобок - интерпретатор подумал бы, что мы ему передаем второй агрумент - хэш в {} и выдал бы ошибку, что хэш не валидный. За гибкость нужно платить, время от времени.
В руби есть негласное правило - когда передаешь в метод однострочный блок - используй синтаксис с {}, когда в блоке - несколько строк - то с do - end.

Передали мы блок, как его вызвать?
a) При помощи yield:
Code:
def say(first) second = yield puts "#{first} #{second}" end say('hello') { 'world' } # выведет 'hello world'

b) Получить блок в аргумент
Code:
def say(first, &block) #мы можем попросить руби погрузить блок в аргумент #для этого заведем новый аргумент и перед его именем поставим символ & #такие аргументы должны идти последними! #если методу передали блок, то он сохранится в block #если не передали - в block будет nil second = block.call #исполняем блок, получем в переменную second значение последнего в нем выражения puts "#{first} #{second}" end
Приведенный выше код выдаст ошибку, если мы вызовем say без блока (мы ведь вызываем yield без блока или пытаемся вызвать call у nil). Для того, чтобы узнать, передали ли блок методу можно использовать block_given?
Code:
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
Кстати метод, который использует блоки в руби называется итератором.

В блок можно передавать аргументы
Code:
def say yield 'hello' end #или def say(&block) block.call 'hello' end #а вот так мы принимаем аргумент внутри блока say { |word| puts "hello #{word}" } say do |word| puts "hello #{word}" end
Можно использовать аргументы по умолчанию
Code:
def say yield 'hello' end say { |a, b = 'world'| puts "#{a} #{b}" }

Или опускать скобки в последнем аргументе - хэше
Code:
def say yield 'hello', a: 'world' end say { |a, b| puts "#{a} #{b[:a]}" }

Или сгрузить оставшиеся аргументы в массив
Code:
def say yield 'hello', 'w', 'o', 'r', 'l', 'd' end say { |a, *chars| puts "#{a} #{chars.join}" }

Этим блоки похожи на методы. Но только похожи - на самом деле тут используются правила для параллельного присваивания переменных, а не для методов.
Code:
def say yield 'a' end say { |a, b| } #в переменную b поместится nil def say yield [1, 2] end say { |a, b| } #в a будет 1, в b будет 2

Теперь мы можем сами посмотреть, что же такое блок - давайте вернем блок из метода и посмотрим что же это такое
Code:
def say(&block) block end block = say { 'hello' } block.class #=> Proc
Так вот, блоки, которые мы передаем методам - на самом деле объекты класса Proc. Раз есть класс - значит мы можем инициализировать блок напрямую, не возвращая его из метода.
Code:
block = Proc.new { 'hello' } #Это конечно смущает, но чтобы получить объект блока, мы должны передать в Proc.initialize блок #вот такое масло масляное block = proc { 'hello' } #тоже самое, что и сверху, короткий синтаксис.

Теперь мы можем этот объект исполнить напрямую
Code:
block.call #=> 'hello' #в call можно передавать аргументы для блока
И мы можем передавать его методам
Code:
say(&block) #мы вызываем метод say, передаем ему block, чтобы указать что его нужно использовать как блок, а не как обычный аргумент, мы используем символ &
Последнее можно использовать, когда у вас несколько вызовов методов с одними и теми же блоками

У символа & есть еще одно полезное применение. Допустим в блок передается объект, если все что делает блок - вызывает один единственный метод у этого объекта, то можно написать так
Code:
arr = %w(one two three for five) #это такой короткий синтаксис для массива строк arr.map(&:length) #мы хотим у каждого элмента вызвать метод length arr.map { |string| string.length } #это то же самое, что и в предыдущем примере.


Proc объекты конечно же могут принимать аргументы
Code:
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, которая работает с аргументами совсем как метод.
Code:
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
Code:
-> { 'hello world' } #тоже самое, что и lambda { 'hello world' } ->(a, b) { } #тоже самое, что и lambda { |a, b| }
Так же lambda от proc отличается тем, как в них работает return, если вызвать такой proc или lambda внутри метода. Lambda просто вернет значение, proc же тоже вернет значение... из метода, прервав его дальнейшее исполнение. В общем об этом полезно знать, но лучше стараться не использовать.


Блоки очень похожи на методы, но есть очень важные различия по поводу локальных перменных - блоки видят локальные переменные, методы - нет. Это достигается путем привязки блока к binding объекту в том месте, где блок был создан.
Code:
a = 1 def say a end p = proc { a } say() #=> выдаст ошибку p.call #=> вернет 1
Я этим очень часто пользуюсь, зачастую даже не замечая этого. Есть еще одна очень важна особенность о который нужно всегда помнить - блок всегда привязан к тому контексту, где был определен. Если вы к примеру, определите proc, а в нем вызываете локальные переменные, инстанс переменные и т.п., потом передадите этот proc не важно куда, в другой класс в тридцатом модуле, он все равно при вызове будет искать все эти переменные в том месте, где было определен.
Code:
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
Думаю на этом все, надеюсь вам было интересно!
Спасибо сказали: AnnTenna, Lekste, DeadElf79, Ren310, strelokhalfer, Dprizrak1, Lipton, Amphilohiy, yuryol

Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.

Программист Ruby 2 место Готв Победитель Сбитой кодировки Учитель Оратор
Больше
11 года 8 мес. назад #73361 от Amphilohiy
Amphilohiy ответил в теме Блоки
Небольшое дополнение, хоть и не очень в тему. В Эйсе схема работы между Сценами и окнами заключается в событиях, которые именуют handler. Суть проста - окна отлавливают события по апдейту, и вызывают подходящий handler в виде переданного им метода (как правило метода сцены). Т.к. сцена все окна держит в своему пространстве, то и переданные методы тоже могут спокойно работать с другими окнами. В итоге окно может влиять на другое окно, хоть по сути в классе ничего подобного не описано. Но это о пользе биндингов в мукере.

Я верю, что иногда компьютер сбоит, и он выдает неожиданные результаты, но остальные 100% случаев это чья-то криворукость.
Спасибо сказали: Iren_Rin

Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.

Время создания страницы: 0.090 секунд
Работает на Kunena форум