Iterator trong Ruby
Ở bài trước mình có nói với các bạn về việc sử dụng các vòng lặp for, vòng lặp while, hay until. Chúng rõ ràng là những cốt lõi trong ngôn ngữ lập trình Ruby, thế nhưng những cách viết vậy có lẽ vẫn hơi thủ công và cồng kềnh. Trong bài này mình sẽ giới thiệu với các bạn các cách để làm việc với vòng lặp dễ dàng và nhanh hơn
1. Iterator là gì?
Iterators là các công cụ vòng lặp giúp chúng ta thực hiện truy cập vào các phần tử của một tập các đối tượng một cách tuần tự.
Cụ thể ở đây Ruby cung cấp cho chúng ta các phương thức để làm việc với các collection như là Array hay là Hash. Collection giúp chúng ta lưu dữ một tập hợp các dữ liệu.
Có rất nhiều các iterator trong Ruby đã được định nghĩa sẵn như each, map, select, find... Để có thể hiểu rõ hơn rằng khi nào dùng iterator nào thì chúng ta sẽ lần lượt đi tìm hiểu các iterator này nhé.
Bài viết này được đăng tại [free tuts .net]
2. Các iterators trong Ruby
each
each cũng giống như cách mà chúng ta làm việc với các vòng lặp thông thường như for, while hay until. Nó sẽ lần lượt truy cập tới các phần tử trong collection. Và vấn đề đáng lưu tâm nhất ở đây là each sẽ trả về mảng ban đầu chứ không phải là mảng kết quả.
Cú pháp
collection.each do |variable_name| # code end
hoặc nếu những đoạn code bên trong đơn giản chúng ta có thể viết tắt như này, điều này sẽ áp dụng tương tự cho các iterator khác.
collection.each {|variable_name| // code }
collection ở đây các bạn có thể truyền vào là một Range, Array hoặc Hash đều được cả nhé.
[1, 2, 3, 4].each do |e| puts e * 2 end
Kết quả:
2 4 6 8
Tại sao mà mình là nói là each sẽ trả về mảng ban đầu thay vì mảng kết quả. Ví dụ nếu chúng ta muốn lấy ra các phần tử có giá trị lớn hơn 2 trong mảng chả hạn với each thì theo như logic chúng ta sẽ viết như thế này
a = [1,2, 3, 4] a.each do |e| e > 2 end puts "Mang a #{a}"
Kết quả
Mang a [1, 2, 3, 4]
Nên là nếu như chúng ta muốn lấy vài phần tử trong mảng cho trước thì dùng each sẽ không phù hợp.
map
Khác với each, thì kết quả của map sẽ trả về một mảng mới sau khi đã lặp qua các phần tử của một collection, kết quả trả về sẽ tùy thuộc vào cách mà chúng ta xử lý collection đó. Nhưng mà lưu ý là mảng ban đầu sẽ không thay đổi giá trị nhé.
Cú pháp:
collection.map do |variable_name| # code end
Ví dụ
[1,2, 3, 4].map do |e| e * 2 end
Kết quả
2 4 6 8
Nếu như chúng ta muốn lấy các giá trị mà thỏa mãn với các điều kiện cho trước thì kết quả sẽ trả về là một mảng các giá trị true hoặc false
[1,2,3,4,5,6,7,8,9,10].map{ |e| e > 5 }
Kết quả
false false false false false true true true true true
select
select cũng là một trong những iterator phổ biến thường được dùng. Nó sẽ lặp qua lần lượt các phần tử trong collection và trả về 1 mảng mới thỏa mãn điều kiện được định nghĩa trong block.
Cú pháp
collection.select do |variable_name| # code end
Giả sử chúng ta muốn trả về các kết quả nếu giá trị nhỏ hơn 5 thì sử dụng select sẽ trả lại cho kết quả hợp lý
[1,2,3,4,5,6,7,8,9,10].select{ |e| e > 5 }
Kết quả
6, 7, 8, 9, 10
Chúng ta không thể lặp lại mảng rồi thay đổi giá trị của các phần tử trong mảng nếu như sử dụng select
[1,2,3,4,5,6,7,8,9,10].select{ |e| e * 5 }
Kết quả
1,2,3,4,5,6,7,8,9,10
collect
collect thì về cơ bản là giống map. Nó cũng lặp qua lần lượt các phần tử trong mảng và thực hiện các biểu thức trong block
[1,2, 3, 4].collect {|e| e * 2}
Kết quả
2, 4, 6, 8
Nó cũng sẽ trả về một mảng các giá trị true hoặc false nếu như chúng ta chọn các giá trị theo điều kiện
[1, 2, 3, 4].collect {|e| e > 5}
Kết quả
false, false, false, false
inject
inject sẽ giúp chúng ta tính tổng các giá trị trong mảng.
inject có thể nhận tham số truyền vào là một giá trị nào đó. Nếu như chúng ta không truyền giá trị nào thì mặc định tổng ban đầu của mảng sẽ lấy giá trị đầu tiên của mảng làm tổng. Nếu truyền giá trị khác thì tổng ban đầu của mảng sẽ bằng giá trị chúng ta truyền vào.
Ví dụ
[1,2,3,4,5,6,7,8,9,10].inject{ |sum, e| sum += e }
Kết quả
55
Nếu chúng ta truyền thêm tham số vào inject
[1,2,3,4,5,6,7,8,9,10].inject(15){ |sum, e| sum += e }
Kết quả
70
detect
detect là một iterator khá hay, nó sẽ trả về giá trị đầu tiên thỏa mãn điều kiện trong block
[1,2,3,4,5,6,7,8,9,10].detect{ |e| e > 4 }
Kết qủa
5
Nếu như biểu thức bên trong block không phải là một điều kiện thì mặc định nó sẽ trả về phần tử đầu tiên của mảng
[1,2,3,4,5,6,7,8,9,10].detect{ |e| e * 2 }
Kết quả
1
Ngoài hàm detect này ra thì chúng ta còn có hàm find với chức năng tương tự
[1,2,3,4,5,6,7,8,9,10].find{ |e| e > 4 }
Kết quả
5
Có thể thấy, find và detect sẽ đều không lặp quả tất cả các phần tử nếu như một phần tử trong mảng thỏa mãn điều kiện, thì vòng lặp này kết thúc luôn. Việc này chúng ta cũng có thể liên tưởng khi sử dụng break để thoát khỏi vòng lặp.
each_with_index
each_with_index thì tương tự với phương thức each mà mình đã nói tới ở trên. Tuy nhiên với each_with_index cho phép chúng ta truyền thêm một tham số nữa, đó là chỉ số (index) của các phần tử mảng. Trong rất nhiều các trường hợp mà chúng ta sẽ cần tới cái index này.
Cú pháp
collection.each_with_index {|item, index| // code }
Ví dụ
[1,2,3,4,5].each_with_index { |item, index| puts "index: #{index} for #{item}" }
Kết quả
index: 0 for 1 index: 1 for 2 index: 2 for 3 index: 3 for 4 index: 4 for 5
times
Iterator times này chúng ta sẽ không áp dụng trên các collection như các phương thức ở trên, mà với iterator này chúng ta sẽ áp dụng trên một số. Có nghĩa là chúng ta sẽ lặp lại một đoạn code với số lần mà chúng ta định nghĩa
Cú pháp
number.times do //code end
Ví dụ
5.times do puts "Hello" end
Kết quả
Hello Hello Hello Hello Hello
3. Kết luận
Qua bài viết này chúng ta có thể thấy được sức mạnh của các iterator trong Ruby là như thế nào. Đây là những iterator rất phổ biến và hay được sử dụng trong việc coding. Hãy kiểm tra kĩ các trường hợp mà bạn cần xử lý để áp dụng các iterator một cách chính xác và nhanh gọn nhất có thể. Iterator trong Ruby là rất phong phú và đa dạng nên hãy áp dụng nó thay vì viết các vòng lặp đơn thuần như for, while, until vì đôi khi chúng hơi làm "phình to" code của chúng ta.