Войти на сайт

Авторизация, ждите ...
×

ТЕМА: Блоки

Блоки 10 года 2 мес. назад #73327

  • Iren_Rin
  • Iren_Rin аватар
  • Вне сайта
  • Мастер
  • Сообщений: 247
  • Спасибо получено: 537
  • КоммерсантПрограммист RubyПроект месяца 1 местоУчительПроект года 1 место
Сегодня речь пойдет о блоках в руби. Долго думал, как бы понятнее сформулировать что же такое блок, но это слишком абстрактная вещь. На данном этапе давайте условим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
Думаю на этом все, надеюсь вам было интересно!
Администратор запретил публиковать записи гостям.
За этот пост поблагодарили: AnnTenna, Lekste, DeadElf79, Ren310, strelokhalfer, Dprizrak1, Lipton, Amphilohiy, yuryol

Блоки 10 года 2 мес. назад #73361

  • Amphilohiy
  • Amphilohiy аватар
  • Вне сайта
  • Светлый дракон
  • Сообщений: 547
  • Спасибо получено: 666
  • ОраторПобедитель Сбитой кодировки2 место ГотвПрограммист RubyУчитель
Небольшое дополнение, хоть и не очень в тему. В Эйсе схема работы между Сценами и окнами заключается в событиях, которые именуют handler. Суть проста - окна отлавливают события по апдейту, и вызывают подходящий handler в виде переданного им метода (как правило метода сцены). Т.к. сцена все окна держит в своему пространстве, то и переданные методы тоже могут спокойно работать с другими окнами. В итоге окно может влиять на другое окно, хоть по сути в классе ничего подобного не описано. Но это о пользе биндингов в мукере.
Я верю, что иногда компьютер сбоит, и он выдает неожиданные результаты, но остальные 100% случаев это чья-то криворукость.
Администратор запретил публиковать записи гостям.
За этот пост поблагодарили: Iren_Rin
Время создания страницы: 0.825 секунд