блока, чтобы метод map
вызывал его для каждого элемента массива. А зачем передавать self
в виде сообщения элементу массива? Затем, что объект proc
является замыканием и, следовательно, помнит контекст, в котором был создан. А в момент создания self
был ссылкой на символ, для которого вызывался метод to_proc
.
6.2. Диапазоны
Понятие диапазона интуитивно понятно, но и у него имеются некоторые неочевидные особенности и способы применения. Одним из самых простых является числовой диапазон:
digits = 0..9
scalel = 0..10
scale2 = 0...10
Оператор ..
включает конечную точку, а оператор ...
не включает. (Если это вас неочевидно, просто запомните.) Таким образом, диапазоны digits
и scale2
из предыдущего примера одинаковы.
Но диапазоны могут состоять не только из целых чисел — более того, не только из чисел. Началом и концом диапазона в Ruby может быть любой объект. Однако, как мы вскоре увидим, не все диапазоны осмыслены или полезны.
Основные операции над диапазоном — обход, преобразование в массив, а также выяснение, попадает ли некоторый объект в данный диапазон. Рассмотрим разнообразные варианты этих и других операций.
6.2.1. Открытые и замкнутые диапазоны
Диапазон называется
r1 = 3..6 # Замкнутый.
r2 = 3...6 # Открытый.
a1 = r1.to_a # [3,4,5,6]
а2 = r2.to_a # [3,4,5]
Нельзя сконструировать диапазон, который не включал бы начальную точку. Можно считать это ограничением языка.
6.2.2. Нахождение границ диапазона
Методы first
и last
возвращают соответственно левую и правую границу диапазона. У них есть синонимы begin
и end
(это еще и ключевые слова, но интерпретируются как вызов метода, если явно указан вызывающий объект).
r1 = 3..6
r2 = 3...6
r1a, r1b = r1. first, r1.last # 3,6
r1c, r1d = r1.begin, r1.end # 3,6
r2a, r2b = r1.begin, r1.end # 3,6
Метод exclude_end?
сообщает, включена ли в диапазон конечная точка:
r1.exclude_end? # false
r2.exclude_end? # true
6.2.3. Обход диапазона
Обычно диапазон можно обойти. Для этого класс, которому принадлежат границы диапазона, должен предоставлять осмысленный метод succ
(следующий).
(3..6).each {|x| puts x } # Печатаются четыре строки
# (скобки обязательны).
Пока все хорошо. И тем не менее будьте очень осторожны при работе со строковыми диапазонами! В классе String
имеется метод succ
, но он не слишком полезен. Пользоваться этой возможностью следует только при строго контролируемых условиях, поскольку метод succ
определен не вполне корректно. (В определении используется, скорее, «интуитивно очевидный», нежели лексикографический порядок, поэтому существуют строки, для которых «следующая» не имеет смысла.)
r1 = '7'..'9'
r2 = '7'..'10'
r1.each {|x| puts x } # Печатаются три строки.
r2.each {|x| puts x } # Ничего не печатается!
Предыдущие примеры похожи, но ведут себя по-разному. Отчасти причина в том, что границы второго диапазона — строки разной длины. Мы ожидаем, что в диапазон входят строки '7'
, '8'
, '9'
и '10'
, но что происходит на самом деле?
При обходе диапазона r2
мы начинаем со значения '7'
и входим в цикл, который завершается, когда текущее значение окажется больше правой границы. Но ведь '7'
и '10'
— не числа, а строки, и сравниваются они как строки, то есть лексикографически. Поэтому левая граница оказывается больше правой, и цикл не выполняется ни разу.
А что сказать по поводу диапазонов чисел с плавающей точкой? Такой диапазон можно сконструировать и, конечно, проверить, попадает ли в него конкретное число. Это полезно. Но обойти такой диапазон нельзя, так как метод succ
отсутствует.
fr = 2.0..2.2
fr.each {|x| puts x } # Ошибка!
Почему для чисел с плавающей точкой нет метода succ
? Теоретически можно было бы увеличивать число на некоторое приращение. Но величина такого приращения сильно зависела бы от конкретной машины, при этом даже для обхода «небольшого» диапазона понадобилось бы гигантское число итераций, а полезность такой операции весьма сомнительна.
6.2.4. Проверка принадлежности диапазону
Зачем нужен диапазон, если нельзя проверить, принадлежит ли ему конкретный объект? Эта задача легко решается с помощью метода include?
:
r1 = 23456..34567
x = 14142