Я пытаюсь понять блоки yield
и как они работают в Ruby.
Как yield
используется? Многие приложения Rails, на которые я смотрел, yield
странным образом используются.
Может кто-нибудь объяснить мне или показать мне, куда идти, чтобы понять их?
Я пытаюсь понять блоки yield
и как они работают в Ruby.
Как yield
используется? Многие приложения Rails, на которые я смотрел, yield
странным образом используются.
Может кто-нибудь объяснить мне или показать мне, куда идти, чтобы понять их?
Ответы:
Да, сначала это немного озадачивает.
В 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
racsO
если the_name = ""
"Oscar"
(не очень понятно в ответе)
person.do_with_name {|string| yield string, something_else }
В 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 =)
Вполне возможно, что кто-то даст здесь действительно подробный ответ, но я всегда находил этот пост Роберта Сосински отличным объяснением тонкостей между блоками, процессами и лямбдами.
Я должен добавить, что я считаю, что пост, на который я ссылаюсь, относится к 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 на этой машине, поэтому выше может быть ошибка.
Я хотел бы добавить, почему вы так поступили, к и без того отличным ответам.
Не знаю, на каком языке вы говорите, но если предположить, что это статический язык, то это будет выглядеть знакомо. Вот как вы читаете файл в 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();
}
}
}
Игнорируя всю цепочку потоковых вещей, Идея заключается в следующем
Вот как ты это делаешь в ruby
File.open("readfile.rb", "r") do |infile|
while (line = infile.gets)
puts "#{counter}: #{line}"
counter = counter + 1
end
end
Дико отличается. Ломая это
Здесь, вместо обработки первого и второго шагов, вы в основном делегируете это другому классу. Как вы можете видеть, это значительно снижает объем кода, который вам нужно написать, что облегчает чтение и снижает вероятность утечек памяти или блокировок файлов.
Теперь не то, чтобы вы не могли делать что-то подобное в Java, на самом деле, люди делают это уже десятилетия. Это называется Стратегия паттерном . Разница в том, что без блоков, для чего-то простого, такого как пример файла, стратегия становится излишней из-за количества классов и методов, которые вам нужно написать. С блоками это такой простой и элегантный способ сделать это, что не имеет никакого смысла НЕ структурировать ваш код таким образом.
Это не единственный способ использования блоков, но другие (например, шаблон Builder, который вы можете увидеть в API form_for в rails) достаточно похожи, чтобы было понятно, что происходит, когда вы обернетесь вокруг этого. Когда вы видите блоки, обычно безопасно предположить, что вызов метода - это то, что вы хотите сделать, а блок описывает, как вы хотите это сделать.
File.readlines("readfile.rb").each_with_index do |line, index| puts "#{index + 1}: #{line}" end
и еще больше смеемся над парнями из Java.
IO.foreach('readfile.rb').each_with_index { |line, index| puts "#{index}: #{line}" }
(плюс нет проблем с памятью)
Я нашел эту статью очень полезной. В частности, следующий пример:
#!/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
Таким образом, по сути каждый раз, когда делается вызов yield
ruby, будет запускаться код в 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.
В 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
Я иногда использую «yield» так:
def add_to_http
"http://#{yield}"
end
puts add_to_http { "www.example.com" }
puts add_to_http { "www.victim.com"}
Logger
не должна выполнять какую-либо задачу, если пользователю это не нужно. Вы должны объяснить свои, хотя ...
Есть два момента, которые я хочу сделать о доходности здесь. Во-первых, хотя многие ответы здесь говорят о различных способах передачи блока методу, который использует 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, вы должны четко указать, каким будет ваш метод перечисления.
Выход можно использовать как безымянный блок для возврата значения в методе. Рассмотрим следующий код:
Def Up(anarg)
yield(anarg)
end
Вы можете создать метод «Вверх», которому присваивается один аргумент. Теперь вы можете назначить этот аргумент yield, который будет вызывать и выполнять связанный блок. Вы можете назначить блок после списка параметров.
Up("Here is a string"){|x| x.reverse!; puts(x)}
Когда метод Up вызывает yield с аргументом, он передается в переменную блока для обработки запроса.