0

Block

block.jpg

Ở các phần trước chúng ta đã cùng nhau nghiên cứu về Object, Methods và cách xử lí duplicate. Hôm nay chúng ta sẽ tiếp tục tìm hiểu về Block.

Bạn có biết cách thức hoạt động của các block không?

Định nghĩa và gọi block.

Đầu tiền là phần định nghĩa block, bạn có thể định nghĩa một block với hai dấu ngoặc nhọn {} hoặc là từ khóa do ... end. Hầu hết các lập trình viên có xu hướng sử dụng dấu ngoặc nhọn cho khối chỉ có 1 dòng và từ khóa do ... end cho các khối có nhiều dòng.

User.all.each { |u| puts "Full name: " + u.last_name + " " + u.first_name }

User.all.each do |u|
  puts "Full name: " + u.last_name + " " + u.first_name
  puts "Birth day: " + u.birthday
  puts "Address: " + u.address
end

Tuy nhiên, quy ước này là không cần phài áp dụng một cách cứng nhắc, bạn có thể chọn một trong hai.

Bạn có thể định nghĩa một block duy nhất khi bạn gọi một phương thức. Block được truyền thẳng vào phương thức và phương thức có thể gọi lại block với từ khóa yield. Ví dụ :

[4] pry(main)> def test_method(a, b)
[4] pry(main)*   a + yield(a, b)
[4] pry(main)* end
=> :test_method
[5] pry(main)> test_method(5, 2) {|x, y| (x - y) * 3 }
=> 14

Tùy chọn, một block có thể có đối số, như x và y trong ví dụ ở trên. Khi bạn gọi lại cho các block, bạn có thể cung cấp giá trị cho đối số của nó, giống như bạn làm khi bạn gọi một phương thức. Ngoài ra, cũng như một phương thức, một khối trả về kết quả của dòng cuối cùng của đoạn code mà nó thực hiện.

**block_given? **

Bây giờ với phương thức test_method ở ví dụ bên trên, ta thử không truyền một block vào.

[7] pry(main)> test_method(5, 2)
LocalJumpError: no block given (yield)
from (pry):7:in `test_method'

Ngay lập tức nó sẽ báo lỗi “no block given “. Ở bên trong mọt phương thức, bạn có thể kiểm tra tham số truyền vào có chứa một block hay không với Kernel#block_given?( )

def test_method(a, b)
  if block_given?
    puts "block exist"
    a + yield(a, b)
  else
    "no block"
  end
end
[2] pry(main)> test_method(5, 2)
=> "no block"
[3] pry(main)> test_method(5, 2) {|x, y| (x - y) * 3 }
block exist
=> 14

Như vậy với Kernel#block_given?( ) chúng ta có thể tránh được trường hợp bị lỗi khi không truyền vào một block vào một phương thức cần sử dụng.

** Khởi tạo một đối tượng với các giá trị mặc định bằng block.**

Nếu bạn đã từng thử mở một tập tin .gemspec của bất kì gem nào đó thì bạn có thể thấy mô hình này. Đây là một mô hình dùng với block của ruby để khởi tạo một đối tượng với các giá trị mặc định. Cách mà nó hoạt động đó chính là bạn khởi tạo bằng cách gọi yield(self). Ở bên trong phương thức khởi tạo thì self chính là đối tượng đang được khởi tạo.

class Cat
  attr_accessor :color, :age

  def initialize
    yield(self)
  end
end

cat = Cat.new do |c|
  c.color = "White"
  c.age = 2
end

puts "My cat's color is #{cat.color} and it's #{car.age} years old."
# My cat's color is White and it's 2 years old.

Block được sử dụng rất nhiều bởi vì tính đơn giản và dễ dùng, tuy nhiên có một hạn chế đó là khi ta muốn thay đổi đầu vào thì phải viết lại toàn bộ block.

Bao đóng (Closures)

Một block không phải là một đoạn code trôi nổi, riêng biệt. Khi chạy nó cần một môi trường : các biến cục bộ, biến thực thể, self … Block khi chạy nó chưa cả code và một tập hợp các ràng buộc (bindings) . Bạn sẽ tự hỏi là nó lấy những ràng buộc đó ở đâu? Nó chỉ đơn giản là lấ các ràng buộc đang có tại thời điểm định nghĩa và sau đó mang theo những ràng buộc đó.

def my_method
  x = "Goodbye"
  puts x
  yield("beautiful" )
end
irb(main):028:0> x = "Hello"
=> "Hello"
irb(main):029:0> my_method {|y| "#{x}, #{y} world" }
Goodbye
=> "Hello, beautiful world"

Tại sao khi kết quả cuối cùng lại là "Hello, beautiful world" mà không phải là "Goodbye, beautiful world" ?

Khi bạn tạo ra một block, bạn có những ràng buộc địa phương như là biến x. Sau đó bạn truyền block tới một phương thức và ở đây cũng có những ràng buộc và nó bao gồm biến x. Tuy nhiên đoạn code trong block nhìn thấy biến x được tạo ra trước khi định nghĩa block chứ không phải là biến x ở bên trong phương thức. Và người ta gọi thuộc tính đó là thuộc tính bao đóng của block.

Scope

Các block đều có phạm vi của nó, bạn có thể thấy tất cả các ràng buộc trên tất cả các phạm vi. Ở bên dưới bạn sẽ thấy có một loạt các biến địa phương. Ở bên trên, bạn sẽ thấy bạn đang đứng trong một đối tượng với các phương thức và các biến thực thể riêng, đó chính là đối tượng hiện tại (self). Xa hơn bạn sẽ thấy cây của các hằng số.

Ví dụ sau đây sẽ cho thấy cách mà phạm vi thay đổi khi chương trình của bạn chạy, chúng ta có thể theo dõi các các ràng buộc với phương thức Kernel # local_variables ().

v1 = 1
  class MyClass
    v2 = 2
    local_variables
  def my_method
    v3 = 3
    local_variables
  end
  local_variables
end
irb(main):011:0> obj = MyClass.new
=> #<MyClass:0x0000000328a7a0>
irb(main):012:0> obj.my_method
=> [:v3]
irb(main):013:0> obj.my_method
=> [:v3]
irb(main):014:0> local_variables
=> [:obj, :v1]

instance_eval()

Đây là phương thức và cũng là cách để thay đổi code, thay đổi các ràng buộc theo ý của mình.

class MyClass
  def initialize
    @v = 1
  end
end
obj = MyClass.new
obj.instance_eval do
  self # => #<MyClass:0x3340dc @v=1>
  @v # => 1
end

obj.instance_eval { @v = v }
obj.instance_eval { @v }
# => 2

Callable objects

Về cơ bản thì sử dụng block là một quá trình hai bước. Đầu tiên, bạn viết code ở một nơi, và thứ hai, bạn gọi khối (with yield) để thực thi. Cơ chế "đóng gói code đầu tiền, sau đó gọi nó"không chỉ dành riêng cho blocks. Có ít nhất ba cách khác trong Ruby nơi bạn có thể đóng gói code:

• Trong một proc, mà về cơ bản là một đối tượng trả lại block.

• Trong một lambda, mà là một sự thay đổi nhỏ về một proc

• Trong một phương thức.

Proc object

Hãy tưởng tưởng rằng bạn muốn viết và lưu lại một block để có thể chạy nó sau này. Để làm việc đó bạn cần một đối tượng. Và bạn có thể sử dụng một lớp được cung cấp trong thư viện chuẩn của Ruby đó chính là Proc. Một proc là một block và được trả lại bên trong một đối tượng. Bạn có thể tạo ra một Proc bằng cách truyền một block vào Proc,new. Sau đó bạn có thể sử dụng đối tượng trả lại block với Proc#call()

irb(main):011:0> pr = Proc.new {|x| x + 2 }
=> #<Proc:0x0000000392f358@(irb):11>
irb(main):012:0> # more code...
irb(main):013:0* pr.call(2)
=> 4

Lambda

Lambda cũng giống như một proc, cũng là một object và trả về giá trị như các hàm khác.

irb(main):022:0> la = lambda{|x| "Hello #{x}"}
=> #<Proc:0x00000003808d30@(irb):22 (lambda)>
irb(main):023:0> la.call("world")
=> "Hello world"

lambda có 3 cách gọi.

irb(main):023:0> la.call("world")
=> "Hello world"
irb(main):024:0> la["the earth"]
=> "Hello the earth"
irb(main):025:0> la.("Viet Nam")
=> "Hello Viet Nam"

PROC vs LAMBDA

Lambda là một đối tượng của Proc tuy nhiên chúng có sự khác nhau là Lambda thì kiểm tra các tham số được truyền vào còn Proc thì lại không.

irb(main):026:0> proc = Proc.new {|x| "Hello #{x}"}
=> #<Proc:0x00000003797388@(irb):26>
irb(main):027:0> proc.call("World", "Earth")
=> "Hello World"
irb(main):028:0> lam = lambda{|x| "Hello #{x}"}
=> #<Proc:0x00000003754a88@(irb):28 (lambda)>
irb(main):029:0> lam.call("World", "Earth")
ArgumentError: wrong number of arguments (2 for 1)

Với các tham số không truyền giá trị thì proc sẽ mặc định là nil.

irb(main):030:0> proc.call()
=> "Hello "
irb(main):031:0> lam.call()
ArgumentError: wrong number of arguments (0 for 1)

Khi sử dụng return với proc thì sẽ trả lại giá trị ngay sau khi thực hiện proc.

Khi sử dụng return với lambda thì nó sẽ trả lại giá trị và tiếp tục chạy.

class Car
  def with_proc
    Proc.new{return "Using proc"}.call
    puts "after call proc"
  end
  def with_lambda
    lambda { return "Using lambda"}.call
    puts "after call lambda"
  end
end
irb(main):043:0> car = Car.new
=> #<Car:0x0000000363b7f0>
irb(main):044:0> car.with_proc
=> "Using proc"
irb(main):045:0> car.with_lambda
after call lambda
=> nil

Như vậy chúng ta đã tìm hiểu về Block, hẹn gặp lại các bạn vào phần sau.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí