Trong Ruby Magic, chúng tôi thích đi sâu vào điều kỳ diệu đằng sau những thứ chúng tôi sử dụng hàng ngày để hiểu cách chúng hoạt động. Trong ấn bản này, chúng ta sẽ khám phá sự khác biệt giữa khối, procs và lambdas.
Trong ngôn ngữ lập trình với các hàm hạng nhất, các hàm có thể được lưu trữ trong các biến và được chuyển làm đối số cho các hàm khác. Các hàm thậm chí có thể sử dụng các hàm khác làm giá trị trả về của chúng.
Đóng là một hàm hạng nhất với một môi trường. Môi trường là ánh xạ tới các biến đã tồn tại khi quá trình đóng được tạo. Việc đóng sẽ giữ lại quyền truy cập vào các biến này, ngay cả khi chúng được xác định trong một phạm vi khác.
Ruby không có các hàm hạng nhất, nhưng nó có các bao đóng ở dạng khối, procs và lambdas. Các khối được sử dụng để chuyển các khối mã tới các phương thức và procs và lambda cho phép lưu trữ các khối mã trong các biến.
Khối
Trong Ruby, khối là các đoạn mã có thể được tạo để thực thi sau này. Các khối được chuyển tới các phương thức mang lại chúng trong do
và end
từ khóa. Một trong nhiều ví dụ là #each
phương thức này lặp lại trên các đối tượng có thể liệt kê.
[1,2,3].each do |n|
puts "#{n}!"
end
[1,2,3].each { |n| puts "#{n}!" } # the one-line equivalent.
Trong ví dụ này, một khối được chuyển đến Array#each
, chạy khối cho từng mục trong mảng và in nó ra bảng điều khiển.
def each
i = 0
while i < size
yield at(i)
i += 1
end
end
Trong ví dụ đơn giản này về Array#each
, trong while
vòng lặp, yield
được gọi để thực thi khối được truyền cho mọi mục trong mảng. Lưu ý rằng phương thức này không có đối số, vì khối được truyền ngầm cho phương thức.
Khối ẩn và yield
Từ khóa
Trong Ruby, các phương thức có thể nhận các khối một cách hoàn toàn và rõ ràng. Truyền khối ngầm hoạt động bằng cách gọi yield
từ khóa trong một phương thức. yield
từ khóa là đặc biệt. Nó tìm và gọi một khối đã truyền, vì vậy bạn không cần phải thêm khối vào danh sách các đối số mà phương thức chấp nhận.
Bởi vì Ruby cho phép truyền khối ngầm định, bạn có thể gọi tất cả các phương thức với một khối. Nếu nó không gọi yield
, khối bị bỏ qua.
irb> "foo bar baz".split { p "block!" }
=> ["foo", "bar", "baz"]
Nếu phương thức được gọi does lợi nhuận, khối đã truyền được tìm thấy và được gọi với bất kỳ đối số nào đã được chuyển đến yield
từ khóa.
def each
return to_enum(:each) unless block_given?
i = 0
while i < size
yield at(i)
i += 1
end
end
Ví dụ này trả về một bản sao của Enumerator
trừ khi một khối được đưa ra.
yield
và block_given?
từ khóa tìm khối trong phạm vi hiện tại. Điều này cho phép truyền các khối một cách ngầm định, nhưng ngăn mã truy cập trực tiếp vào khối vì nó không được lưu trữ trong một biến.
Chuyển khối một cách rõ ràng
Chúng ta có thể chấp nhận một cách rõ ràng một khối trong một phương thức bằng cách thêm nó làm đối số bằng cách sử dụng một tham số dấu và (thường được gọi là &block
). Vì khối bây giờ là rõ ràng, chúng tôi có thể sử dụng #call
phương thức trực tiếp trên đối tượng kết quả thay vì dựa vào yield
.
&block
đối số không phải là đối số thích hợp, vì vậy việc gọi phương thức này bằng bất kỳ thứ gì khác ngoài một khối sẽ tạo ra ArgumentError
.
def each_explicit(&block)
return to_enum(:each) unless block
i = 0
while i < size
block.call at(i)
i += 1
end
end
Khi một khối được chuyển như vậy và được lưu trữ trong một biến, nó sẽ tự động được chuyển đổi thành proc .
Procs
"Proc" là một bản sao của Proc
lớp, chứa một khối mã được thực thi và có thể được lưu trữ trong một biến. Để tạo một chương trình, bạn gọi Proc.new
và vượt qua nó một khối.
proc = Proc.new { |n| puts "#{n}!" }
Vì một proc có thể được lưu trữ trong một biến, nó cũng có thể được truyền cho một phương thức giống như một đối số bình thường. Trong trường hợp đó, chúng tôi không sử dụng ký hiệu và vì proc được chuyển một cách rõ ràng.
def run_proc_with_random_number(proc)
proc.call(random)
end
proc = Proc.new { |n| puts "#{n}!" }
run_proc_with_random_number(proc)
Thay vì tạo một proc và chuyển nó cho phương thức, bạn có thể sử dụng cú pháp tham số dấu và của Ruby mà chúng ta đã thấy trước đó và thay vào đó sử dụng một khối.
def run_proc_with_random_number(&proc)
proc.call(random)
end
run_proc_with_random_number { |n| puts "#{n}!" }
Lưu ý dấu và được thêm vào đối số trong phương thức. Thao tác này sẽ chuyển đổi một khối đã truyền thành một đối tượng proc và lưu trữ nó trong một biến trong phạm vi phương thức.
Mẹo :Mặc dù rất hữu ích khi có proc trong phương pháp trong một số trường hợp, nhưng việc chuyển đổi một khối thành một proc sẽ tạo ra một lượt truy cập hiệu suất. Thay vào đó, hãy sử dụng các khối ngầm định bất cứ khi nào có thể.
#to_proc
Các ký hiệu, hàm băm và phương thức có thể được chuyển đổi thành procs bằng cách sử dụng #to_proc
của chúng các phương pháp. Một cách sử dụng thường thấy của điều này là chuyển một proc được tạo từ một biểu tượng sang một phương thức.
[1,2,3].map(&:to_s)
[1,2,3].map {|i| i.to_s }
[1,2,3].map {|i| i.send(:to_s) }
Ví dụ này cho thấy ba cách gọi tương đương #to_s
trên mỗi phần tử của mảng. Trong biểu tượng đầu tiên, một ký hiệu, có tiền tố là dấu và, được chuyển, tự động chuyển nó thành proc bằng cách gọi #to_proc
của nó phương pháp. Hai phần cuối cho thấy proc đó có thể trông như thế nào.
class Symbol
def to_proc
Proc.new { |i| i.send(self) }
end
end
Mặc dù đây là một ví dụ đơn giản, nhưng việc triển khai Symbol#to_proc
hiển thị những gì đang diễn ra. Phương thức trả về một proc nhận một đối số và gửi self
với nó. Kể từ self
là biểu tượng trong ngữ cảnh này, nó gọi Integer#to_s
phương pháp.
Lambdas
Lambdas về cơ bản là procs với một số yếu tố phân biệt. Chúng giống các phương thức "thông thường" hơn theo hai cách:chúng thực thi số lượng đối số được truyền khi chúng được gọi và chúng sử dụng trả về "bình thường".
Khi gọi một lambda mong đợi một đối số mà không có một đối số hoặc nếu bạn truyền một đối số cho lambda không mong đợi nó, Ruby sẽ tạo ra một ArgumentError
.
irb> lambda (a) { a }.call
ArgumentError: wrong number of arguments (given 0, expected 1)
from (irb):8:in `block in irb_binding'
from (irb):8
from /Users/jeff/.asdf/installs/ruby/2.3.0/bin/irb:11:in `<main>'
Ngoài ra, lambda xử lý từ khóa trả về giống như cách một phương thức xử lý. Khi gọi một proc, chương trình mang lại quyền điều khiển cho khối mã trong proc. Vì vậy, nếu proc trả về, phạm vi hiện tại sẽ trở lại. Nếu một proc được gọi bên trong một hàm và gọi return
, hàm cũng ngay lập tức trả về.
def return_from_proc
a = Proc.new { return 10 }.call
puts "This will never be printed."
end
Hàm này sẽ nhường quyền kiểm soát cho proc, vì vậy khi nó trả về, hàm sẽ trả về. Gọi hàm trong ví dụ này sẽ không bao giờ in đầu ra và trả về 10.
def return_from_lambda
a = lambda { return 10 }.call
puts "The lambda returned #{a}, and this will be printed."
end
Khi sử dụng lambda, nó sẽ được in. Gọi return
trong lambda sẽ hoạt động giống như gọi return
trong một phương thức, vì vậy a
biến được điền bằng 10
và dòng được in ra bảng điều khiển.
Khối, procs và lambdas
Bây giờ chúng ta đã đi sâu vào cả khối, procs và lambdas, hãy thu nhỏ lại và tóm tắt so sánh.
- Các khối được sử dụng rộng rãi trong Ruby để truyền các bit mã cho các hàm. Bằng cách sử dụng
yield
từ khóa, một khối có thể được truyền ngầm mà không cần phải chuyển đổi nó thành một proc. - Khi sử dụng các tham số có tiền tố là dấu và, việc chuyển một khối cho một phương thức dẫn đến một proc trong ngữ cảnh của phương thức. Procs hoạt động giống như các khối, nhưng chúng có thể được lưu trữ trong một biến.
- Lambdas là các procs hoạt động giống như các phương thức, nghĩa là chúng thực thi tính hữu ích và trả về dưới dạng các phương thức thay vì trong phạm vi mẹ của chúng.
Điều này kết thúc cái nhìn của chúng tôi về các đóng cửa trong Ruby. Có nhiều điều để tìm hiểu về các đóng như phạm vi từ vựng và ràng buộc, nhưng chúng tôi sẽ giữ điều đó cho một tập trong tương lai. Trong thời gian chờ đợi, vui lòng cho chúng tôi biết những gì bạn muốn đọc trong phần sau của Ruby Magic, các bản đóng cửa hoặc bằng cách khác tại @AppSignal.