22.11.2016   3758

Управление памятью в PHP: передача в функцию, классы, массивы

Передача значений в функции в PHP: управляем памятью

Всем привет!

В предыдущей статье мы познакомились с механизмом copy-on-write в PHP, благодаря которому копирование значений происходит только тогда, когда это действительно необходимо.

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

Итак, начнём. Помните в прошлой статье мы говорили о таблице символов, в которой хранятся имена переменных? Так вот таких таблиц на самом деле может быть несколько.

Давайте рассмотрим следующий код:

function func($var)
{
    $var++;
}
$var = 123;
func($var);
echo $var;

Результат:

123

В данном случае одна из этих таблиц предназначена для хранения имён в глобальной области видимости, а вторая будет хранить имена, использующиеся внутри функции func().
Что же произойдёт в момент вызова функции func()? А произойдёт нечто похожее на присваивание, которое мы уже рассматривали. В момент передачи аргумента в функцию произойдёт создание нового имени в таблице символов функции, это имя в данный момент будет ссылаться на то же значение, что и глобальная переменная $var.

Структура zval при этом сейчас содержит информацию о двух ссылках на данное значение. И лишь в момент модификации переменной внутри функции значение будет скопировано и изменено, а имя из таблицы символов функции начнёт ссылаться на это новое значение.
Как мы видим, при передаче в функцию происходит нечто похожее на процесс присваивания переменной, и снова в действии механизм copy-on-write.

Рассмотрим теперь следующий пример:

<?php
function func(&$x)
{
    $x++;
}
$x = 123;
func($x);
echo $x;

Он выведет:

124

От предыдущего кода отличается лишь наличием знака & перед аргументом функции. Рассмотрим, что происходило со значением zval. После определения глобальной переменной $x происходит её передача в функцию по ссылке. Число ссылок на значение установилось равным двум, а флаг, говорящий о том, что значение используется по ссылке, даёт PHP понять, что перед модификацией значение не нужно копировать, а стоит изменять непосредственно его. В общем, история та же, что и с простым присваиванием по ссылке.

Объекты

Рассмотрим такой код:

<?php
function func($obj)
{
    $obj->prop = 123;
}
$obj = new stdClass();
$obj->prop = 1;
func($obj);
var_dump($obj);

Вывод будет следующим:

class stdClass#1 (1) {
public $prop =>
int(123)
}

После передачи объекта в функцию и изменению его свойства внутри неё, мы видим что свойство объекта изменилось и вне функции. Отсюда, как Вы уже догадались, можно сделать вывод, что объекты всегда передаются по ссылке, независимо от того, был ли знак & перед аргументом, или нет. Да и наверняка Вы уже до этого слышали, что объекты в PHP всегда передаются по ссылке. Так вот это не так :)
Но мы же только что видели обратное, как это не так? Объясняю. Видели на самом деле мы лишь изменение свойства объекта, а не объекта как такового. Дело в том, что значение объекта в PHP представляет собой лишь идентификатор объекта, и этот идентификатор уже используется для поиска данных этого объекта. Таким образом в момент передачи в функцию объекта поля zval принимают следующие значения:

  • число ссылок — 2;
  • объект передан по ссылке — 0.

А в момент обращения к свойству!!! объекта происходит поиск свойства по идентификатору объекта и его модификация. Значение самого объекта при этом остаётся неизменным. Проверить это можно следующим кодом:

<?php
function func($obj)
{
    $obj = 123;
}
$obj = new stdClass();
$obj->prop = 1;
func($obj);
var_dump($obj);

Вывод будет следующим:

class stdClass#1 (1) {
public $prop =>
int(1)
}

Как видим, объект не изменился. А всё дело в том, что внутри функции в момент присваивания имени из таблицы символов функции другого значения произошло создание нового значения, а у zval объекта число ссылок уменьшилось до 1.
Если теперь всё же добавить & перед аргументом функции:

<?php
function func(&$obj)
{
    $obj = 123;
}
$obj = new stdClass();
$obj->prop = 1;
func($obj);
var_dump($obj);

То вывод будет следующим:

int(123)

Объяснять, думаю, не нужно. Стандартное поведение при передаче по ссылке, рассмотренное ранее.
Такое поведение объектов, когда при передаче в функцию или присваивании переменной мы можем работать с его свойствами, не производя при этом его копирования позволяет реализовать в PHP эффективное внедрение зависимостей (dependency injection).
Пример:

<?php
class Db {
    private $dbhost;

    public function __construct(Config $config)
    {
        $this->dbhost = $config->db->host;
    }
}

Здесь в конструкторе класса Db происходит передача объекта класса Config. Мы можем использовать его свойства (как читать, так и изменять) не выполняя при этом копирования и не расходуя память. Да, вот такой вот PHP умный и хороший.
Стоит отметить, что иногда нам может потребоваться скопировать объект, и работать с его копией, внося изменения только в ней. Для этого используется конструкция clone.
Использоваться она может так:

$obj2 = clone $obj1;

Или даже непосредственно при передаче в функцию:

func(clone $obj);

Массивы

Отдельно стоит упомянуть о массивах. Они, так же как и скаляры и объекты по умолчанию передаются по значению. Кроме одного единственного случая — если на копию массива при передаче его в функцию нам не хватит памяти, он будет передан по ссылке, и мы при этом никак не будем об этом уведомлены :)
Для работы с массивами в PHP имеется несколько конструкций и функций. Одни функции работают по значению, возвращая в результате новый массив, другие позволяют работать по ссылке, позволяя здорово экономить память. Стоит отметить, что зачастую именно при работе с массивами приходится задумываться о расходуемой памяти. Рассмотрим несколько примеров.
Возьмём функцию array_map($callback, array $array)
Она возвращает массив, содержащий элементы $array после их обработки callback-функцией:

$arr1 = [1, 2, 3];
$arr2 = array_map(function ($item) { return $item * 2; }, $arr1);

После выполнения данного кода будет создан новый массив $arr2, содержащий в себе элементы $arr1, умноженные на 2. При этом в момент выполнения функции значения массива $arr1 копировались и в итоге заняли такое же количество памяти, что и исходный массив.

Теперь рассмотрим функцию array_walk(array &$array, $funcname) {}
Как мы видим, массив она принимает по ссылке. Функция занимается тем что применяет заданную пользователем функцию к каждому элементу массива. Как Вы уже догадались, копирования значений при этом не происходит.

$arr1 = [1, 2, 3];
array_walk($arr1, function (&$item) { $item *= 2; });

Данный код пройдётся по каждому значению $arr1 и изменит непосредственно его значение. С точки зрения производительности по памяти, этот вариант более предпочтителен.
Аналогично можно поступить и с foreach. Использовать элементы массива по значению:

foreach ($arr as $item) {
…
}

или по ссылке:

foreach ($arr as &$item) {
...
}

На этом данная статья заканчивается. Ждите следующего материала на тему сборщика мусора и циклических ссылок. Спасибо за прочтение, всего Вам наилучшего ;)

Ссылки на статьи этой серии:
Управление памятью в PHP. Начало
>Управление памятью в PHP. Передача в функцию, классы, массивы
Управление памятью в PHP. Циклические ссылки и сборщик мусора

Комментарии

Чтобы написать комментарий нужно войти на сайт.
Или получить доступ прямо сейчас:


Нажимая эту кнопку
Вы принимаете политику конфиденциальности