Khối và sản lượng trong Ruby


275

Tôi đang cố gắng hiểu các khối yieldvà cách chúng hoạt động trong Ruby.

Làm thế nào được yieldsử dụng? Nhiều ứng dụng Rails tôi đã xem xét sử dụng yieldmột cách kỳ lạ.

Ai đó có thể giải thích cho tôi hoặc chỉ cho tôi nơi để đi để hiểu họ?


2
Bạn có thể quan tâm đến câu trả lời cho tính năng năng suất của Ruby liên quan đến khoa học máy tính . Mặc dù đó là một câu hỏi hơi khác so với câu hỏi của bạn, nhưng nó có thể làm sáng tỏ vấn đề.
Ken Bloom

Câu trả lời:


393

Vâng, lúc đầu hơi khó hiểu.

Trong Ruby, các phương thức có thể nhận được một khối mã để thực hiện các phân đoạn mã tùy ý.

Khi một phương thức mong đợi một khối, nó gọi nó bằng cách gọi yieldhàm.

Điều này rất tiện dụng, ví dụ, lặp đi lặp lại qua một danh sách hoặc để cung cấp một thuật toán tùy chỉnh.

Lấy ví dụ sau:

Tôi sẽ định nghĩa một Personlớp được khởi tạo với một tên và cung cấp một do_with_namephương thức mà khi được gọi, sẽ chỉ truyền namethuộc tính cho khối nhận được.

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

    def do_with_name 
        yield( @name ) 
    end
end

Điều này sẽ cho phép chúng ta gọi phương thức đó và vượt qua một khối mã tùy ý.

Ví dụ, để in tên, chúng tôi sẽ làm:

person = Person.new("Oscar")

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

Sẽ in:

Hey, his name is Oscar

Lưu ý, khối nhận, như một tham số, một biến được gọi name(NB bạn có thể gọi biến này là bất cứ thứ gì bạn thích, nhưng sẽ rất hợp lý khi gọi nó name). Khi mã gọi yieldnó sẽ điền tham số này với giá trị là @name.

yield( @name )

Chúng tôi có thể cung cấp một khối khác để thực hiện một hành động khác. Ví dụ: đảo ngược tên:

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

Chúng tôi đã sử dụng chính xác cùng một phương thức ( do_with_name) - nó chỉ là một khối khác nhau.

Ví dụ này là tầm thường. Các cách sử dụng thú vị hơn là lọc tất cả các thành phần trong một mảng:

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

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

=> ["tuesday", "thursday"]

Hoặc, chúng tôi cũng có thể cung cấp thuật toán sắp xếp tùy chỉnh, ví dụ dựa trên kích thước chuỗi:

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

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

Tôi hy vọng điều này sẽ giúp bạn hiểu nó tốt hơn.

BTW, nếu khối là tùy chọn, bạn nên gọi nó như sau:

yield(value) if block_given?

Nếu không phải là tùy chọn, chỉ cần gọi nó.

BIÊN TẬP

@hmak đã tạo một repl.it cho các ví dụ sau: https://repl.it/@makstaks/blocksandyieldsrubyexample


nó in như thế nào racsOnếu the_name = ""
Paritosh Piplwar

2
Xin lỗi, tên là một biến đối tượng được khởi tạo với "Oscar" (không rõ ràng trong câu trả lời)
OscarRyz

Mã như thế này thì sao? person.do_with_name {|string| yield string, something_else }
f.ardelian

7
Vì vậy, theo thuật ngữ Javascripty, đó là một cách tiêu chuẩn hóa để chuyển một cuộc gọi lại đến một phương thức nhất định và gọi nó. Cảm ơn đã giải thích!
yitznewton

Nói một cách tổng quát hơn - một khối là đường cú pháp "nâng cao" của ruby ​​cho mẫu Chiến lược. bởi vì cách sử dụng thông thường là cung cấp một mã để làm một cái gì đó trong bối cảnh của hoạt động khác. Nhưng những cải tiến của ruby ​​mở ra một cách tuyệt vời như viết DSL bằng cách sử dụng khối để vượt qua bối cảnh xung quanh
Roman Bulgakov

25

Trong Ruby, các phương thức có thể kiểm tra xem liệu chúng có được gọi theo cách mà một khối được cung cấp ngoài các đối số bình thường hay không. Thông thường, điều này được thực hiện bằng block_given?phương thức nhưng bạn cũng có thể gọi khối là một Proc rõ ràng bằng cách thêm tiền tố vào dấu và ( &trước tên đối số cuối cùng.

Nếu một phương thức được gọi với một khối thì phương thức đó có thể yieldđiều khiển khối (gọi khối) với một số đối số, nếu cần. Xem xét phương pháp ví dụ này chứng minh:

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

Hoặc, sử dụng cú pháp đối số khối đặc biệt:

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

Tốt để biết các cách khác nhau để kích hoạt một khối.
LPing

22

Rất có khả năng ai đó sẽ cung cấp một câu trả lời thực sự chi tiết ở đây, nhưng tôi luôn thấy bài đăng này của Robert Sosinski là một lời giải thích tuyệt vời về sự tinh tế giữa các khối, procs & lambdas.

Tôi nên thêm rằng tôi tin rằng bài đăng tôi đang liên kết là dành riêng cho ruby ​​1.8. Một số thứ đã thay đổi trong ruby ​​1.9, chẳng hạn như các biến khối là cục bộ của khối. Trong 1.8, bạn sẽ nhận được một cái gì đó như sau:

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

Trong khi 1.9 sẽ cung cấp cho bạn:

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

Tôi không có 1.9 trên máy này vì vậy ở trên có thể có lỗi trong đó.


Mô tả tuyệt vời trong bài viết đó, tôi đã mất nhiều tháng để tự mình tìm ra tất cả =)
maerics

Tôi đồng ý. Tôi không nghĩ rằng tôi đã biết một nửa những điều được giải thích cho đến khi tôi đọc nó.
theIV

Liên kết cập nhật là 404 bây giờ, quá. Đây là liên kết Wayback Machine .
klenwell

@klenwell cảm ơn vì đã đề phòng, tôi đã cập nhật lại liên kết.
ngày

13

Tôi muốn sắp xếp thêm lý do tại sao bạn sẽ làm mọi thứ theo cách đó cho câu trả lời tuyệt vời.

Không biết bạn đến từ ngôn ngữ nào, nhưng giả sử đó là ngôn ngữ tĩnh, loại thứ này sẽ trông quen thuộc. Đây là cách bạn đọc một tập tin trong 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();
    }
  }
}

Bỏ qua tất cả các chuỗi xích, Ý tưởng là thế này

  1. Khởi tạo tài nguyên cần được dọn sạch
  2. sử dụng tài nguyên
  3. đảm bảo làm sạch nó

Đây là cách bạn làm điều đó trong ruby

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

Hoang dã khác nhau. Phá vỡ cái này

  1. báo cho lớp File cách khởi tạo tài nguyên
  2. nói với lớp tập tin phải làm gì với nó
  3. cười vào những người java vẫn đang gõ ;-)

Ở đây, thay vì xử lý bước một và hai, về cơ bản, bạn ủy thác điều đó cho một lớp khác. Như bạn có thể thấy, điều đó làm giảm đáng kể số lượng mã bạn phải viết, giúp mọi thứ dễ đọc hơn và giảm khả năng những thứ như rò rỉ bộ nhớ hoặc khóa tệp không bị xóa.

Bây giờ, nó không giống như bạn không thể làm một cái gì đó tương tự trong java, trên thực tế, mọi người đã làm điều đó trong nhiều thập kỷ nay. Nó được gọi là mẫu Chiến lược . Sự khác biệt là không có khối, đối với một cái gì đó đơn giản như ví dụ về tệp, chiến lược trở nên quá mức do số lượng lớp và phương thức bạn cần viết. Với các khối, đó là một cách đơn giản và thanh lịch để làm điều đó, nó không có nghĩa gì khi KHÔNG cấu trúc mã của bạn theo cách đó.

Đây không phải là cách duy nhất được sử dụng, nhưng các khối khác (như mẫu Builder, mà bạn có thể thấy trong form_for api trong đường ray) tương tự nhau đến mức rõ ràng những gì đang diễn ra khi bạn quấn đầu xung quanh điều này. Khi bạn thấy các khối, thường an toàn khi giả định rằng cuộc gọi phương thức là điều bạn muốn làm và khối đó mô tả cách bạn muốn thực hiện.


5
Chúng ta hãy đơn giản hóa điều đó một chút: File.readlines("readfile.rb").each_with_index do |line, index| puts "#{index + 1}: #{line}" endvà cười thậm chí còn khó hơn với những kẻ Java.
Michael Hampton

1
@MichaelHampton, cười sau khi bạn đọc một tập tin dài vài gigabyte.
akostadinov 17/2/2015

@akostadinov Không ... điều đó khiến tôi muốn khóc!
Michael Hampton

3
@MichaelHampton Hoặc, tốt hơn nữa: IO.foreach('readfile.rb').each_with_index { |line, index| puts "#{index}: #{line}" }(cộng với không có vấn đề về bộ nhớ)
Vụ kiện của Quỹ Monica

12

Tôi thấy bài viết này rất hữu ích. Cụ thể, ví dụ sau:

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

cái nào sẽ cho đầu ra sau:

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

Vì vậy, về cơ bản mỗi khi một cuộc gọi được thực hiện cho yieldruby sẽ chạy mã trong dokhối hoặc bên trong {}. Nếu một tham số được cung cấp cho yieldthì điều này sẽ được cung cấp dưới dạng tham số cho dokhối.

Đối với tôi, đây là lần đầu tiên tôi thực sự hiểu những gì các dokhối đang làm. Về cơ bản, đây là cách để hàm cung cấp quyền truy cập vào các cấu trúc dữ liệu nội bộ, là cách lặp hoặc để cấu hình chức năng.

Vì vậy, khi trong đường ray bạn viết như sau:

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

Điều này sẽ chạy respond_tochức năng mang lại dokhối với formattham số (nội bộ) . Sau đó, bạn gọi .htmlhàm trên biến nội bộ này, từ đó tạo ra khối mã để chạy renderlệnh. Lưu ý rằng .htmlsẽ chỉ mang lại nếu nó là định dạng tệp được yêu cầu. (tính kỹ thuật: các chức năng này thực sự sử dụng block.callkhông phải yieldnhư bạn có thể thấy từ nguồn nhưng chức năng về cơ bản là giống nhau, xem câu hỏi này để thảo luận.) Điều này cung cấp một cách để chức năng thực hiện một số khởi tạo sau đó lấy đầu vào từ mã gọi và sau đó tiến hành xử lý nếu được yêu cầu.

Hoặc nói theo một cách khác, nó tương tự như một hàm lấy một hàm ẩn danh làm đối số và sau đó gọi nó trong javascript.


8

Trong Ruby, một khối về cơ bản là một đoạn mã có thể được truyền vào và thực thi bằng bất kỳ phương thức nào. Các khối luôn được sử dụng với các phương thức, thường cung cấp dữ liệu cho chúng (dưới dạng đối số).

Các khối được sử dụng rộng rãi trong đá quý Ruby (bao gồm cả Rails) và trong mã Ruby được viết tốt. Chúng không phải là đối tượng, do đó không thể được gán cho các biến.

Cú pháp cơ bản

Một khối là một đoạn mã được bao quanh bởi {} hoặc do..end. Theo quy ước, cú pháp dấu ngoặc nhọn nên được sử dụng cho các khối đơn dòng và cú pháp do..end nên được sử dụng cho các khối nhiều dòng.

{ # This is a single line block }

do
  # This is a multi-line block
end 

Bất kỳ phương thức nào cũng có thể nhận được một khối làm đối số ngầm. Một khối được thực thi bởi câu lệnh lợi suất trong một phương thức. Cú pháp cơ bản là:

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.

Khi đạt được câu lệnh năng suất, phương thức thiền mang lại điều khiển cho khối, mã trong khối được thực thi và điều khiển được trả về phương thức, tiếp tục thực hiện ngay sau câu lệnh lợi suất.

Khi một phương thức chứa một tuyên bố lợi suất, nó sẽ nhận được một khối tại thời điểm gọi. Nếu một khối không được cung cấp, một ngoại lệ sẽ được đưa ra sau khi đạt được tuyên bố lợi tức. Chúng tôi có thể làm cho khối tùy chọn và tránh ngoại lệ được nêu ra:

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

Output:
Today we will practice zazen. 

Không thể truyền nhiều khối cho một phương thức. Mỗi phương pháp chỉ có thể nhận được một khối.

Xem thêm tại: http://www.zenruby.info/2016/04/int sinhtion-to-blocks-in-ruby.html


Đây là câu trả lời (duy nhất) thực sự khiến tôi hiểu thế nào là khối và năng suất, và cách sử dụng chúng.
Eric Wang

5

Đôi khi tôi sử dụng "năng suất" như thế này:

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

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

Được rôi nhưng tại sao ? Có rất nhiều lý do, chẳng hạn như lý do Loggerphải không thực hiện một số tác vụ nếu người dùng không cần. Bạn nên giải thích về bạn mặc dù ...
Ulysse BN

4

Hiệu suất, nói một cách đơn giản, cho phép phương thức bạn tạo để thực hiện và gọi các khối. Từ khóa năng suất cụ thể là vị trí nơi 'công cụ' trong khối sẽ được thực hiện.


1

Có hai điểm tôi muốn thực hiện về năng suất ở đây. Đầu tiên, trong khi rất nhiều câu trả lời ở đây nói về các cách khác nhau để chuyển một khối sang một phương thức sử dụng năng suất, thì chúng ta cũng hãy nói về luồng điều khiển. Điều này đặc biệt có liên quan vì bạn có thể mang lại NHIỀU lần cho một khối. Hãy xem một ví dụ:

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

Khi mỗi phương thức được gọi, nó thực thi từng dòng một. Bây giờ khi chúng ta đến khối 3.times, khối này sẽ được gọi 3 lần. Mỗi lần nó gọi năng suất. Sản lượng đó được liên kết với khối liên kết với phương thức được gọi là mỗi phương thức. Điều quan trọng cần lưu ý là mỗi lần sản lượng được gọi, nó sẽ trả lại quyền điều khiển cho khối của từng phương thức trong mã máy khách. Khi khối được thực hiện xong, nó sẽ quay trở lại khối 3.times. Và điều này xảy ra 3 lần. Vì vậy, khối trong mã máy khách được gọi trong 3 lần riêng biệt do năng suất được gọi rõ ràng là 3 lần riêng biệt.

Điểm thứ hai của tôi là về enum_for và năng suất. enum_for khởi tạo lớp Enumerator và đối tượng Enumerator này cũng đáp ứng với năng suất.

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" 

Vì vậy, hãy chú ý mỗi khi chúng ta gọi các loại với trình vòng lặp bên ngoài, nó sẽ chỉ gọi năng suất một lần. Lần sau chúng ta gọi nó, nó sẽ gọi sản lượng tiếp theo và cứ thế.

Có một mẩu tin thú vị liên quan đến enum_for. Các tài liệu trực tuyến nêu sau:

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

Nếu bạn không chỉ định một biểu tượng làm đối số cho enum_for, ruby ​​sẽ nối trình liệt kê vào từng phương thức của người nhận. Một số lớp không có mỗi phương thức, như lớp String.

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

Do đó, trong trường hợp một số đối tượng được gọi bằng enum_for, bạn phải rõ ràng về phương thức liệt kê của bạn.


0

Yield có thể được sử dụng như một khối không tên để trả về một giá trị trong phương thức. Hãy xem xét các mã sau đây:

Def Up(anarg)
  yield(anarg)
end

Bạn có thể tạo một phương thức "Lên" được gán một đối số. Bây giờ bạn có thể gán đối số này để mang lại kết quả sẽ gọi và thực thi một khối liên quan. Bạn có thể gán khối sau danh sách tham số.

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

Khi phương thức Up gọi năng suất, với một đối số, nó được chuyển đến biến khối để xử lý yêu cầu.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.