Maison  >  Article  >  développement back-end  >  Parlons de la façon d'utiliser le générateur PHP

Parlons de la façon d'utiliser le générateur PHP

不言
不言original
2018-07-16 11:10:421686parcourir

PHP a introduit la fonctionnalité "Générateur" dans la version 5.5, mais cette fonctionnalité n'a pas attiré l'attention des gens. Dans la migration officielle de PHP 5.4.x vers PHP 5.5.x, il est introduit qu'il peut implémenter un itérateur (Iterator) de manière simple. Mais au-delà de cela, dans quels scénarios les générateurs peuvent-ils être utilisés ?

La mise en œuvre du générateur est complétée via le mot-clé rendement. Les générateurs fournissent un moyen simple d'implémenter des itérateurs sans pratiquement aucune surcharge supplémentaire ni la complexité de l'implémentation des itérateurs via des classes qui implémentent l'interface de l'itérateur.

La documentation fournit un exemple simple illustrant cet itérateur simple, voir le code ci-dessous :

function xrange($start, $limit, $step = 1) {
    for ($i = $start; $i <= $limit; $i += $step) {
        yield $i;
    }
}

Comparons-le à un tableau sans support d'itérateur :

foreach xrange($start, $limit, $step = 1) {
    $elements = [];
    
    for ($i = $start; $i <= $limit; $i += $step) {
        $elements[] = $i;
    }

    return $elements;
}

Les deux versions de la fonction prennent en charge foreach pour parcourir tous les éléments :

foreach (xrange(1, 100) as $i) {
    print $i . PHP_EOL;
}

Ainsi, en plus d'une définition de fonction plus courte, nous pouvons également obtenir Quoi ? rendement Qu'as-tu fait exactement ? Pourquoi les données peuvent-elles être renvoyées lorsque la première fonction est définie, même sans l'instruction return ?

Commençons par la valeur de retour. Les générateurs sont une fonction très spéciale en PHP. Lorsqu'une fonction contient yield, alors cette fonction n'est plus une fonction ordinaire, elle renvoie toujours une instance "Générateur". Le générateur implémente l'interface Iterator, c'est pourquoi il est capable de parcourir foreach.

Ensuite, j'utilise les méthodes de l'interface Iterator pour réécrire la boucle foreach précédente. Vous pouvez consulter les résultats sur 3v4l.org.

$generator = xrange(1, 100);

while($generator->valid()) {
    print $generator->current() . PHP_EOL;

    $generator->next();
}

Nous pouvons clairement voir que les générateurs sont une technologie plus avancée. Écrivons maintenant un nouvel exemple de générateur pour mieux comprendre comment il est traité à l'intérieur du générateur.

function foobar() {
    print 'foobar - start' . PHP_EOL;

    for ($i = 0; $i < 5; $i++) {
        print &#39;foobar - yielding...&#39; . PHP_EOL;
        yield $i;
        print &#39;foobar - continued...&#39; . PHP_EOL;
    }

    print &#39;foobar - end&#39; . PHP_EOL;
}

$generator = foobar();

print &#39;Generator created&#39; . PHP_EOL;

while ($generator->valid()) {
    print "Getting current value from the generator..." . PHP_EOL;

    print $generator->current() . PHP_EOL;

    $generator->next();
}
Generator created
foobar - start
foobar - yielding...
Getting current value from the generator...
1
foobar - continued
foobar - yielding...
Getting current value from the generator...
2
foobar - continued
foobar - yielding...
Getting current value from the generator...
3
foobar - continued
foobar - yielding...
Getting current value from the generator...
4
foobar - continued
foobar - yielding...
Getting current value from the generator...
5
foobar - continued
foobar - end

Hein ? Pourquoi Générateur créé est-il imprimé en premier ? C'est parce que le générateur ne fait rien jusqu'à ce qu'il soit utilisé. Dans l'exemple ci-dessus, c'est le code $generator->valid()** qui lance l'exécution du générateur. Nous voyons que le générateur fonctionne jusqu'au premier **yield** et renvoie le flux de contrôle à l'appelant **$generator->valid(). Lorsque $generator->next() est appelé, l'exécution du générateur reprend et cesse de fonctionner au prochain yield, et ainsi de suite jusqu'à ce qu'il n'y ait plus de yield jusqu'à. Nous disposons désormais de fonctions de terminal qui peuvent effectuer une pause et reprendre sur n'importe quel rendement. Cette fonctionnalité permet d'écrire les fonctions de report requises par le client.

Vous pouvez créer une fonction qui lit tous les utilisateurs à partir de l'API GitHub. La pagination est prise en charge, mais vous pouvez masquer ces détails et récupérer la page de données suivante uniquement en cas de besoin. Vous pouvez utiliser yield pour obtenir les données de chaque utilisateur de la page actuelle. Jusqu'à ce que tous les utilisateurs de la page actuelle soient obtenus, vous pouvez alors obtenir la page de données suivante.

class GitHubClient {
    function getUsers(): Iterator {
        $uri = '/users';

        do {
            $response = $this->get($uri);
            foreach ($response->items as $user) {
                yield $user;
            }

            $uri = $response->nextUri;
        } while($uri !== null);
    }
}

Le client peut itérer sur tous les utilisateurs ou arrêter l'itération à tout moment.

Utiliser des générateurs comme itérateurs est ennuyeux

Oui, vous avez la bonne idée. Toutes les explications que j'ai données ci-dessus peuvent être obtenues par n'importe qui à partir de la documentation PHP. Mais en tant qu'itérateur, ces utilisations n'utilisent même pas la moitié de ses puissantes capacités. Les générateurs fournissent également des fonctions send() et throw() qui ne font pas partie de l'interface Iterator. Nous avons parlé plus tôt de la possibilité de suspendre et de reprendre l'exécution du générateur. Lorsque vous devez restaurer le générateur, vous pouvez non seulement utiliser la méthode Generator::next(), mais également utiliser Generator::send() et Generator :: throw() Méthode.

Generator::send() vous permet de spécifier la valeur de retour de yield, tandis que Generator::throw() vous permet de request rendement lève une exception. Grâce à ces méthodes, nous pouvons non seulement obtenir des données du générateur, mais également envoyer de nouvelles données au générateur.

Regardons un exemple de journal Logger tiré du multitâche coopératif utilisant des coroutines (fortement recommandé de lire cet article).

function logger($filename) {
    $fileHandle = fopen($filename, 'a');

    while (true) {
        fwrite($fileHandle, yield . "\n");
    }
}

$logger = logger(__DIR__ . '/log');
$logger->send('Foo');
$logger->send('Bar');

rendement est utilisé ici comme expression. Lorsque nous envoyons des données, les données sont renvoyées par yield puis passées en paramètre à fwrite().

Pour être honnête, cet exemple est inutile dans les projets réels. Il n'est utilisé que pour démontrer le principe d'utilisation de Generator::send(), mais le simple fait de pouvoir envoyer des données n'a pas beaucoup d'effet. Ce serait différent s’il existait une classe prenant en charge les fonctions ordinaires.

使用生成器的乐趣来自于通过 yield 创建数据,然后由「生成器执行程序(generator runner)」依据这个数据来处理业务,然后再继续执行生成器。这就是「协程(coroutines)」和「状态流解析器(stateful streaming parsers)」实例。在讲解协程和状态流解析器之前,我们快速浏览一下如何在生成器中返回数据,我们还没有将接触这方面的知识。从 PHP 5.5 开始我们可以在生成器内部使用 return; 语句,但是不能返回任何值。执行 return; 语句的唯一目的是结束生成器执行。

不过从 PHP 7.0 起支持返回值。这个功能在用于迭代时可能有些奇怪,但是在其他使用场景如协程时将非常有用,例如,当我们在执行一个生成器时我们可以依据返回值处理,而无需直接对生成器进行操作。下一节我们将讲解 return 语句在协程中的使用。

异步生成器

Amp 是一款 PHP 异步编程的框架。支持异步协程功能,本质上是等待处理结果的占位符。「生成器执行程序」为 Coroutine类。它会订阅异步生成器(yielded promise),当有执行结果可用时则继续生成器处理。如果处理失败,则会抛出异常给生成器。你可以到 amphp/amp 版本库查看实现细节。在 Amp 中的 Coroutine 本身就是一个 Promise。如果这个协程抛出未经捕获的异常,这个协程就执行失败了。如果解析成功,那么就返回一个值。这个值看起来和普通函数的返回值并无二致,只不过它处于异步执行环境中。这就是需要生成器需要有返回值的意义,这也是为何我们将这个特性加入到 PHP 7.0 中的原因,我们会将最后执行的yield 值作为返回值,但这不是一个好的解决方案。

Amp 可以像编写阻塞代码一样编写非阻塞代码,同时允许在同一进程中执行其它非阻塞事件。一个使用场景是,同时对一个或多个第三方 API 并行的创建多个 HTTP 请求,但不限于此。得益于事件循环,可以同时处理多个 I/O 处理,而不仅仅是只能处理多个 HTTP请求这类操作。

Loop::run(function() {
    $uris = [
        "https://google.com/",
        "https://github.com/",
        "https://stackoverflow.com/",
    ];

    $client = new Amp\Artax\DefaultClient;
    $promises = [];

    foreach ($uris as $uri) {
        $promises[$uri] = $client->request($uri);
    }

    $responses = yield $promises;

    foreach ($responses as $uri => $response) {
        print $uri . " - " . $response->getStatus() . PHP_EOL;
    }
});

但是,拥有异步功能的协程并非只能够在 yield 右侧出现变量,还可以在它的左侧。这就是我们前面提到的解析器。

$parse = new Parser((function(){
    while (true) {
        $line = yield "\r\n";

        if (trim($line) === "") {
            continue;
        }

        print "New item: {$line}" . PHP_EOL;
    }
})());

for ($i = 0; $i < 100; $i++) {
    $parser->push("bar\r");
    $parser->push("\nfoo");
}

解析器会缓存所有输入直到接收的是 rn。这类生成器解析器并不能简化简单协议处理(如换行分隔符协议),但是对于复杂的解析器,如在服务器解析 HTTP 请求的 Aerys。

相关推荐:

如何利用PHP实现开发中基于layUI的三级联动效果的代码

关于IIS下PHP快三平台源码的架设环境的配置过程

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn