Pull to refresh

PHP closures и передача аргументов по ссылке

Reading time8 min
Views4K
Ради интереса я решил сделать механизм замыканий (closures) на PHP. Я знаю, что в PHP 5.3 такой механизм есть, поэтому подчёркиваю — чисто из академического интереса. И моя любознательность дала (по крайней мере, для меня — кто-то с этим уже мог иметь дело) свои плоды — помимо собственно замыканий я получил интересный метод передачи аргументов.

Механизм замыканий состоит из вот такого класса:

  1. class closure
  2. {
  3.     private static $l_closures = array();
  4.     
  5.     public static function create($_scope, $_args, $_code)
  6.     {
  7.         $id = md5("closure::create>".serialize($_scope).$_args.$_code);
  8.         if(isset(self::$l_closures[$id]))
  9.         {
  10.             print "exists<br>";
  11.             return self::$l_closures[$id]["function"];
  12.         }
  13.         print "create new<br>";
  14.         $header_code = '$scope=closure::scope("'.$id.'");';
  15.         $header_end = 'unset($scope);';
  16.         if($_scope)
  17.         {
  18.             foreach($_scope as $key => $value)
  19.             {
  20.                 if($key == 'scope')
  21.                 {
  22.                     $header_end = '$'.$key.'=&$scope["'.$key.'"];';
  23.                 }
  24.                 else
  25.                 {
  26.                     $header_code .= '$'.$key.'=&$scope["'.$key.'"];';
  27.                 }
  28.             }
  29.         }
  30.         $header_code .= $header_end;
  31.         $closure = array(
  32.             "function" => create_function($_args, $header_code.$_code),
  33.             "scope" => is_array($_scope) ? $_scope : array()
  34.         );
  35.         self::$l_closures[$id] = $closure;
  36.         return $closure["function"];
  37.     }
  38.     
  39.     public static function bind($_scope, $_function)
  40.     {
  41.         $id = md5("closure::bind>".serialize($_scope).$_function);
  42.         if(isset(self::$l_closures[$id]))
  43.         {
  44.             print "exists<br>";
  45.             return self::$l_closures[$id]["function"];
  46.         }
  47.         print "create new<br>";
  48.         $closure = array(
  49.             "function" => create_function('', '$scope=closure::scope("'.$id.'");$debug=debug_backtrace();$args=$debug[0]["args"];call_user_func_array("'.$_function.'", array_merge($scope,$args));'),
  50.             "scope" => is_array($_scope) ? $_scope : array()
  51.         );
  52.         self::$l_closures[$id] = $closure;
  53.         return $closure["function"];
  54.     }
  55.     
  56.     public static function scope($_id)
  57.     {
  58.         if(isset(self::$l_closures[$_id]))
  59.         {
  60.             return self::$l_closures[$_id]["scope"];
  61.         }
  62.         return array();
  63.     }
  64. }
* This source code was highlighted with Source Code Highlighter.


и не из чего больше. Вот так это выглядит в действии:

  1. function outer1($_arg)
  2. {
  3.     $local_var = 70;
  4.     return closure::create(
  5.         /* scope */ array('_arg' => &$_arg, 'local_var' => &$local_var),
  6.         /* args */ '&$_inner_arg',
  7.         /* code */ 'print $_arg." ".$local_var." ".$_inner_arg; $_arg = 110; $_inner_arg = 120;'
  8.     );
  9. }
  10.  
  11. $a = 50;
  12. $inner1 = outer1(&$a);
  13. $inner1 = outer1(&$a);
  14. $inner1 = outer1(&$a);
  15. $ia = 90;
  16. $inner1(&$ia);
  17. print " ".$a." ".$ia."<br>";
  18.  
  19. function inner2($_arg, $local_var, &$_inner_arg)
  20. {
  21.     print $_arg." ".$local_var." ".$_inner_arg; $_arg = 110; $_inner_arg = 120;
  22. }
  23.  
  24. function outer2($_arg)
  25. {
  26.     $local_var = 70;
  27.     return closure::bind(
  28.         /* scope    */ array(&$_arg, &$local_var),
  29.         /* function */ 'inner2'
  30.     );
  31. }
  32.  
  33. $a = 50;
  34. $inner2 = outer2(&$a);
  35. $inner2 = outer2(&$a);
  36. $inner2 = outer2(&$a);
  37. $ia = 90;
  38. $inner2(&$ia);
  39. print " ".$a." ".$ia."<br>";
* This source code was highlighted with Source Code Highlighter.


Метод closure::create внешне выглядит так же, как и create_function — только в начале ему передаётся ещё один параметр, окружение в виде ассоциативного массива, ключами которого являются имена переменных (без знака $), а значениями — соответственно их значения.

closure::bind принимает окружение в виде массива со значениями переменных и функцию, котороая будет замкнута на окуржение. Функция должна принимать в начале переменные окружения, и лишь затем аргументы, с которыми её будут вызывать.

print «create new»; и print «exists»; введены, чтобы показать, что каждое замыкание создаётся только один раз.

Пока PHP 5.3 не осел ещё плотно на всех хостингах, можно использовать данный мехнизм.

Теперь обратите внимание на строчки 37-39. Для наглядности я напишу отдельный пример:

  1. function f()
  2. {
  3.     $args = func_get_args();
  4.     $args[0] = 10;
  5. }
  6. $a = 20;
  7. f(&$a);
  8. print $a."<br>";
  9. $a = 20;
  10. call_user_func_array('f', array(&$a));
  11. print $a."<br>";
* This source code was highlighted with Source Code Highlighter.


На выходе из функции f значение переменной $a будет, как и было, 20 — функция func_get_args возвращает не массив аргументов, а массив значений аргументов, т.е. ссылка пропала и мы изменили локальную переменную.

Теперь рассмотрим вот такой пример:

  1. function f()
  2. {
  3.     $debug = debug_backtrace();
  4.     $debug[0]["args"][0] = 10;
  5. }
  6. $a = 20;
  7. f(&$a);
  8. print $a."<br>";
  9. $a = 20;
  10. call_user_func_array('f', array(&$a));
  11. print $a."<br>";
* This source code was highlighted with Source Code Highlighter.


Пример выведет 10, т.е. то, чего мы ожидали! В debug_backtrace хранятся именно сами аргументы. call_user_func_array умеет передавать значения по ссылке, так что

  1. call_user_func_array('f', array(&$a));
* This source code was highlighted with Source Code Highlighter.


так же запишет в $a значение 10.

Я вполне допускаю, что для кого-то это окажется бояном, но, принимая во внимание то, что я этого раньше не знал и в интернете на эту тему найти ничего не мог, это может пригодиться и другим — потому не будьте слишком строги.

Благодарю за внимание!

UPD#1 05.12.09

Метод scope класса closure нужно изменить так:

  1. public static function scope($_id)
  2. {
  3.     $result = array();
  4.     if(isset(self::$l_closures[$_id]))
  5.     {
  6.         if(self::$l_closures[$_id]["scope"])
  7.         {
  8.             foreach(self::$l_closures[$_id]["scope"] as $key => $value)
  9.             {
  10.                 $result[$key] = &self::$l_closures[$_id]["scope"][$key];
  11.             }
  12.         }
  13.     }
  14.     return $result;
  15. }
* This source code was highlighted with Source Code Highlighter.


Когда ссылка на переменную остаётся единственной, PHP её зачем-то преобразует в само значение, т.е. локальные переменные уже будут передаваться из окружения не по ссылке, а по значению, что бесполезно. С такой же модификацией пример

  1. function inner2($_arg, $local_var, &$_inner_arg)
  2. {
  3.     $local_var ++;
  4.     print $_arg." ".$local_var." ".$_inner_arg; $_arg = 110; $_inner_arg = 120;
  5. }
  6.  
  7. function outer2($_arg)
  8. {
  9.     $local_var = 70;
  10.     return closure::bind(
  11.         /* scope    */ array(&$_arg, &$local_var),
  12.         /* function */ 'inner2'
  13.     );
  14. }
  15.  
  16. $a = 50;
  17. $inner2 = outer2(&$a);
  18. $inner2 = outer2(&$a);
  19. $inner2 = outer2(&$a);
  20. $ia = 90;
  21. $inner2(&$ia);
  22. print " ".$a." ".$ia."<br>";
  23. $inner2(&$ia);
  24. print " ".$a." ".$ia."<br>";
* This source code was highlighted with Source Code Highlighter.


выведет для $local_var 71 и 72 соответственно, тогда как без указанного изменения метода он вывел бы в обоих случаях 71.

UPD#2 05.12.09

В сточке 49 класса нужно писать return call_user_func_array.
Tags:
Hubs:
Total votes 29: ↑18 and ↓11+7
Comments3

Articles