Bloki i plony w Ruby


275

Próbuję zrozumieć bloki i yieldich działanie w Ruby.

Jak yieldstosować? Wiele aplikacji Rails, na które patrzyłem, używało yieldw dziwny sposób.

Czy ktoś może mi wyjaśnić lub pokazać, gdzie mam je zrozumieć?


2
Możesz być zainteresowany odpowiedzią na funkcję Ruby dotyczącą wydajności w odniesieniu do informatyki . Chociaż jest to nieco inne pytanie niż twoje, może rzucić nieco światła na tę sprawę.
— Ken Bloom

Odpowiedzi:


393

Tak, na początku jest to trochę zagadkowe.

W języku Ruby metody mogą odbierać blok kodu w celu wykonania dowolnych segmentów kodu.

Gdy metoda oczekuje bloku, wywołuje go, wywołując yieldfunkcję.

Jest to bardzo przydatne, na przykład, do iteracji po liście lub zapewnienia niestandardowego algorytmu.

Weź następujący przykład:

Mam zamiar zdefiniować Personklasę zainicjowaną nazwą i podać do_with_namemetodę, która po wywołaniu przekaże nameatrybut do otrzymanego bloku.

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

    def do_with_name 
        yield( @name ) 
    end
end

Pozwoliłoby nam to wywołać tę metodę i przekazać dowolny blok kodu.

Na przykład, aby wydrukować nazwę, zrobilibyśmy:

person = Person.new("Oscar")

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

Wydrukowałby:

Hey, his name is Oscar

Zauważ, że blok otrzymuje jako parametr zmienną o nazwie name(uwaga: możesz wywoływać tę zmienną w dowolny sposób, ale warto ją wywołać name). Po wywołaniu kodu yieldwypełnia ten parametr wartością @name.

yield( @name )

Możemy zapewnić kolejny blok, aby wykonać inną akcję. Na przykład odwróć nazwę:

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

Zastosowaliśmy dokładnie tę samą metodę ( do_with_name) - to tylko inny blok.

Ten przykład jest trywialny. Bardziej interesujące zastosowania to filtrowanie wszystkich elementów w tablicy:

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

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

=> ["tuesday", "thursday"]

Lub możemy również dostarczyć niestandardowy algorytm sortowania, na przykład na podstawie rozmiaru ciągu:

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

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

Mam nadzieję, że to pomoże ci lepiej to zrozumieć.

BTW, jeśli blok jest opcjonalny, powinieneś go nazwać:

yield(value) if block_given?

Jeśli nie jest opcjonalny, po prostu go wywołaj.

EDYTOWAĆ

@hmak utworzył repl.it dla tych przykładów: https://repl.it/@makstaks/blocksandyieldsrubyexample


jak to drukuje, racsOjeśli the_name = ""
— Paritosh Piplewar

2
Niestety, nazwa jest zmienną instancji zainicjowaną za pomocą "Oscar" (nie jest bardzo jasne w odpowiedzi)
— OscarRyz

Co z takim kodem? person.do_with_name {|string| yield string, something_else }
— f .ardelian

7
Tak więc, w kategoriach Javascripty, jest to znormalizowany sposób przekazywania wywołania zwrotnego do danej metody i wywoływania jej. Dziękuję za wyjaśnienie!
— yitznewton,

Mówiąc bardziej ogólnie - blok to rubinowy „ulepszony” cukier składniowy dla wzorca strategii. ponieważ typowym zastosowaniem jest dostarczenie kodu do zrobienia czegoś w kontekście innej operacji. Ale ulepszenia ruby ​​otwierają drogę do tak fajnych rzeczy, jak pisanie DSL-ów za pomocą bloku do przekazywania kontekstu
— Roman Bułhakow

25

W języku Ruby metody mogą sprawdzić, czy zostały wywołane w taki sposób, że oprócz zwykłych argumentów podano blok. Zwykle odbywa się to za pomocą block_given?metody, ale można również odwołać się do bloku jako jawnego Proc, poprzedzając znak ampersand ( &) przed ostateczną nazwą argumentu.

Jeśli metoda jest wywoływana z blokiem, metoda może yieldsterować blokiem (wywołać blok) z pewnymi argumentami, jeśli to konieczne. Rozważ tę przykładową metodę, która pokazuje:

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 =)

Lub używając specjalnej składni argumentu blokowego:

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 =)

Dobrze znać różne sposoby wyzwalania bloku.
— LPing

22

Całkiem możliwe, że ktoś udzieli tu naprawdę szczegółowej odpowiedzi, ale zawsze uważałem ten post Roberta Sosińskiego za doskonałe wyjaśnienie subtelności między blokami, procami i lambdami.

Powinienem dodać, że moim zdaniem post, do którego prowadzę link, jest specyficzny dla ruby ​​1.8. Niektóre rzeczy uległy zmianie w Ruby 1.9, na przykład zmienne blokowe są lokalne dla bloku. W wersji 1.8 otrzymasz coś takiego:

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

Podczas gdy 1.9 zapewni Ci:

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

Nie mam 1.9 na tym komputerze, więc powyższe może zawierać błąd.


Świetny opis w tym artykule, zajęło mi kilka miesięcy, aby dowiedzieć się, że to wszystko na własną rękę =)
— maerics

Zgadzam się. Chyba nie znałem połowy tego wszystkiego, dopóki nie przeczytałem.
— theIV

Zaktualizowany link to teraz także 404. Oto link Wayback Machine .
— klenwell

@klenwell dzięki za zgłoszenie, zaktualizowałem link ponownie.
— theIV

13

Chciałem w pewnym sensie dodać, dlaczego tak robisz w odpowiedzi na i tak świetne odpowiedzi.

Nie mam pojęcia, z jakiego języka pochodzisz, ale zakładając, że jest to język statyczny, tego rodzaju rzeczy będą wyglądały znajomo. W ten sposób czytasz plik w Javie

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();
    }
  }
}

Ignorując cały łańcuch związany z strumieniem, Chodzi o to

  1. Zainicjuj zasób, który należy oczyścić
  2. użyj zasobu
  3. upewnij się, że to posprzątasz

Tak to robisz w rubinie

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

Zupełnie inaczej. Rozbicie tego

  1. powiedz klasie File, jak zainicjować zasób
  2. powiedz klasie plików, co z nią zrobić
  3. śmiej się z gości z Java, którzy wciąż piszą ;-)

Tutaj, zamiast zajmować się krokiem pierwszym i drugim, w zasadzie delegujesz to do innej klasy. Jak widać, radykalnie zmniejsza to ilość kodu, który musisz napisać, co ułatwia czytanie i zmniejsza ryzyko wycieków pamięci lub braku możliwości usunięcia blokad plików.

Teraz nie jest tak, że nie można zrobić czegoś podobnego w java, w rzeczywistości ludzie robią to od dziesięcioleci. To się nazywa wzór strategii . Różnica polega na tym, że bez bloków, w przypadku czegoś tak prostego jak przykład pliku, strategia staje się nadmierna z powodu ilości klas i metod, które musisz napisać. W przypadku bloków jest to tak prosty i elegancki sposób, aby to zrobić, że NIE ma sensu NIE strukturyzować kodu w ten sposób.

Nie jest to jedyny sposób, w jaki używane są bloki, ale inne (jak wzorzec Konstruktora, który można zobaczyć w form_for api w szynach) są na tyle podobne, że powinno być oczywiste, co się dzieje, gdy obejdziesz się tym. Kiedy widzisz bloki, zwykle bezpiecznie jest założyć, że wywołanie metody jest tym, co chcesz zrobić, a blok opisuje, jak chcesz to zrobić.


5
Uprośćmy to trochę: File.readlines("readfile.rb").each_with_index do |line, index| puts "#{index + 1}: #{line}" endi śmiejmy się jeszcze mocniej z ludzi z Java.
— Michael Hampton

1
@MichaelHampton, śmiej się po przeczytaniu pliku o długości kilku gigabajtów.
— akostadinov

@akostadinov Nie ... to sprawia, że ​​chcę płakać!
— Michael Hampton

3
@MichaelHampton Lub, jeszcze lepiej: IO.foreach('readfile.rb').each_with_index { |line, index| puts "#{index}: #{line}" }(plus brak problemów z pamięcią)
— pozew Fund Moniki

12

Uważam, że ten artykuł jest bardzo przydatny. W szczególności następujący przykład:

#!/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

co powinno dać następujące dane wyjściowe:

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

Zasadniczo za każdym razem, gdy zostanie wykonane wywołanie yieldruby, uruchomi kod w dobloku lub w środku {}. Jeśli podano parametr, yieldto zostanie on podany jako parametr do dobloku.

Dla mnie to był pierwszy raz, kiedy naprawdę zrozumiałem, co dorobią bloki. Zasadniczo jest to sposób, w jaki funkcja umożliwia dostęp do wewnętrznych struktur danych, zarówno w celu iteracji, jak i konfiguracji funkcji.

Kiedy więc w szynach piszesz:

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

Uruchomi to respond_tofunkcję, która zwróci doblok z parametrem (wewnętrznym) format. Następnie wywołujesz .htmlfunkcję dla tej zmiennej wewnętrznej, co z kolei daje blok kodu do uruchomienia renderpolecenia. Pamiętaj, że .htmlprzyniesie to tylko wtedy, gdy jest to wymagany format pliku. (technicznie: te funkcje faktycznie block.callnie używają tego, yieldco widać ze źródła, ale funkcjonalność jest zasadniczo taka sama, zobacz to pytanie w celu omówienia.) Zapewnia to sposób, aby funkcja wykonała pewną inicjalizację, a następnie pobierała dane wejściowe z kodu wywołującego i następnie w razie potrzeby kontynuować przetwarzanie.

Innymi słowy, jest podobny do funkcji przyjmującej anonimową funkcję jako argument, a następnie wywołującej ją w javascript.


8

W Ruby blok jest zasadniczo fragmentem kodu, który można przekazać i wykonać dowolną metodą. Bloki są zawsze używane z metodami, które zwykle przekazują im dane (jako argumenty).

Bloki są szeroko stosowane w klejnotach Ruby (w tym Railsach) oraz w dobrze napisanym kodzie Ruby. Nie są obiektami, dlatego nie można ich przypisywać do zmiennych.

Podstawowa składnia

Blok to fragment kodu ujęty w {} lub do..end. Zgodnie z konwencją w przypadku bloków jednowierszowych należy stosować składnię nawiasów klamrowych, aw przypadku bloków wieloliniowych należy stosować składnię do..end.

{ # This is a single line block }

do
  # This is a multi-line block
end 

Każda metoda może otrzymać blok jako domyślny argument. Blok jest wykonywany przez instrukcję return w ramach metody. Podstawowa składnia to:

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.

Po osiągnięciu instrukcji fedrowania metoda medytacji daje kontrolę nad blokiem, kod w bloku jest wykonywany, a kontrola jest zwracana do metody, która wznawia wykonywanie natychmiast po instrukcji fed.

Gdy metoda zawiera instrukcję dochodu, oczekuje otrzymania bloku w czasie wywołania. Jeśli blok nie zostanie podany, wyjątek zostanie zgłoszony po osiągnięciu instrukcji dochodu. Możemy uczynić blok opcjonalnym i uniknąć generowania wyjątku:

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

Output:
Today we will practice zazen. 

Nie można przekazać wielu bloków do metody. Każda metoda może otrzymać tylko jeden blok.

Zobacz więcej na: http://www.zenruby.info/2016/04/introduction-to-blocks-in-ruby.html


To jest (jedyna) odpowiedź, która naprawdę pozwala mi zrozumieć, co jest blokiem i wydajnością oraz jak z nich korzystać.
— Eric Wang

5

Czasami używam „wydajności” w ten sposób:

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

puts add_to_http { "www.example.com" }
puts add_to_http { "www.victim.com"}

Dobrze ale dlaczego ? Istnieje wiele powodów, takich jak to, Loggerże użytkownik nie musi wykonywać jakiegoś zadania, jeśli użytkownik nie musi tego robić. Powinieneś jednak wyjaśnić swoje ...
— Ulysse BN

4

Mówiąc prościej, Yields pozwala, aby tworzona metoda przyjmowała i wywoływała bloki. Słowo kluczowe wydajności w szczególności jest miejscem, w którym zostaną wykonane „rzeczy” w bloku.


1

Chciałbym tu wspomnieć o dwóch kwestiach dotyczących wydajności. Po pierwsze, podczas gdy wiele odpowiedzi tutaj mówi o różnych sposobach przekazania bloku do metody wykorzystującej wydajność, porozmawiajmy również o przepływie sterowania. Jest to szczególnie istotne, ponieważ możesz dać WIELU CZASÓW blokowi. Spójrzmy na przykład:

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

Po wywołaniu każdej metody wykonuje się linia po linii. Teraz, gdy przejdziemy do bloku 3. razy, ten blok zostanie wywołany 3 razy. Za każdym razem, gdy wywołuje plon. Ta wydajność jest powiązana z blokiem związanym z metodą, która wywołała każdą metodę. Należy zauważyć, że za każdym razem, gdy wywoływana jest wydajność, zwraca kontrolę z powrotem do bloku każdej metody w kodzie klienta. Po zakończeniu wykonywania blok wraca do bloku 3. razy. I dzieje się to 3 razy. Tak więc blok w kodzie klienta jest wywoływany 3 odrębnymi okazjami, ponieważ fedrunek jest jawnie nazywany 3 oddzielnymi czasami.

Moja druga uwaga dotyczy enum_for i fed. enum_for tworzy instancję klasy Enumerator, a ten obiekt Enumerator odpowiada również na fed.

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" 

Zauważ więc, że za każdym razem, gdy wywołujemy rodzaje za pomocą zewnętrznego iteratora, wywoła to wydajność tylko raz. Następnym razem, gdy go nazwiemy, wywoła następną wydajność i tak dalej.

Jest ciekawa ciekawostka w odniesieniu do enum_for. Dokumentacja online zawiera następujące informacje:

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

Jeśli nie podasz symbolu jako argumentu dla enum_for, ruby ​​podłączy moduł wyliczający do każdej metody odbiornika. Niektóre klasy nie mają każdej metody, na przykład klasa String.

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

Tak więc, w przypadku niektórych obiektów wywoływanych za pomocą enum_for, musisz wyraźnie określić, jaka będzie twoja metoda wyliczania.


0

Wydajność może być użyta jako blok bezimienny do zwrócenia wartości w metodzie. Rozważ następujący kod:

Def Up(anarg)
  yield(anarg)
end

Możesz utworzyć metodę „W górę”, która ma przypisany jeden argument. Możesz teraz przypisać ten argument, aby uzyskać wynik, który wywoła i wykona powiązany blok. Możesz przypisać blok po liście parametrów.

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

Gdy wywołanie metody Up daje argument z argumentem, jest on przekazywany do zmiennej blokowej w celu przetworzenia żądania.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.