Блоки и выходы в рубине

275

Я пытаюсь понять блоки yieldи как они работают в Ruby.

Как yieldиспользуется? Многие приложения Rails, на которые я смотрел, yieldстранным образом используются.

Может кто-нибудь объяснить мне или показать мне, куда идти, чтобы понять их?

Мэтт Элхотиби
источник
2
Возможно, вас заинтересует ответ на характеристику доходности Руби в отношении информатики . Хотя этот вопрос несколько отличается от вашего, он может пролить свет на этот вопрос.
Кен Блум

Ответы:

393

Да, сначала это немного озадачивает.

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

Когда метод ожидает блок, он вызывает его, вызывая yieldфункцию.

Это очень удобно, например, для перебора списка или для создания собственного алгоритма.

Возьмите следующий пример:

Я собираюсь определить Personкласс, инициализированный именем, и предоставить do_with_nameметод, который при вызове просто передает nameатрибут полученному блоку.

class Person 
    def initialize( name ) 
         @name = name
    end

    def do_with_name 
        yield( @name ) 
    end
end

Это позволило бы нам вызвать этот метод и передать произвольный кодовый блок.

Например, чтобы напечатать имя, мы бы сделали:

person = Person.new("Oscar")

#invoking the method passing a block
person.do_with_name do |name|
    puts "Hey, his name is #{name}"
end

Будет печатать:

Hey, his name is Oscar

Обратите внимание, что блок получает в качестве параметра переменную с именем name(NB. Вы можете вызывать эту переменную как угодно, но имеет смысл ее вызывать name). Когда код вызывает, yieldон заполняет этот параметр значением @name.

yield( @name )

Мы могли бы предоставить другой блок для выполнения другого действия. Например, измените имя:

#variable to hold the name reversed
reversed_name = ""

#invoke the method passing a different block
person.do_with_name do |name| 
    reversed_name = name.reverse
end

puts reversed_name

=> "racsO"

Мы использовали точно такой же метод ( do_with_name) - это просто другой блок.

Этот пример тривиален. Более интересные способы фильтрации всех элементов массива:

 days = ["monday", "tuesday", "wednesday", "thursday", "friday"]  

 # select those which start with 't' 
 days.select do | item |
     item.match /^t/
 end

=> ["tuesday", "thursday"]

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

 days.sort do |x,y|
    x.size <=> y.size
 end

=> ["monday", "friday", "tuesday", "thursday", "wednesday"]

Я надеюсь, что это поможет вам лучше понять это.

Кстати, если блок не является обязательным, вы должны назвать его следующим образом:

yield(value) if block_given?

Если не является обязательным, просто вызовите его.

РЕДАКТИРОВАТЬ

@hmak создал repl.it для этих примеров: https://repl.it/@makstaks/blocksandyieldsrubyexample

OscarRyz
источник
как это печатать, racsOесли the_name = ""
Паритош Пиплевар
2
Извините, это имя переменной экземпляра, инициализированной "Oscar" (не очень понятно в ответе)
OscarRyz
Как насчет кода, как это? person.do_with_name {|string| yield string, something_else }
f.ardelian
7
Так что в терминах Javascripty это стандартизированный способ передачи обратного вызова для данного метода и его вызова. Спасибо за объяснение!
Ицньютон
В более общем смысле - это рубиновый «улучшенный» синтаксический сахар для паттерна Стратегия. потому что типичное использование заключается в предоставлении кода, чтобы сделать что-то в контексте другой операции. Но улучшения в ruby ​​открывают путь к таким классным вещам, как написание DSL с использованием блоков для передачи контекста
Роман Булгаков
25

В Ruby методы могут проверять, были ли они вызваны таким образом, чтобы блок был предоставлен в дополнение к обычным аргументам. Обычно это делается с помощью block_given?метода, но вы также можете ссылаться на блок как на явный Proc, добавив префикс ampersand ( &) перед окончательным именем аргумента.

Если метод вызывается с блоком, то метод может yieldуправлять блоком (вызывать блок) с некоторыми аргументами, если это необходимо. Рассмотрим этот пример метода, который демонстрирует:

def foo(x)
  puts "OK: called as foo(#{x.inspect})"
  yield("A gift from foo!") if block_given?
end

foo(10)
# OK: called as foo(10)
foo(123) {|y| puts "BLOCK: #{y} How nice =)"}
# OK: called as foo(123)
# BLOCK: A gift from foo! How nice =)

Или, используя специальный синтаксис аргумента блока:

def bar(x, &block)
  puts "OK: called as bar(#{x.inspect})"
  block.call("A gift from bar!") if block
end

bar(10)
# OK: called as bar(10)
bar(123) {|y| puts "BLOCK: #{y} How nice =)"}
# OK: called as bar(123)
# BLOCK: A gift from bar! How nice =)
maerics
источник
Полезно знать разные способы срабатывания блока.
LPing
22

Вполне возможно, что кто-то даст здесь действительно подробный ответ, но я всегда находил этот пост Роберта Сосински отличным объяснением тонкостей между блоками, процессами и лямбдами.

Я должен добавить, что я считаю, что пост, на который я ссылаюсь, относится к ruby ​​1.8. Некоторые вещи изменились в ruby ​​1.9, например, переменные блока являются локальными для блока. В 1.8 вы получите что-то вроде следующего:

>> a = "Hello"
=> "Hello"
>> 1.times { |a| a = "Goodbye" }
=> 1
>> a
=> "Goodbye"

Тогда как 1.9 даст вам:

>> a = "Hello"
=> "Hello"
>> 1.times { |a| a = "Goodbye" }
=> 1
>> a
=> "Hello"

У меня нет 1.9 на этой машине, поэтому выше может быть ошибка.

theIV
источник
Отличное описание в этой статье, мне потребовались месяцы, чтобы понять, что все это
само
Я согласен. Я не думаю, что знал половину объясненного материала, пока не прочитал.
2010 года
Обновленная ссылка теперь тоже 404. Вот ссылка на Wayback Machine .
Кленвелл
@klenwell спасибо за внимание, я снова обновил ссылку.
TheIV
13

Я хотел бы добавить, почему вы так поступили, к и без того отличным ответам.

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

public class FileInput {

  public static void main(String[] args) {

    File file = new File("C:\\MyFile.txt");
    FileInputStream fis = null;
    BufferedInputStream bis = null;
    DataInputStream dis = null;

    try {
      fis = new FileInputStream(file);

      // Here BufferedInputStream is added for fast reading.
      bis = new BufferedInputStream(fis);
      dis = new DataInputStream(bis);

      // dis.available() returns 0 if the file does not have more lines.
      while (dis.available() != 0) {

      // this statement reads the line from the file and print it to
        // the console.
        System.out.println(dis.readLine());
      }

      // dispose all the resources after using them.
      fis.close();
      bis.close();
      dis.close();

    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

Игнорируя всю цепочку потоковых вещей, Идея заключается в следующем

  1. Инициализируйте ресурс, который необходимо очистить
  2. использовать ресурс
  3. не забудьте убрать это

Вот как ты это делаешь в ruby

File.open("readfile.rb", "r") do |infile|
    while (line = infile.gets)
        puts "#{counter}: #{line}"
        counter = counter + 1
    end
end

Дико отличается. Ломая это

  1. скажите классу File, как инициализировать ресурс
  2. скажите классу файла, что с ним делать
  3. смеяться над парнями из Java, которые все еще печатают ;-)

Здесь, вместо обработки первого и второго шагов, вы в основном делегируете это другому классу. Как вы можете видеть, это значительно снижает объем кода, который вам нужно написать, что облегчает чтение и снижает вероятность утечек памяти или блокировок файлов.

Теперь не то, чтобы вы не могли делать что-то подобное в Java, на самом деле, люди делают это уже десятилетия. Это называется Стратегия паттерном . Разница в том, что без блоков, для чего-то простого, такого как пример файла, стратегия становится излишней из-за количества классов и методов, которые вам нужно написать. С блоками это такой простой и элегантный способ сделать это, что не имеет никакого смысла НЕ структурировать ваш код таким образом.

Это не единственный способ использования блоков, но другие (например, шаблон Builder, который вы можете увидеть в API form_for в rails) достаточно похожи, чтобы было понятно, что происходит, когда вы обернетесь вокруг этого. Когда вы видите блоки, обычно безопасно предположить, что вызов метода - это то, что вы хотите сделать, а блок описывает, как вы хотите это сделать.

Мэтт Бриггс
источник
5
Давайте немного упростим это: File.readlines("readfile.rb").each_with_index do |line, index| puts "#{index + 1}: #{line}" endи еще больше смеемся над парнями из Java.
Майкл Хэмптон
1
@MichaelHampton, смейтесь после того, как прочитаете файл длиной в пару гигабайт.
Акостадинов
@akostadinov Нет ... это заставляет меня плакать!
Майкл Хэмптон
3
@MichaelHampton Или, еще лучше: IO.foreach('readfile.rb').each_with_index { |line, index| puts "#{index}: #{line}" }(плюс нет проблем с памятью)
Фонд Моника иск
12

Я нашел эту статью очень полезной. В частности, следующий пример:

#!/usr/bin/ruby

def test
  yield 5
  puts "You are in the method test"
  yield 100
end

test {|i| puts "You are in the block #{i}"}

test do |i|
    puts "You are in the block #{i}"
end

который должен дать следующий вывод:

You are in the block 5
You are in the method test
You are in the block 100
You are in the block 5
You are in the method test
You are in the block 100

Таким образом, по сути каждый раз, когда делается вызов yieldruby, будет запускаться код в doблоке или внутри {}. Если указан параметр, yieldто он будет предоставлен в качестве параметра для doблока.

Для меня это был первый раз, когда я действительно понял, что doделают блоки. Это в основном способ для функции предоставить доступ к внутренним структурам данных, будь то для итерации или для конфигурации функции.

Поэтому, когда в рельсах вы пишете следующее:

respond_to do |format|
  format.html { render template: "my/view", layout: 'my_layout' }
end

Это запустит respond_toфункцию, которая выдает doблок с (внутренним) formatпараметром. Затем вы вызываете .htmlфункцию для этой внутренней переменной, которая в свою очередь выдает блок кода для запуска renderкоманды. Обратите внимание, что .htmlвыдаст только если это запрошенный формат файла. (техническая специфика: эти функции на самом деле используют block.callне так, yieldкак вы можете видеть из источника, но функциональность по сути та же самая, см. этот вопрос для обсуждения.) Это обеспечивает способ для функции выполнить некоторую инициализацию, а затем принять ввод из вызывающего кода и затем продолжите обработку, если требуется.

Или, другими словами, это похоже на функцию, принимающую анонимную функцию в качестве аргумента и вызывающую ее в javascript.

zelanix
источник
8

В Ruby блок - это, по сути, кусок кода, который может быть передан и выполнен любым методом. Блоки всегда используются с методами, которые обычно подают к ним данные (в качестве аргументов).

Блоки широко используются в гемах Ruby (включая Rails) и в хорошо написанном коде Ruby. Они не являются объектами, поэтому не могут быть присвоены переменным.

Основной синтаксис

Блок - это фрагмент кода, заключенный в {} или do..end. По соглашению синтаксис фигурных скобок должен использоваться для однострочных блоков, а синтаксис do..end должен использоваться для многострочных блоков.

{ # This is a single line block }

do
  # This is a multi-line block
end 

Любой метод может получить блок в качестве неявного аргумента. Блок выполняется оператором yield в методе. Основной синтаксис:

def meditate
  print "Today we will practice zazen"
  yield # This indicates the method is expecting a block
end 

# We are passing a block as an argument to the meditate method
meditate { print " for 40 minutes." }

Output:
Today we will practice zazen for 40 minutes.

Когда оператор yield достигнут, метод meditate передает управление блоку, код внутри блока выполняется и управление возвращается методу, который возобновляет выполнение сразу после оператора yield.

Когда метод содержит оператор yield, он ожидает получить блок во время вызова. Если блок не указан, то исключение будет выдано после достижения оператора yield. Мы можем сделать блок необязательным и избежать исключения:

def meditate
  puts "Today we will practice zazen."
  yield if block_given? 
end meditate

Output:
Today we will practice zazen. 

Невозможно передать несколько блоков в метод. Каждый метод может получить только один блок.

Смотрите больше на: http://www.zenruby.info/2016/04/introduction-to-blocks-in-ruby.html


источник
Это единственный ответ, который действительно помогает мне понять, что такое блок и доходность и как их использовать.
Эрик Ван
5

Я иногда использую «yield» так:

def add_to_http
   "http://#{yield}"
end

puts add_to_http { "www.example.com" }
puts add_to_http { "www.victim.com"}
Самет Сазак
источник
Хорошо, но почему ? Есть много причин, например, одна Loggerне должна выполнять какую-либо задачу, если пользователю это не нужно. Вы должны объяснить свои, хотя ...
Ulysse BN
4

Проще говоря, выходы позволяют создаваемому методу принимать и вызывать блоки. Ключевое слово yield - это место, где будет выполняться «материал» в блоке.

ntarpey
источник
1

Есть два момента, которые я хочу сделать о доходности здесь. Во-первых, хотя многие ответы здесь говорят о различных способах передачи блока методу, который использует yield, давайте также поговорим о потоке управления. Это особенно актуально, так как вы можете выдавать НЕСКОЛЬКО раз на блок. Давайте посмотрим на пример:

class Fruit
  attr_accessor :kinds

  def initialize 
    @kinds = %w(orange apple pear banana)
  end

  def each 
    puts 'inside each'
    3.times { yield (@kinds.tap {|kinds| puts "selecting from #{kinds}"} ).sample }
  end  
end

f = Fruit.new
f.each do |kind|
  puts 'inside block'
end    

=> inside each
=> selecting from ["orange", "apple", "pear", "banana"]
=> inside block
=> selecting from ["orange", "apple", "pear", "banana"]
=> inside block
=> selecting from ["orange", "apple", "pear", "banana"]
=> inside block

Когда каждый метод вызывается, он выполняется построчно. Теперь, когда мы дойдем до блока 3.times, этот блок будет вызываться 3 раза. Каждый раз, когда это вызывает доходность. Этот выход связан с блоком, связанным с методом, который вызвал каждый метод. Важно отметить, что каждый раз, когда yield вызывается, он возвращает управление обратно в блок каждого метода в клиентском коде. По завершении выполнения блока он возвращается обратно в блок 3.times. И это происходит 3 раза. Таким образом, этот блок в клиентском коде вызывается 3 раза, поскольку yield явно вызывается 3 раза.

Мое второе замечание касается enum_for и yield. enum_for создает экземпляр класса Enumerator, и этот объект Enumerator также отвечает на yield.

class Fruit
  def initialize
    @kinds = %w(orange apple)
  end

  def kinds
    yield @kinds.shift
    yield @kinds.shift
  end
end

f = Fruit.new
enum = f.to_enum(:kinds)
enum.next
 => "orange" 
enum.next
 => "apple" 

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

Есть интересная новость в отношении enum_for. Документация онлайн заявляет следующее:

enum_for(method = :each, *args)  enum
Creates a new Enumerator which will enumerate by calling method on obj, passing args if any.

str = "xyz"
enum = str.enum_for(:each_byte)
enum.each { |b| puts b }    
# => 120
# => 121
# => 122

Если вы не укажете символ в качестве аргумента enum_for, ruby ​​подключит перечислитель к каждому методу получателя. Некоторые классы не имеют метода each, как класс String.

str = "I like fruit"
enum = str.to_enum
enum.next
=> NoMethodError: undefined method `each' for "I like fruit":String

Таким образом, в случае некоторых объектов, вызываемых с помощью enum_for, вы должны четко указать, каким будет ваш метод перечисления.

Донато
источник
0

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

Def Up(anarg)
  yield(anarg)
end

Вы можете создать метод «Вверх», которому присваивается один аргумент. Теперь вы можете назначить этот аргумент yield, который будет вызывать и выполнять связанный блок. Вы можете назначить блок после списка параметров.

Up("Here is a string"){|x| x.reverse!; puts(x)}

Когда метод Up вызывает yield с аргументом, он передается в переменную блока для обработки запроса.

gkstr1
источник