Skip to content

Instantly share code, notes, and snippets.

@oNguyenNgocTrung
Created September 23, 2018 17:52
Show Gist options
  • Save oNguyenNgocTrung/ab526c3152dd79d7405c29873e9a9acb to your computer and use it in GitHub Desktop.
Save oNguyenNgocTrung/ab526c3152dd79d7405c29873e9a9acb to your computer and use it in GitHub Desktop.
Block trong Ruby
layout title tags
post
Block trong Ruby
ruby
block

Block là gì?

Về cơ bản thì block là một hay nhiều dòng code được bao bởi {} hay doend

Về chức năng thì chúng như nhau, nhưng thường sẽ sử dụng {} cho block nào có ít code, thường là một dòng và ngược lại, nếu có nhiều code, thường hai dòng trở lên thì dùng do ... end mục đích chỉ để dễ đọc hơn thôi.

Ví dụ khi viết code nhiều dòng

[1, 2, 3].each do |n|
  # Prints out a number
  puts "#{n}"
end

Chúng ta có thể viết thành 1 dòng như thế này

[1, 2, 3].each {|n| puts "#{n}"}

|n| được gọi là tham số block (block parameter), giá trị của nó trong trường hợp này được gọi lần lượt theo giá trị của mảng đã cho, đầu tiên nhận giá trị là 1, lần thứ 2 là 2, cuối cùng là 3

Kết quả

1
2
3

yield và cách sử dụng

yield là một phần sức mạnh của ruby block, nó chịu trách nhiệm về mọi sự ảo diệu cũng như nhầm lẫn trong block. Phần lớn sự nhầm lẫn đều đến từ cách gọi block và việc làm thế nào để truyền tham số vào trong nó

def my_method
  puts "reached the top"
  yield
  puts "reached the bottom"
end

my_method do
  puts "reached yield"
end

Kết quả

reached the top
reached yield
reached the bottom

Về cơ bản, khi bạn thực thi my_method và đến dòng gọi đến yield, nó sẽ chuyển đến gọi block truyền tham số. Sau đoạn code bên trong block chạy xong, nó sẽ lại tiếp tục chạy code trong hàm my_method tiếp.

yield có tác dụng thực thi code trong block, và bản thân nó sẽ nhận giá trị return từ block đó.

def test_yield
  puts "Start of method"
  puts yield
  puts "End of method"
end

test_yield {1 + 1}

Kết quả

Start of method
2
End of method
=> nil

Ví dụ trên block {1 + 1} sẽ return về 2, yield thực thi code trong block nên sẽ nhận được return từ block là 2.

yield cũng có thể truyền tham số vào.

def test_yield
  puts "Hello"
  yield "Foo", "Bar"
end

test_yield {|str1, str2, str3| puts str1 + str2}

test_yield do |str1, str2|
  puts str1 + str2
end
Hello
FooBar
=> nil

Dễ dàng nhận ra, | ... | sẽ chứa danh sách tham số được sử dụng cho block, được truyền từ yield vào. Danh sách tham số trong | ... | sẽ map chính xác với danh sách tham số truyền vào khi gọi yield, gọi thừa hay thiếu cũng không sao, chỉ là không có dữ liệu cho tham số thôi.

def test_yield
  yield "a"
end

test_yield do |str1, str2|
  puts str1
  puts str2.nil?
end

kêt quả

a
true
def test_yield
  yield "a", "b"
end

test_yield do |str1|
  puts str1
end

Kết quả

a
=> nil

Sử dụng biến cục bộ trong block.

Để khai báo biến cục bộ dùng trong block, trong danh sách tham số khai báo bởi | ... |, ta sẽ dùng ; để chia danh sách tham số thành 2 phần, phần đầu là những tham số được truyền vào qua yield, phần còn lại phía sau là danh sách biến dùng cục bộ trong block.

x = 10
y = 10
1.times do |n; y|
  x = n
  y = n
  puts "x inside the block : #{x}"
  puts "y inside the block : #{y}"
end
puts "x outside the block : #{x}"
puts "y outside the block : #{y}"

Kết quả

x inside the block : 0
y inside the block : 0
x outside the block : 0
y outside the block : 10

Ta dễ dàng thấy, biến x không được khai báo như biến local của block, nên giá trị của nó ở bên ngoài block đã bị thay đổi khi ta gán x = n, ngược lại giá trị của y vẫn không bị thay đổi bên ngoài block.

Sử dụng block_given? để kiểm tra có block nào được truyền vào method hay không.

Một điều mà không nhắc cũng hiểu, đó là nếu block được truyền vào nhưng ta không gọi tới yield thì block đó cũng không có tác dụng gì và không hề được thực thi. Còn ngược lại, ta gọi yield nhưng không truyền block vào thì sao ?

def test_no_block
  yield
end
test_no_block
LocalJumpError: no block given (yield)

Sẽ có lỗi ngay lập tức. Để tránh lỗi xảy ra, ta có thể sử dụng method block_given?, method này sẽ kiểm tra xem có block nào được truyền vào method mà ta đang gọi yield hay không.

def test_block
  if block_given?
    yield
  else
    puts "No block given"
  end
end

test_block
test_block {puts "I saw a block"}
No block given
I saw a block

Sử dụng &block

Sử dụng cú pháp này để truyền reference của block như là một tham số vào trong method.

def test_method &block
  puts block
  block.call
end

test_method {puts "Hello Proc"}
#<Proc:0x007f99e9896950@(pry):193>
Hello Proc
=> nil

Ta có thể thấy block ta truyền vào là một instance của class Proc.

Sử dụng &:something

["a", "b"].map &:upcase
=> ["A", "B"]

Như vừa nhắc tới cách sử dụng &block, sẽ không có chuyện gì nếu tham số truyền vào method là reference của một block, nhưng nếu nó không reference tới block thì method sẽ gọi tới to_proc để chuyển nó thành block cho việc sử dụng như bình thường. Như ví dụ trên, đầu tiên ta sẽ có :upcase.to_proc để tạo ra một instance của Proc, sau đó truyền reference của block vừa tạo ra vào trong method .map để sử dụng.

Ứng dụng

Ứng dụng của block thì khỏi cần kể, dùng nó ở mọi lúc mọi nơi, nó cũng là một phần tạo ra sự đặc biệt cho ngôn ngữ Ruby. Tiện vừa nhắc tới việc nếu tham số truyền vào không phải reference tới một block, nó sẽ được to_proc để cố gắng chuyển thành một block. Ta có thể áp dụng nó cho bài toán cơ bản sau :

Từ một mảng các số nguyên, lọc ra những số chia hết cho 3.

class Fixnum
  def to_proc
    Proc.new do |obj|
      obj % self == 0
    end
  end
end

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].select &3
puts numbers
3
6
9

3 là một Fixnum, nên ta định nghĩa thêm method to_proc nhằm mục đích đưa logic để xác định nó có chia hết cho 3 hay không.

Kết luận

Block là một trong những tính năng mạnh mẽ của ruby nhưng thường xuyên bị bỏ quên. Bạn có thể thấy block không chỉ đơn giản là một đoạn code giữa doend, mà trong bài này ta còn được biết đến yield, nó cho phép bạn chèn các đoạn code vào bất cứ đâu trong phương thức. Điều đó có nghĩa là bạn có một phương thức mà có nhiều cách hoạt động khác nhau giống như có nhiều phương thức.

Tài liệu tham khảo

Mastering ruby blocks in less than 5 minutes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment