Диапазоны, множества и итерация 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 — фундаментальный итератор, кормящий блоки элементами коллекции.

Проверьте себя
1. В чём разница между диапазонами (1..5) и (1...5)?
AНикакой разницы нет
B(1..5) включает 5, а (1...5) исключает 5
C(1...5) включает дробные числа
D(1..5) работает только с буквами
2. Что возвращает метод each после перебора коллекции?
AНовую преобразованную коллекцию
BИсходную коллекцию без изменений
Cnil
DКоличество элементов