Статьи

Сохранить память путем переключения на генераторы

После выхода PHP 5.5 у нас теперь есть доступ к генераторам . Генераторы — довольно интересная языковая функция, которая позволяет вам сэкономить немного памяти, если они используются в нужных местах.

Чтобы продемонстрировать, представьте, что в нашей модели есть функция, которая выбирает записи из базы данных:

<?php

function getArticles() {

   $articles = [];
   $result = $this->db->query('SELECT * FROM articles');

   while($record = $result->fetch(PDO::FETCH_ASSOC)) {

       $articles[] = $this->mapToObject($record);

   }

   return $articles;

}

?>

Предыдущий пример — довольно распространенный шаблон в приложении CRUD. В этом примере мы извлекаем список записей из базы данных и применяем некоторую функцию к записям перед их возвратом.

Где-то еще в приложении, это может быть использовано следующим образом:

<?php

foreach($model->getArticles() as $article) {

    echo "<li>", htmlspecialchars($article->title), "</li>";

}
?>

Проблема с памятью

Если наша articlesтаблица содержит много записей, мы сохраняем каждую из этих $articlesпеременных. Это означает, что ваше «пиковое использование памяти» зависит от количества записей. Для многих небольших вариантов использования это может и не быть проблемой, но иногда вам приходится работать с большим количеством данных.

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

Каждая из этих функций, как правило, имеет цикл (foreach) и будет увеличивать использование памяти по мере увеличения объема данных.

Генераторы на помощь

Генератор позволяет вам возвращать «подобный массиву» результат из функции, но возвращать только одну запись за раз.

Можно getArticles()относительно легко преобразовать нашу функцию в генератор. Вот наша новая функция:

<?php

function getArticles() {

   $result = $this->db->query('SELECT * FROM articles');

   while($record = $result->fetch(PDO::FETCH_ASSOC)) {

       yield $this->mapToObject($record);

   }

}

?>

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

Наш предыдущий код «view» не нуждается в модификации, он все еще работает точно так же, как есть:

<?php

foreach($model->getArticles() as $article) {

    echo "<li>", htmlspecialchars($article->title), "</li>";

}
?>

Различия? Каждый раз, когда getArticles()метод «генерирует» новую запись, функция эффективно «приостанавливает», и для каждой итерации foreachцикла функция продолжается до тех пор, пока она не достигнет другой yield.

На что обращать внимание

Результат getArticles()теперь больше не возвращает массив, но фактически возвращает «итератор», который является объектом.

Такие вещи, как цикл foreach, ведут себя в основном одинаково, но не все, что вы можете сделать с массивом, вы можете сделать и с итератором.

Например, прежде чем мы переключились на генераторы, мы могли бы получить доступ к определенной записи, getArticles()например так:

<?php

$fifthArticle = $model->getArticles()[4];

?>

С генераторами вы больше не можете этого делать, и это приведет к ошибке. Переключение на генераторы означает, что вы должны получить доступ к результату по порядку.

Большой PHP Fuck-Up

К сожалению, PHP также не позволяет использовать функции массива обхода на генераторах, в том числе array_filter, array_mapи т.д.

Сначала вы можете преобразовать результат итератора обратно в массив:

<?php

$array = iterator_to_array(
    $model->getArticles()
);

?>

Но преобразование в массив несколько отменяет необходимость использования генераторов.

Для меня это то, что можно мгновенно добавить в печально известную статью «Фрактал плохого дизайна» . У нас были генераторы, начиная с PHP 5.5, итераторы, начиная с PHP 5.0, и array_map, начиная с PHP 4.0, поэтому у сопровождающих PHP было более десяти лет, чтобы исправить этот недостаток.