Truyền block vào method không sử dụng &block
Có 2 cách để truyền vào một block cho một hàm trong Ruby. Cách 1 Sử dụng từ khoá yield: def speak puts yield end speak { "Hello" } # Hello # => nil Cách 2 Chèn vào trước argument cuối của một hàm với một dấu & (ampersand) để sau đó tạo ra một đối tượng Proc ...
Có 2 cách để truyền vào một block cho một hàm trong Ruby.
Cách 1
Sử dụng từ khoá yield:
def speak puts yield end
speak { "Hello" } # Hello # => nil
Cách 2
Chèn vào trước argument cuối của một hàm với một dấu & (ampersand) để sau đó tạo ra một đối tượng Proc từ bất kể block nào được truyền vào. Đối tượng này có thể được thực thi với hàm call như sau:
def speak(&block) puts block.call end
speak { "Hello" } # Hello # => nil
Vấn đề của cách thứ 2 là khi khởi tạo đối tượng Proc mới sẽ làm ảnh hưởng đến tốc độ, anh Aaron Patterson có giải thích chi tiết trong bài nói “ZOMG WHY IS THIS CODE SO SLOW?" tại RubyConf X, (đoạn 30 phút ở trang 181).
Ta có thể kiểm tra thông qua benchmark, block_benchmark.rb:
require "benchmark" def speak_with_block(&block) block.call end def speak_with_yield yield end n = 1_000_000 Benchmark.bmbm do |x| x.report("&block") do n.times { speak_with_block { "ook" } } end x.report("yield") do n.times { speak_with_yield { "ook" } } end end
Kết quả cho thấy sự khác biệt rõ rệt giữa 2 cách:
$ ruby block_benchmark.rb Rehearsal ------------------------------------------ &block 1.410000 0.020000 1.430000 ( 1.430050) yield 0.290000 0.000000 0.290000 ( 0.291750) --------------------------------- total: 1.720000sec user system total real &block 1.420000 0.030000 1.450000 ( 1.452686) yield 0.290000 0.000000 0.290000 ( 0.292179)
Như vậy ta có thể nhận thấy việc sử dụng yield sẽ nhanh hơn &block. Nhưng khi ta cần truyền 1 block cho 1 method thì sao?
Ví dụ, ta có 1 class với hàm tell_ape gọi đến hàm tell. Kiểu pattern thường được xử lý bằng method_missing nhưng ta sẽ giữ và khai báo toàn bộ các hàm để tiện giải thích:
class Monkey # Monkey.tell_ape { "ook!" } # ape: ook! # => nil def self.tell_ape(&block) tell("ape", &block) end def self.tell(name, &block) puts "#{name}: #{block.call}" end end
Cách làm trên là không thể nếu sử dụng yield
class Monkey # Monkey.tell_ape { "ook!" } # ArgumentError: wrong number of arguments (2 for 1) def self.tell_ape tell("ape", yield) end def self.tell(name) puts "#{name}: #{yield}" end end
Và cách trên cũng không thể chạy nếu sử dụng &block:
class Monkey # Monkey.tell_ape { "ook!" } # TypeError: wrong argument type String (expected Proc) def self.tell_ape tell("ape", &yield) end def self.tell(name) puts "#{name}: #{yield}" end end
Tuy nhiên có 1 cách để chỉ tạo ra 1 object Proc khi cần thiết, đó là cách sử dụng một đặc tính ít được biết đến của hàm Proc.new, anh Aaron có giải thích trong bài nói được nhắc ở trên
Nếu Proc.new được gọi từ bên trong một hàm mà không có bất kỳ argument nào thì nó sẽ trả về một Proc có kèm block được đưa cho hàm ở ngoài.
Nếu Proc.new được gọi từ bên trong một hàm với không có argument nào của chính nó, nó sẽ trả về một Proc có chứa block cho method chứa nó.
def speak puts Proc.new.call end
speak { "Hello" } # Hello
Điều này có nghĩa là ta có thể truyền vào một blockgiữa các methods mà không cần phải sử dụng &block
class Monkey # Monkey.tell_ape { "ook!" } # ape: ook! # => nil def self.tell_ape tell("ape", &Proc.new) end def self.tell(name) puts "#{name}: #{yield}" end end
Dĩ nhiên là nếu dùng Proc.new, thì sẽ mất thời gian hơn so với yield (khi các đối tượng Proc được khởi tạo với &block) nhưng nó sẽ tránh được các khởi tạo không cần thiết của các đối tượng Proc khi ta không cần đến chúng. Ta có thể sử dụng benmark để làm rõ điều trên: proc_new_benchmark.rb:
require "benchmark" def sometimes_block(flag, &block) if flag && block block.call end end def sometimes_proc_new(flag) if flag && block_given? Proc.new.call end end
n = 1_000_000 Benchmark.bmbm do |x| x.report("&block") do n.times do sometimes_block(false) { "won't get used" } end end x.report("Proc.new") do n.times do sometimes_proc_new(false) { "won't get used" } end end end
Ta có thể thấy sự khác biệt khá lớn:
$ ruby code/proc_new_benchmark.rb Rehearsal -------------------------------------------- &block 1.080000 0.160000 1.240000 ( 1.237644) Proc.new 0.160000 0.000000 0.160000 ( 0.156077) ----------------------------------- total: 1.400000sec user system total real &block 1.090000 0.080000 1.170000 ( 1.178771) Proc.new 0.160000 0.000000 0.160000 ( 0.155053)
Mấu chốt ở đây là khi sử dụng &block thì sẽ luôn tạo ra object Proc mới, ngay cả khi ta không cần dùng đến. Bằng cách sử dụng Proc.new khi cần, ta có thể tránh việc khởi tạo toàn bộ các object.
Tuy nhiên, có thể bạn sẽ phải cân nhắc về code dễ đọc hay tốc độ. Như với method somtimes_block cần truyền vào block. Và nghiễm nhiên ta sẽ hiểu rằng block đó được sử dụng để làm gì trong method đó. Nhưng với method sometimes_proc_new thì rất dễ ngây hiểu nhầm