Когда я переключался на Ruby из фона PHP, одной из самых серых областей для меня были блоки. В Ruby повсюду есть блоки, и их плохое понимание остановит ваше обучение на этапе «новичка». Когда вы только начинаете работать с Ruby, вы неизбежно будете использовать блоки исключительно для итерации.
Привет блоки
3.times {|n| puts "Printed #{n} times" }
Приведенный выше код должен быть «привет миром» блоков, и из этой единственной строки мы узнаем много нового о Ruby. У нас есть число ( Fixnum
times
В случае, если вы пропустили важный бит, «метод, который принимает блок». На данный момент, просто слепо принять блок — это просто кусок кода, который мы можем передать, и который может быть определен только после вызова метода, он может принимать параметры и имеет 2 стиля синтаксиса. У нас есть блоки, определенные в фигурных скобках, как указано выше, или определенные в do
end
Обычной практикой является использование фигурных скобок, если содержимое блока состоит из одной строки (хотя Авди Гримм дает хороший аргумент для их смешивания в зависимости от того, что делает блок ). В любом случае, приведенный выше код будет так же правильно написан так:
3.times do |n|
puts "Printed #{n} times"
end
В PHP мы совсем не привыкли к блокам. На самом деле, можно сказать, что для чего-то настолько тривиального, было бы гораздо лучше просто использовать наш надежный цикл (для каждого). Но слишком много раз я сталкивался со следующим.
for($i = 0; $i < count($values); $i++)
Теперь в этой единственной строке кода есть множество проблем (сейчас она используется рядом с вами). Для начала count()
Но даже это не самая большая моя неприятность. Использование переменной $i
$i
Я сам виню C, $i
Если рассматривать прошлую область видимости, семантику или даже реализовывать это как цикл foreach, у нас все еще есть то, что выглядит как внешняя циклическая конструкция, вмешивающаяся в наши данные. Я писал такие вещи изо дня в день. Но в наши дни, проведя некоторое время в Ruby в реальном времени, он просто не может использовать циклические конструкции, такие как for
Помещая бизнес в блок, он становится чище, и мы получаем закрытие бесплатно (локальные переменные блоков находятся только в области действия блока).
Закрытие PHP
В PHP 5.3 были введены замыкания. Это сделало операцию, которую мы ожидаем со times
class MyInt {
protected $value = 0;
public function __construct($value) {
$this->value = $value;
}
public function times($pBlock) {
for($i = 0; $i < $this->value; $i++) {
$pBlock($i);
}
}
}
$echo_statement = function($value) {
echo $value;
};
$val = new MyInt(3);
$val->times($echo_statement);
Таким образом, мы в основном создали новый класс, который будет действовать как Ruby Fixnum
$i
Это довольно мощная штука, но посмотрите на циклы кода, через которые мы должны перейти, чтобы получить что-то такое же лаконичное, как блок Ruby.
Пролить бобы на методы
Если бы мы создали нашу собственную реализацию times
class Fixnum
def times
for i in (1..self)
yield i
end
end
end
5.times { |n| puts "Im Hi #{n}" }
Выше мы снова открыли класс Fixnum
times
Поэтому мы написали цикл for, который перебирает диапазон от 1 до чего угодно. Самая интересная строка в приведенном выше: yield i
Метод yield
Подожди минутку. Где мы проходили мимо указанного блока? Ну, каждый метод принимает блок (независимо от того, определили вы его или нет). Поэтому нам даже не нужно объявлять блок как параметр для нашего метода times
Он знает, что блок может быть там. Но мы решили позволить ему работать с использованием yield
Так что же произойдет, если мы забудем передать блок? Ну, ответ будет LocalJumpError : no block given
Однако мы можем дать этому методу немного больше гибкости, используя block_given?
,
class Fixnum
def times
if block_given?
for i in (1..self)
yield i
end
else
Enumerable::Enumerator.new self
end
end
end
5.times { |n| puts "Im Hi #{n}" }
Im Hi 0
Im Hi 1
Im Hi 2
Im Hi 3
Im Hi 4
=> 1..5
5.times
=> #<Enumerable::Enumerator:0xb737acb4>
Здесь мы добавили немного криволинейного шара, возвращая новый объект Enumerable::Enumerator
Это было главным образом, чтобы подражать реальной реализации times
Он вернет объект Enumerator, что позволит нам сделать следующее:
enumerator = 5.times
=> #<Enumerable::Enumerator:0xb737acb4>
enumerator.max
=> 4
enumerator.min
=> 0
enumerator.minmax
=> [0, 4]
Блок Сфера
Блоки являются замыканиями, они берут кусок кода и его локальную среду (переменные и т. Д.) И сохраняют его для выполнения позже. Поэтому лучше поговорить о влиянии блоков на окружающую среду. В соответствии с тем, что мы, естественно, ожидаем, переменные, определенные внутри блока, выбрасываются после выполнения блока. Но есть некоторые непредсказуемые поведения в зависимости от того, какую версию Ruby мы используем при определении блоков:
n = 10
y = 0
[1,2,3].each do |n|
x = n
y = y + n
end
y.inspect
# => "6"
n.inspect
# => "3" -- In 1.9.X this will be 10
defined? x
# => nil
Как и ожидалось, переменная x
Однако мы можем видеть n
Я не знаю о вас, но я чувствую, что это поведение неестественно. Я слышал случаи, когда это действительно «особенность», но мне еще не приходилось сталкиваться с такими ситуациями. Наконец, y
Я упоминаю это поведение с параметрами блока ( n изменяется) для тех из вас, кто играет с Ruby 1.8.x. В Ruby 1.9.x параметр n остается неизменным для блока. Я предпочитаю такое поведение и предлагаю вам также работать с последней и лучшей версией Ruby.
Завершение
- Блоки — это кусок кода, который позже откладывается для исполнения (продолжайте, просто скажите «закрытие»)
- Все методы в Ruby принимают блок по умолчанию, и мы можем вызвать блок, используя
yield
- Блоки работают в собственной закрытой среде
- Переменные, локальные для блока, выбрасываются после выполнения
- Переменные, определенные до определения блока, заключены в область видимости блоков.
Можно с уверенностью сказать, что мы добились хороших результатов на пути изучения блоков, в следующий раз мы рассмотрим магию lamda
Proc