Đã đến lúc cho một tập khác của Ruby Magic! Lần này, chúng ta sẽ xem xét một trong những tính năng kỳ diệu nhất của Ruby, cung cấp hầu hết các phương thức bạn sẽ sử dụng khi làm việc với các lớp có thể liệt kê của Ruby như Array
, Hash
và Range
. Trong quá trình này, chúng ta sẽ tìm hiểu những gì bạn có thể làm với các đối tượng có thể liệt kê, cách liệt kê hoạt động và cách làm cho một đối tượng có thể liệt kê bằng cách triển khai một phương thức duy nhất.
Enumerable
, #each
và Enumerator
Liệt kê đề cập đến việc đi ngang qua các đối tượng. Trong Ruby, chúng tôi gọi một đối tượng là enumerable khi nó mô tả một tập hợp các mục và một phương thức để lặp lại từng mục.
Các bảng liệt kê tích hợp sẵn có được các tính năng liệt kê của chúng bằng cách đưa vào Enumerable
mô-đun cung cấp các phương thức như #include?
, #count
, #map
, #select
và #uniq
, giữa những người khác. Hầu hết các phương thức được liên kết với mảng và hàm băm không thực sự được triển khai trong chính các lớp này, chúng được bao gồm.
Lưu ý :Một số phương pháp, như #count
và #take
trên Array
lớp học, là được triển khai cụ thể cho các mảng thay vì sử dụng các mảng từ Enumerable
mô-đun. Điều đó thường được thực hiện để giúp hoạt động nhanh hơn.
Enumerable
mô-đun dựa trên một phương thức có tên #each
, cần được triển khai trong bất kỳ lớp nào mà nó bao gồm. Khi được gọi với một khối trên một mảng, #each
phương thức sẽ thực thi khối cho mỗi phần tử của mảng.
irb> [1,2,3].each { |i| puts "* #{i}" }
* 1
* 2
* 3
=> [1,2,3]
Nếu chúng ta gọi #each
phương thức trên một mảng không có truyền một khối để thực thi cho từng phần tử của nó, chúng ta sẽ nhận được một bản sao của Enumerator
.
irb> [1,2,3].each
=> #<Enumerator: [1, 2, 3]:each>
Các bản sao của Enumerator
mô tả cách lặp qua một đối tượng. Điều tra viên lặp qua các đối tượng theo cách thủ công và liệt kê chuỗi.
irb> %w(dog cat mouse).each.with_index { |a, i| puts "#{a} is at position #{i}" }
dog is at position 0
cat is at position 1
mouse is at position 2
=> ["dog", "cat", "mouse"]
#with_index
phương pháp là một ví dụ điển hình về cách hoạt động của các điều tra viên đã thay đổi. Trong ví dụ này, #each
được gọi trên mảng để trả về một liệt kê. Sau đó, #with_index
được gọi để thêm chỉ số vào từng phần tử của mảng để cho phép in chỉ mục của từng phần tử.
Làm cho các đối tượng có thể liệt kê được
Bên dưới, các phương pháp như #max
, #map
và #take
dựa vào #each
phương thức hoạt động.
def max
max = nil
each do |item|
if !max || item > max
max = item
end
end
max
end
Trong nội bộ, Enumerable
Các phương thức của có triển khai C, nhưng ví dụ trên cho thấy gần như thế nào #max
làm. Bằng cách sử dụng #each
để lặp lại tất cả các giá trị và ghi nhớ giá trị cao nhất, nó sẽ trả về giá trị lớn nhất.
def map(&block)
new_list = []
each do |item|
new_list << block.call(item)
end
new_list
end
#map
hàm gọi khối đã truyền với mỗi mục và đặt kết quả vào một danh sách mới để trả về sau khi lặp qua tất cả các giá trị.
Vì tất cả các phương thức trong Enumerable
sử dụng #each
ở một mức độ nào đó, bước đầu tiên của chúng ta trong việc tạo một lớp tùy chỉnh có thể liệt kê là triển khai #each
phương pháp.
Triển khai #each
Bằng cách triển khai #each
và bao gồm Enumerable
mô-đun trong một lớp, nó trở nên có thể liệt kê và nhận các phương thức như #min
, #take
và #inject
miễn phí.
Mặc dù hầu hết các tình huống đều cho phép quay trở lại một đối tượng hiện có như một mảng và gọi #each
trên đó, hãy xem một ví dụ mà chúng ta phải tự viết nó từ đầu. Trong ví dụ này, chúng tôi sẽ triển khai #each
trên danh sách được liên kết để làm cho nó có thể liệt kê được.
Danh sách được liên kết:danh sách không có mảng
Danh sách được liên kết là một tập hợp các phần tử dữ liệu, trong đó mỗi phần tử trỏ đến phần tử tiếp theo. Mỗi phần tử trong danh sách có hai giá trị, được đặt tên là head và đuôi . Phần đầu giữ giá trị của phần tử và phần đuôi là liên kết đến phần còn lại của danh sách.
[42, [12, [73, nil]]
Đối với danh sách được liên kết có ba giá trị (42, 12 và 73), phần đầu của phần tử đầu tiên là 42 và phần đuôi là liên kết đến phần tử thứ hai. Phần đầu của phần tử thứ hai là 12 và phần đuôi chứa phần tử thứ ba. Đầu của phần tử thứ ba là 73 và đuôi là nil
, cho biết phần cuối của danh sách.
Trong Ruby, một danh sách liên kết có thể được tạo bằng cách sử dụng một lớp chứa hai biến cá thể có tên @head
và @tail
.
class LinkedList
def initialize(head, tail = nil)
@head, @tail = head, tail
end
def <<(item)
LinkedList.new(item, self)
end
def inspect
[@head, @tail].inspect
end
end
#<<
được sử dụng để thêm các giá trị mới vào danh sách, phương thức này hoạt động bằng cách trả về một danh sách mới với giá trị đã truyền là phần đầu và danh sách trước đó là phần đuôi.
Trong ví dụ này, #inspect
phương thức được thêm vào để chúng ta có thể xem trong danh sách để kiểm tra xem nó chứa những phần tử nào.
irb> LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]
Bây giờ chúng ta có một danh sách được liên kết, hãy triển khai #each
trên đó. #each
hàm nhận một khối và thực thi nó cho mỗi giá trị trong đối tượng. Khi triển khai nó trên danh sách được liên kết của chúng tôi, chúng tôi có thể sử dụng tính chất đệ quy của danh sách để làm lợi thế của chúng tôi bằng cách gọi khối đã truyền trên @head
của danh sách và gọi #each
trên @tail
, nếu nó tồn tại.
class LinkedList
def initialize(head, tail = nil)
@head, @tail = head, tail
end
def <<(item)
LinkedList.new(item, self)
end
def inspect
[@head, @tail].inspect
end
def each(&block)
block.call(@head)
@tail.each(&block) if @tail
end
end
Khi gọi #each
trên một phiên bản của danh sách được liên kết của chúng tôi, nó gọi khối đã chuyển với @head
hiện tại . Sau đó, nó gọi từng thứ trên danh sách được liên kết trong @tail
trừ khi đuôi là nil
.
irb> list = LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]
irb> list.each { |item| puts item }
42
12
73
=> nil
Bây giờ danh sách được liên kết của chúng tôi phản hồi với #each
, chúng ta có thể include Enumberable
để làm cho danh sách của chúng tôi có thể liệt kê được.
class LinkedList
include Enumerable
def initialize(head, tail = nil)
@head, @tail = head, tail
end
def <<(item)
LinkedList.new(item, self)
end
def inspect
[@head, @tail].inspect
end
def each(&block)
block.call(@head)
@tail.each(&block) if @tail
end
end
irb> list = LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]
irb> list.count
=> 3
irb> list.max
=> 73
irb> list.map { |item| item * item }
=> [1764, 144, 5329]
irb> list.select(&:even?)
=> [42, 12]
Trả lại Enumerator
phiên bản
Bây giờ chúng ta có thể lặp lại tất cả các giá trị trong danh sách liên kết của mình, nhưng chúng ta chưa thể xâu chuỗi các hàm có thể liệt kê được. Để làm điều đó, chúng tôi sẽ cần trả về một Enumerator
ví dụ khi #each
của chúng tôi hàm được gọi mà không có khối.
class LinkedList
include Enumerable
def initialize(head, tail = nil)
@head, @tail = head, tail
end
def <<(item)
LinkedList.new(item, self)
end
def inspect
[@head, @tail].inspect
end
def each(&block)
if block_given?
block.call(@head)
@tail.each(&block) if @tail
else
to_enum(:each)
end
end
end
Để bọc một đối tượng trong một liệt kê, chúng tôi gọi #to_enum
phương pháp trên đó. Chúng tôi chuyển :each
, vì đó là phương pháp mà điều tra viên nên sử dụng trong nội bộ.
Bây giờ, gọi #each
của chúng tôi phương thức không có khối sẽ cho phép chúng tôi liệt kê chuỗi.
irb> list = LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]
irb> list.each
=> #<Enumerator: [42, [12, [73, nil]]]:each>
irb> list.map.with_index.to_h
=> {42=>0, 12=>1, 73=>2}
Chín dòng mã và một dòng bao gồm
Bằng cách triển khai #each
bằng cách sử dụng Enumerable
mô-đun và trả về Enumerator
đối tượng của riêng chúng tôi, chúng tôi có thể tăng thêm danh sách được liên kết của mình bằng cách thêm chín dòng mã và một bao gồm.
Phần này kết thúc phần tổng quan của chúng ta về các phép liệt kê trong Ruby. Chúng tôi rất muốn biết bạn nghĩ gì về bài viết này hoặc nếu bạn có bất kỳ câu hỏi nào. Chúng tôi luôn theo dõi các chủ đề để điều tra và giải thích, vì vậy nếu có điều gì kỳ diệu trong Ruby mà bạn muốn đọc, đừng ngần ngại cho chúng tôi ngay tại @AppSignal!