Блоки, процедуры, лямбды и методы, доступные в Ruby, все вместе называются замыканиями.
Что такое закрытие?
Проще говоря, все переменные, на которые есть ссылка в блоке, остаются доступными в течение всего жизненного цикла блока, даже если они определены в другой области. Его можно определить в одной области и использовать в другой. Лучший способ запомнить это - представить закрытие как маленький рюкзак, в котором хранятся все переменные, которые были в области видимости при создании метода. Вы можете перенести рюкзак в другой прицел и использовать там указанные переменные, потому что они все еще находятся в рюкзаке.
В Ruby есть замыкания в виде блоков, procs, и лямбда-выражений em. >.
Насколько они разные?
Блоки используются для передачи, ну, блоков кода методам; procs и lambdas позволяют хранить блоки кода в переменных.
Блок - это просто кусок кода, заключенный в фигурные скобки или ключевые слова do и end. Вы можете думать о блоке как о теле метода. Как и метод, он может принимать параметры (в начале блока между вертикальными полосами). Также как и метод, тело блока сохраняется для последующего вызова. Они появляются в исходном коде Ruby сразу после вызова какого-либо другого метода, и вы можете почти думать о них как об одном дополнительном параметре, передаваемом методу. В зависимости от того, как написан блок, вы можете передать в свои блоки одну или несколько переменных.
Давайте посмотрим на простой пример:
Блок вызывается для каждого элемента массива. Каждый элемент передается в блок (или передается) как параметр num. Переменная, определенная внутри блока, является локальной для блока.
Затем мы выполняем операцию с помощью итератора. Итератор - это просто метод, который может вызывать блок кода (например, старый добрый each) для каждого элемента коллекции (в нашем случае - массива).
Что делает блоки интересными, так это то, что вы можете передавать им параметры и получать от них значения. В следующем примере мы можем увидеть простой метод, который возвращает элементы ряда Фибоначчи до определенного значения:
В этом примере у оператора yield есть параметр, значение которого передается в блок. Переменная «num» получает значение, переданное в yield, и распечатывает последовательность.
Блок также может возвращать значение методу. Значение последнего выражения, вычисленного в блоке, передается обратно в метод как значение yield. Давайте рассмотрим подробнее, проанализировав .find:
В приведенном выше примере каждый элемент массива передается в связанный блок и возвращает элемент, если указанное условие истинно.
Хранение блоков как объектов
Но подождите, это еще не все. Вы также можете преобразовать блок в объект, сохранить его в переменной и передать или вызвать позже.
Ruby предоставляет не один, а два встроенных метода преобразования блока в объект - lambda и Proc.new.
Proc (сокращение от процедуры)
Самый простой способ определить proc - сказать, что процесс - это блок, хранящийся в переменной. Помните, как вам говорили, что все в Ruby - это объект? Что ж, блока действительно нет, а прока есть. В частности, процесс - это блок, превращенный в объект класса Proc, поэтому создание процедуры очень похоже на создание экземпляра класса.
Procs - это тип закрытия в Ruby, и они ведут себя очень похоже на блоки, но, в отличие от блока, процедура выполняется путем вызова для него метода .call и т. д. методу можно передать более одного процесса.
Здесь мы назначаем блок экземпляру класса Proc и назначаем его переменной. Затем мы вызываем для него метод .call:
Procs также может принимать аргументы, которые передаются в метод .call:
Написание собственных методов приема блоков требует использования ключевого слова yield, как показано в примерах выше, где мы исследовали .find
Если вам нужно использовать блок, который проходит через объект, детализируя его различные особенности, такие как имена и значения атрибутов, написание обширного блока в строке везде, где его нужно использовать, звучит очень неаккуратно и непрактично, и создание процедуры, которую вы можете вызвать всякий раз, когда это необходимо, может помочь вам ОСУШИТЬ код. С другой стороны, важно отметить, что преобразование блока в процесс приводит к падению производительности, и неявные блоки могут быть лучшим вариантом, если производительность является проблемой.
Символы, хэши и методы могут быть преобразованы в процедуры с помощью их методов #to_proc. Часто это используется для передачи процесса, созданного из символа, в метод.
В этом примере показаны три эквивалентных способа вызова .capitalize для каждого элемента массива fruit. В первом передается символ с префиксом амперсанда, который автоматически преобразует его в процесс, вызывая его метод .to_proc.
Хотя это упрощенный пример, реализация Symbol.to_proc показывает, что происходит под капотом. Метод возвращает процесс, который принимает один аргумент и отправляет ему self. Поскольку self является символом в этом контексте, он вызывает метод String.capitalize.
Добавив & перед параметром, Ruby преобразует его в процесс и назначит его параметру. Каждый раз, когда Ruby видит & для параметра, он хочет, чтобы этот параметр был Proc.
То, что делает Symbol.to_proc, довольно умно. Он пытается вызвать метод с тем же именем (в нашем примере .capitalize) для данного объекта.
Мы можем явно принять блок в методе, добавив его в качестве аргумента с помощью параметра амперсанда (обычно называемого & block). Поскольку теперь блок является явным, мы можем использовать метод .call непосредственно для результирующего объекта, вместо того, чтобы полагаться на yield.
Аргумент & block не является правильным аргументом, поэтому вызов этого метода со всем, что не является блоком, приведет к ошибке ArgumentError.
Лямбды
Лямбда не сильно отличается от процедуры.
Давайте посмотрим, как каждая лямбда передается в перечислитель. & Перед тем, как он превращает его в блок, который каждый ожидает в качестве аргумента.
Лямбды и процедуры используются почти как взаимозаменяемые, с некоторыми ключевыми отличиями. Лямбды больше похожи на «обычные» методы в двух отношениях:
- они определяют количество переданных аргументов при вызове и
- они используют «нормальную» доходность.
При вызове лямбды, которая ожидает аргумент без аргумента, или если вы передаете аргумент лямбде, которая его не ожидает, Ruby вызывает ArgumentError.
Кроме того, лямбда обрабатывает ключевое слово return так же, как и метод. При вызове процедуры программа передает управление блоку кода в процедуре. Итак, если процедура возвращается, возвращается текущая область видимости. Если процедура вызывается внутри функции и вызывает return, функция также немедленно возвращается.
Эта функция передаст управление процессу, поэтому, когда она вернется, функция вернется. Вызов функции в этом примере никогда не распечатает результат и вернет 10.
При использовании лямбда он будет напечатан. Вызов return в лямбда-выражении будет вести себя как вызов return в методе, поэтому наша переменная a заполняется значением 10, и строка выводится на консоль.