Диапазоны, множества и итерация each
Помимо массивов и хэшей, у Ruby есть лёгкие специализированные коллекции: диапазоны для последовательностей и множества для уникальных элементов. А связывает всё метод each.
Суть: диапазон (Range,1..10) описывает последовательность от и до; множество (Set) хранит только уникальные элементы;each— фундаментальный итератор, на котором держится весь перебор в Ruby.
Диапазон — это компактное описание интервала. 1..10 включает 10 (две точки), а 1...10 — нет (три точки). Диапазоны экономны: они не хранят все числа в памяти, а вычисляют их по требованию.
(1..5).each { |n| print n } # => 12345
puts
puts (1..10).include?(7) # => true
puts ("a".."e").to_a.inspect # => ["a","b","c","d","e"]
puts (1...5).to_a.inspect # => [1,2,3,4] (без 5)
Разбор: множества и итерация each
Множество (Set) автоматически отбрасывает дубликаты и быстро отвечает на вопрос «есть ли элемент». Это идеальная структура для дедупликации и проверок принадлежности. А each — базовый кирпич: почти все остальные методы перебора построены на нём.
require "set"
seen = Set.new
seen << "a"
seen << "a" # дубликат игнорируется
seen << "b"
puts seen.size # => 2
puts seen.include?("a") # => true
fruits = ["яблоко", "груша", "слива"]
fruits.each_with_index do |fruit, i|
puts "#{i + 1}. #{fruit}"
end
Как работает под капотом
Метод each по очереди передаёт каждый элемент в блок (это анонимный кусок кода в фигурных скобках или do..end). Блок — отдельная большая тема следующего раздела, но идею «each кормит блок элементами» важно поймать уже сейчас: на ней стоят map, select и весь модуль Enumerable.
коллекция [ a, b, c ]
|
v
each отдаёт по одному --> блок { |x| ... }
|
a --> блок выполняется с x=a
b --> блок выполняется с x=b
c --> блок выполняется с x=c
|
v
each возвращает исходную коллекцию
Та же идея перебора с индексом на Python — это enumerate:
# Та же логика на Python ▶
fruits = ["яблоко", "груша", "слива"]
for i, fruit in enumerate(fruits, start=1):
print(f"{i}. {fruit}")
# 1. яблоко / 2. груша / 3. слива
Частые ошибки
- Путать .. и ... Две точки включают конец, три — исключают.
(1..3)это 1,2,3;(1...3)это 1,2. - Забывать require "set". Класс Set нужно подключить (в новых версиях Ruby он доступен сразу, но привычка явного require полезна).
- Ждать от each нового результата.
eachвозвращает исходную коллекцию, а не преобразованную. Для трансформации нуженmap(следующий раздел).
Best practices
- Используйте диапазоны для проверок принадлежности интервалу:
(1..100).include?(score)илиscore.between?(1, 100). - Берите Set, когда нужны уникальные элементы и быстрые проверки «есть ли» — это чище, чем массив с
uniqиinclude?. - Для перебора с номером используйте
each_with_index, а не ручной счётчик.
Глубже: бесконечные диапазоны и ленивость
Диапазоны скрывают пару возможностей, о которых новички редко знают, но которые отлично иллюстрируют дух языка. Во-первых, диапазоны бывают бесконечными: запись (1..) означает «от единицы и далее без верхней границы», а (..10) — «всё до десяти включительно». Бесконечные диапазоны удобны в паттерн-матчинге (проверить «больше N») и в срезах массивов. Во-вторых, диапазоны и коллекции поддерживают ленивые вычисления через lazy. Обычно цепочка map.select.first(5) сначала обработала бы всю коллекцию целиком — а с lazy Ruby вычисляет элементы по требованию и останавливается, как только наберёт нужные пять. Это позволяет работать даже с бесконечными последовательностями: (1..Float::INFINITY).lazy.select(&:even?).first(3) вернёт первые три чётных, не пытаясь перебрать бесконечность. Эти возможности не нужны каждый день, но знание о них меняет ваше представление о том, что коллекция не обязана существовать в памяти целиком — она может быть описанием, которое разворачивается ровно настолько, насколько вы попросили.
Итог. Диапазоны компактно описывают последовательности (две точки включают конец, три — нет), множества хранят уникальные элементы и быстро проверяют принадлежность, а each — фундаментальный итератор, кормящий блоки элементами коллекции.