Блоки та врожайність у Ruby


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"]

Я сподіваюся, що це допоможе вам зрозуміти це краще.

BTW, якщо блок є необов’язковим, слід назвати його так:

yield(value) if block_given?

Якщо це не обов'язково, просто запустіть його.

EDIT

@hmak створив repl.it для цих прикладів: https://repl.it/@makstaks/blocksandyieldsrubyexample


як він друкує, racsOякщо the_name = ""
Paritosh Piplewar

2
На жаль, ім'я є змінною екземпляра, ініціалізованою з "Oscar" (не дуже зрозуміло у відповіді)
OscarRyz

Що з таким кодом? person.do_with_name {|string| yield string, something_else }
f.ardelian

7
Отже, з точки зору Javascripty, це стандартизований спосіб передачі зворотного дзвінка до заданого методу та виклику його. Дякую за пояснення!
yitznewton

Більш загальним способом - блоком є ​​"посилений" синтаксичний цукор для рубіну для шаблону стратегії. тому що типовим використанням є надання коду, щоб зробити щось у контексті інших операцій. Але покращення рубіну відкривають шлях до таких цікавих речей, як написання 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 =)

Добре знати різні способи спрацювання блоку.
LPing

22

Цілком можливо, що хтось дасть справді детальну відповідь тут, але я завжди вважав, що цей пост від Роберта Сосінського є чудовим поясненням тонкощів між блоками, документами та лямбдами.

Варто додати, що я вважаю, що посада, з якою я посилаюсь, є специфічною для ruby ​​1.8. Деякі речі змінилися в рубіні 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 на цій машині, тому вищевказане може мати помилку в ньому.


Чудовий опис цієї статті, мені знадобилося кілька місяців, щоб зрозуміти, що все самостійно =)
maerics

Я згоден. Я не думаю, що я знав половину пояснених матеріалів, поки не прочитав.
theIV

Оновлене посилання зараз також 404. Ось посилання Wayback Machine .
klenwell

@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. обов'язково почистіть його

Так ви робите це в рубіні

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, який ви можете побачити у form_for api в рейках) досить схожі, що повинно бути очевидним, що відбувається, коли ви обернете голову навколо цього. Коли ви бачите блоки, звичайно безпечно припустити, що виклик методу - це те, що ви хочете зробити, і блок описує, як ви хочете це зробити.


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

Тому по суті кожен раз, коли здійснюється дзвінок на yieldрубін, запускається код у 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.


8

У Ruby блок - це в основному фрагмент коду, який можна передавати та виконувати будь-яким методом. Блоки завжди використовуються з методами, які зазвичай подають їм дані (як аргументи).

Блоки широко використовуються в дорогоцінних каменях Ruby (включаючи Rails) і в добре написаному коді Ruby. Вони не є об'єктами, тому їх не можна присвоювати змінним.

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

Блок - це фрагмент коду, доданий {} або do..end. За умовою, фігурний синтаксис дужок повинен використовуватися для однорядних блоків, а синтаксис do..end - для багаторядкових блоків.

{ # This is a single line block }

do
  # This is a multi-line block
end 

Будь-який метод може отримати блок як неявний аргумент. Блок виконується оператором дохідності в межах методу. Основний синтаксис:

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.

Коли дохід твердження досягнуто, метод медитації передає контроль блоку, виконується код всередині блоку і контроль повертається до методу, який відновлює виконання відразу після заяви про вихід.

Коли метод містить заяву про вихід, він очікує отримання блоку під час виклику. Якщо блок не надається, виняток буде викинуто, коли буде досягнуто заяву про врожайність. Ми можемо зробити блок необов’язковим і уникнути підвищення винятку:

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

Я іноді використовую "урожай" так:

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

Простіше кажучи, дозволити методу, який ви створюєте, приймати і викликати блоки. Ключове слово врожайності конкретно - це місце, де буде виконуватися "матеріал" у блоці.


1

Тут я хочу зауважити два моменти. По-перше, хоча тут багато відповідей говорять про різні способи передачі блоку методу, який використовує вихід, давайте також поговоримо про контрольний потік. Це особливо актуально, оскільки ви можете віддавати МНОГО кратних разів до блоку. Давайте подивимось на приклад:

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-х разів, цей блок буде викликаний 3 рази. Кожен раз, коли він викликає врожайність. Цей вихід пов'язаний з блоком, пов'язаним із методом, який викликав кожен метод. Важливо зауважити, що кожного разу, коли викликається вихід, він повертає контроль назад до блоку кожного методу в коді клієнта. Після завершення виконання блоку він повертається до блоку 3 рази. І це трапляється 3 рази. Таким чином, цей блок у коді клієнта викликається 3 окремих випадків, оскільки вихід явно називається 3 окремими раз

Мій другий пункт про enum_for та врожайність. enum_for створює клас Enumerator, і цей об’єкт Enumerator також відповідає на вихід.

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" 

Тож зауважте щоразу, коли ми запускаємо види із зовнішнім ітератором, він буде викликати вихід лише один раз. Наступного разу, коли ми його назвемо, це призведе до наступного врожаю тощо.

Є цікавий примружок щодо 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 ​​підключить перечислювач до кожного методу одержувача. У деяких класах немає кожного методу, як клас 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

Ви можете створити метод "Вгору", якому призначений один аргумент. Тепер ви можете призначити цей аргумент для отримання, який буде викликати та виконувати пов'язаний блок. Ви можете призначити блок після списку параметрів.

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

Коли метод Up викликає дохід, з аргументом, він передається до змінної блоку для обробки запиту.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.