Pull to refresh
VK
Building the Internet

Замыкания в php

Reading time 10 min
Views 48K
Не секрет, что в PHP 5.3 был введен ряд интересных новшеств. Разной степени полезности и скандальности. Возможно даже, что выпуск PHP 5.3 — хорошо спланированный PR-ход: самый большой список изменений за последние пять лет, оператор goto (sic!), пространства имен (namespaces) с синтаксисом «не как у всех», позднее статическое связывание (late static binding), более-менее честные анонимные (лямбда) функции (lambda functions), замыкания (closures).

О последних я и хочу рассказать. Справедливости ради хочу добавить, что в PHP 5.3 введено и много другого функционала, который делает этот релиз примечательным: модули Intl (интернационализация), Phar (PHP-архивы, наподобие JAR для JAVA), mysqlnd (новый драйвер для mysql), улучшения в SPL, общее увеличение производительности до 10% и много еще чего.

Замыкания (closures) и лямбда-функции (lambda functions) — нововведения PHP 5.3, вообще говоря, не являются передовым рубежом современных технологий. Удивительно, но и то и другое появилось в функциональном языке программирования LISP в конце 50-х, и было допилено напильником до состояния близкого к современному в 70-х годах. Таким образом, PHP подотстал с грамотной реализацией лет на 40.

Замыкания, если верить википедии — это процедуры, которые ссылаются на свободные переменные в своём лексическом контексте. Академически строго, но если бы я не знал о чем речь, никогда бы не понял из этого определения.

Во многих языках программирования, функция, чье определение вложено в другую функцию, так или иначе может иметь доступ не только к своим локальным переменным, но и к переменным родительской функции. Иногда для этого используется какой-то специальный синтаксис, иногда это работает без дополнительный усилий. Приведу пример на Javascript, потому что там все работает без какого-то особого синтаксиса (те же примеры, но на PHP будут позже):

//*** Пример 1j ***
function outer(x) //Определение внешней функции
{
        var y=2; //Локальная переменная внешней функции
        function inner(a) //Определение внутренней функции
        {
                var b=4; //Локальная переменная внутренней функции
                /* А дальше складываются переменные внутренней и
                 * внешней функций, как будто все они локальные
                 * переменные внутренней функции
                 */
                var res=x+y+a+b;
                alert(res); //Результат 10 в нашем примере.
        }
        inner(3); //Вызов внутренней функции
}
outer(1); //Вызов внешней функции, а она вызовет внутреннюю.


Итак, функция inner, без какого-то специального объявления свободно использует переменные внешней функции outer. Но это еще не замыкание.

Во многих языках программирования функция (имеется в виду не результат выполнения функции, а сама функция как исполняемый код, указатель на функцию), это некий объект особого типа. Как любой другой объект (строка, число, массив), функция может быть присвоена переменной, передана в другую функцию в качестве параметра и возвращена как результат выполнения другой функции. С этим связана и еще одна важная вещь: Функции при ее определении, можно не присваивать имени, а присвоить эту функцию переменной и вызывать ее через эту переменную. Сама же функция не имеет собственного имени, и поэтому остается "анонимной". Такие функции называют так же лямбда функциями.

// *** Пример 2j ***
adder=function(a,b) //У функции нет имени, но она присваивается переменной
{
        return a+b; //вернуть сумму
}
subber=function(a,b) { return a-b; } //То же самое, просто в одну строку
/*
 * Прошу обратить внимание - тут переменным adder и subber присваивается не
 * результат вычисления суммы или разности каких то чисел, а, грубо говоря,
 * сам код функции. Т.е. в переменной adder и subber сейчас не число, а код,
 * который можно вызвать так: x=adder(1,3); и вот x уже будет числом.
 */
function performAction(action, a, b) //Обычная функция с именем performAction
{
        var result=action(a,b); //предполагается что параметр action - это функция
        return result;
}
function makeDivider() //Обычная функция с именем makeDivider
{
        return function (a,b) //Возвращает безымянную функцию
        {
                return a/b;
        }
}
r1=adder(1,2); //Вызываем безымянную функцию через переменную. r1=1+2;
r2=performAction(subber,6,4); //Передаем функцию в другую функцию. r2=6-4;
r3=performAction(function(a,b) {return a*b;} ,5,6); //То же самое прямо на лету. r3=5*6;
divider=makeDivider(); //Вызываем функцию, которая возвращает функцию, сохраняем результат
r4=divider(16,4); //Вызываем функцию возвращенную функцией через переменную: r4=16/4;
r5=makeDivider()(32,16);//То же самое, но без промежуточной переменной: r5=32/16;
alert([r1,r2,r3,r4,r5]); //3,2,30,4,2


Если предыдущие примеры более-менее ясны, то замыкания тоже будут понятны, потому что являются, по сути, комбинацией описанных выше двух принципов:
  • Анонимная функция определенная внутри любой другой функции, имеет доступ к переменным родителя.
  • Если родительская функция вернет дочернюю функцию в качестве результата своей работы, переменные родительской функции останутся доступны в дочерней, несмотря на то, что родительская уже отработала и ее локальные переменные должны удалиться.

Т.е. вложенная функция как бы замыкает на себя переменные из родительских скопов, не давая им уничтожиться. А вот и полезный пример на Javascript:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Closure test</title>
</head>
<body>
<a href="#" id="link1">Нажми</a> <a href="#" id="link2">И меня нажми</a>
<div id="hide1">Скрой меня скорее</div>
<div id="hide2">И меня тоже скрой</div>

<script>
// *** Пример 3j ***
function getHider(id) //Передаем id элемента который надо скрыть
{
        return function()
        {
                document.getElementById(id).style.display='none';
                return false;
        }
        /*
         * У возвращаемой функции нет параметров, но параметр id родительской
         * функции как бы вшивается в нее перед возвратом ее в качестве результата.
         * Вызывая getHider с разными параметрами
         * можно в результате ее работы получить ряд очень похожих функций,
         * каждая из которых умеет скрывать один элемент.
         */
}
document.getElementById('link1').onclick=getHider('hide1');
document.getElementById('link2').onclick=getHider('hide2');
/*
 * Здесь в качестве обработчика события onclick двух ссылок мы
 * назначаем не функцию getHider, а результат ее работы, а им
 * является функция, внутрь которой вшит id='hide1' в первом случае
 * и id='hide2' во втором. Таким образом, функция назначенная
 * обработчиком внутри себя знает, какой элемент ей надо скрыть
 */
</script>

</body>
</html>


Лямбда-функции (и замыкания, будучи ими) как бы откладывают выполнение некоторого кода на более поздний срок. Поэтому часто используются в разработке интерфейсов пользователя в качестве обработчиков событий, что и было показано выше.

Вернемся к PHP. В нем все не так красиво как в Javascript. Во-первых, для того чтобы функция могла использовать переменные родительской функции, надо это явно указать в ее определении (это следует из идеологии языка), во-вторых работает это только с анонимными функциями и работает это все только с PHP 5.3. Пример 1j в исполнении PHP будет выглядеть как-то так:

// *** Пример 1p ***
function outer($x) //Определение внешней функции
{
        $y=2; //Локальная переменная внешней функции
        $inner=function ($a) use ($x, $y) //Определение внутренней функции
        {
                $b=4; //Локальная переменная внутренней функции
                /* А дальше складываются переменные внутренней и
                 * внешней функций, как будто все они локальные
                 * переменные внутренней функции
                 */
                $res=$x+$y+$a+$b;
                echo $res; //Результат 10 в нашем примере.
        };
        $inner(3); //Вызов внутренней функции
}
outer(1);


А наш пример 2j как то так:

// *** Пример 2p ***
$adder=function($a,$b) //У функции нет имени, но она присваивается переменной
{
        return $a+$b; //вернуть сумму
};

$subber=function($a,$b) { return $a-$b; }; //То же самое, просто в одну строку

function performAction($action, $a, $b) //Обычная функция с именем performAction
{
        $result=$action($a,$b); //предполагается что параметр action - это функция
        return $result;
}

function makeDivider() //Обычная функция с именем makeDivider
{
        return function ($a,$b) //Возвращает безымянную функцию
        {
                return $a/$b;
        };
}

$r1=$adder(1,2); //Вызываем безымянную функцию через переменную. r1=1+2;
$r2=performAction($subber,6,4); //Передаем функцию в другую функцию. r2=6-4;
$r3=performAction(function($a,$b) {return $a*$b;} ,5,6); //То же самое прямо на лету. r3=5*6;
$divider=makeDivider(); //Вызываем функцию, которая возвращает функцию, сохраняем результат
$r4=$divider(16,4); //Вызываем функцию возвращенную функцией через переменную: r4=16/4;
//А такие вещи как в r5 в PHP вообще не прокатывают.
//$r5=makeDivider()(32,16);//То же самое, но без промежуточной переменной: r5=32/16;
$r5='php fail';
echo "$r1,$r2,$r3,$r4,$r5"; //3,2,30,4,php fail


Анонимные функции в PHP позволяют упростить и сделать нагляднее применение различных встроенных функций использующих callback, например:

//По старинке:
function cmp($a, $b)
{
    return($a > $b);
}
//тут еще всякий код
uasort($array, 'cmp'); //А тут использование этой функции

//По новому:
uasort($array, function($a, $b) { return($a > $b);});


В старом варианте код функции сравнения и код, где она используется, могут оказаться разнесены довольно далеко друг от друга, к тому же эта функция используется только один раз, но ей надо давать имя и оно может случайно вступить в конфликт с именем другой функции. В новом варианте, все гораздо приятнее, весь код размещен компактно и пространство имен тоже не засоряется.

Может показаться, что анонимные функции уже были в предыдущих версиях PHP. Разве create_function, это не оно — то самое? И да, и нет. create_function при каждом вызове создает новую настоящую именованную функцию в глобальном пространстве имен(sic!), но ее имя начинается с \0 (нулевого байта) и поэтому не может вступить в конфликт с обычными функциями. Но разве можно назвать такую функцию действительно анонимной? Кстати, create_function возвращает строку с именем этой «анонимной» функции. В остальном, использование действительно похоже на использование анонимных функций в PHP 5.3:

//Новый старый лад, работает даже в PHP 4!:
uasort($array, create_function('$a, $b','return $a > $b;'));


Зачем нужны замыкания в PHP я понимаю слабо. Для простого сохранения состояния функции между вызовами? Например так:

function getModernIncrementer()
{
    $x=0;
    return function() use(&$x) //Обязательно указать передачу по ссылке!
    {
       return $x++;
    };
}
$incrementer2=getModernIncrementer();
echo $incrementer2(), $incrementer2(), $incrementer2();//012


Но для этого существует static переменные. Их использование проще, удобнее и требует меньше кода:

function incrementer()
{
    static $x=0;
    return $x++;
}
echo incrementer(),incrementer(),incrementer(); //012


Для сложного сохранения состояния между вызовами существует ООП. Кстати, анонимные функции в PHP 5.3 все же не совсем честные функции! На поверку эта функция оказывается… оказывается… Внимание, фокус:

var_dump(function(){return 1;}); // object(Closure)#1 (0) { }


Функция оказывается объектом класса Closure — и вот оно это ваше ООП. А чтобы объект всячески корчил из себя функцию, разработчики PHP придумали специальный «магический» метод __invoke():

class Test
{
    public function __invoke ()
    {
        return 123;
    }
}
$func = new Test();
echo $func(); //123


Подводя итоги: Анонимные функции и замыкания — очень мощный инструмент. И как любой мощный инструмент, требуют очень аккуратного применения. Они могут существенно упростить код программы, сделать его красивее и повысить его читаемость, а могут ровно наоборот — сделать его абсолютно нечитаемым. Замыкания очень часто применяются в качестве функций обратного вызова при программировании графических интерфейсов. Очень удобно вешать их в качестве обработчиков нажатий на разные кнопочки. Но на PHP практически никто не делает программы с GUI (хотя умельцы существуют), и на это есть некоторые причины — PHP все же язык веб-сценариев, а не десктоп приложений. Поэтому анонимные функции хороши в preg_replace_callback, array_filter и тому подобных функциях, а замыкания следует оставить для Javascript, Python и других языков, где они реализованы действительно хорошо и где реально нужны для использования в GUI.

В заключение: Хочу привести законченный пример использования замыканий с JQuery (при работе с этой библиотекой они используются широко). В примере много вложенных функций, обработчиков, и в них, чтобы избежать постоянных (хоть и быстрых, но некрасивых) поисков DOM-объектов по параметрам, всюду используются переменные уже содержащие нужный объект, доставшиеся от внешней функции (это и есть замыкание). Аналогичный код используется в админке проекта hi-tech.mail.ru, и позволяет динамически добавить возможность редактирования и сохранения через AJAX отдельных блоков страницы (наподобие того, что делает плагин Editable), при этом изначально HTML код страницы не содержит никакой особой разметки для этого.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>dynaedit</title>
<style type="text/css">
.msg, .edt {font-family: "Verdana", "Arial", "Helvetica", sans-serif; font-size: 10pt}
.msg {background-color:#DDf; border: 1px solid #000; padding:2px}
.edt {margin:-2px 0 -2px -1px; padding:0; width:100%; height:100%; border 0}
</style>
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"></script>
<script>
$(document).ready(function()
{
	$("#admin").click(function()
	{
		$(this).remove();
		$("p.msg").each(function()
		{
			var msg_id=this.id.substr(1); //Откуда здесь this? Из each!
			var msg=$(this); //Объект JQuery из DOM элемента <p>
			$("<input/>", //Добавим под ним кнопку
			{
				type: "button",
				value: "Править запись #"+msg_id,
				click: function StartEdit() //При нажатии будем подменять текст внутри p на поле textarea для редактирования
				{
					var edt=$("<textarea/>", //Создаем textarea, помещаем вместо текста в p
					{
						'class':'edt',
						value:msg.html().replace(/<br[^>]*>/gim,"\n"), //откуда msg? из родительской функции => замыкание
						height:msg.height(),
					}).appendTo(msg.empty());
					$(this).val("Сохранить запись #"+msg_id).unbind().click(function() //Меняем надпись и обработчик на кнопке
					{
						//$.post("/ajax/savemessage",{msg_id:msg_id, msg:edt.val()}, function(data){}); //Отправляем на сервер
						msg.html(edt.remove().val().replace(/\n/gm,"<br />")); //Убираем textarea, возвращаем текст
						$(this).val("Править запись #"+msg_id).unbind().click(StartEdit);//Меняем надпись, ставим старый обработчик на кнопке
						return false;
					});//Save
					return false;
				} //StartEdit()
			}).insertAfter(this);//<input/>
		});//$("p.msg").each
		return false;
	});//$("#admin").click
});//$(document).ready
</script>
</head>

<body>
<p id="p1234" class="msg">Это первое сообщение<br />Его можно редактировать!</p>
<p id="p1235" class="msg">Это второе сообщение<br />Его тоже можно редактировать!<br />P.S. Just 4 lulz</p>
<p><a href="#" id="admin">Я админ и хочу редактировать!</a></p>
</body>

</html>


С уважением,
Отдел исследований и разработки Mail.Ru
Tags:
Hubs:
+76
Comments 69
Comments Comments 69

Articles

Information

Website
vk.com
Registered
Founded
Employees
5,001–10,000 employees
Location
Россия
Representative
Миша Берггрен