layout | title | tags | ||
---|---|---|---|---|
post |
Block trong Ruby |
|
Về cơ bản thì block là một hay nhiều dòng code được bao bởi {}
hay do
và end
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
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.
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
Để 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.
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 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
.
["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 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.
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 do
và end
, 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.