惰性求值 (Lazy Evaluation) 是一种编程策略,它将表达式的计算延迟到真正需要其结果时才进行。在 PHP 中,实现惰性求值最主要的机制就是生成器 (Generators) 。生成器允许开发者编写像迭代器一样遍历数据集合的函数,而无需将整个集合一次性加载到内存中。这对于处理大型数据集、无限序列或需要节省内存的场景至关重要。
核心思想:生成器通过 yield 关键字实现了惰性求值,它允许函数暂停执行并返回一个值,并在下次需要时从暂停的地方继续执行,从而按需生成数据,大幅减少内存占用。
一、理解惰性求值 (Lazy Evaluation) 1.1 什么是惰性求值? 传统的“饥饿求值 (Eager Evaluation)”或“及早求值”模式下,当一个函数或表达式被调用时,其所有参数都会在函数体执行前被完全计算。例如:
1 2 3 4 function sum (int $a , int $b ): int { return $a + $b ; } $result = sum (expensiveCalculationA (), expensiveCalculationB ());
而惰性求值 则相反,它不会立即计算表达式的结果,而是返回一个“承诺”或“句柄”,只有当结果真正被使用时,才会触发实际的计算。
1.2 惰性求值的优势
内存效率 :对于大型数据集或无限序列,无需一次性将所有数据加载到内存中。数据按需生成,显著降低内存消耗。
性能提升 :避免了不必要的计算。如果某些数据最终没有被使用,那么生成它的开销就完全避免了。
处理无限序列 :可以轻松表示和处理无限序列(例如斐波那契数列、自然数序列),因为它们永远不会被完全生成。
更好的抽象 :允许将数据生成逻辑与数据消费逻辑分离,提高了代码的模块化。
1.3 PHP 中的惰性求值实现:生成器 在 PHP 中,生成器是实现惰性求值的核心机制。一个生成器函数看起来像普通函数,但它使用 yield 关键字而不是 return 关键字来返回值。当一个生成器函数被调用时,它不会立即执行函数体,而是返回一个 Generator 对象。只有当这个 Generator 对象被遍历时,函数体才会被逐步执行。
二、PHP 生成器 (Generators) 详解 2.1 如何创建生成器? 一个包含 yield 关键字的函数就是一个生成器函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php function myGenerator ( ): Generator { echo "Generator started.\n" ; yield 1 ; echo "Generator resumed after yielding 1.\n" ; yield 2 ; echo "Generator resumed after yielding 2.\n" ; yield 3 ; echo "Generator finished.\n" ; } $generator = myGenerator (); echo "Generator function called, but not executed yet.\n" ;foreach ($generator as $value ) { echo "Received value: " . $value . "\n" ; } echo "Loop finished.\n" ;?>
输出:
1 2 3 4 5 6 7 8 9 Generator function called, but not executed yet. Generator started. Received value: 1 Generator resumed after yielding 1. Received value: 2 Generator resumed after yielding 2. Received value: 3 Generator finished. Loop finished.
从输出可以看出:
调用 myGenerator() 返回了一个 Generator 对象,但函数体并没有执行。
foreach 循环开始时,myGenerator() 的函数体才开始执行,直到第一个 yield 语句。
每次循环获取下一个值时,生成器从上次 yield 暂停的地方继续执行。
2.2 yield 关键字 yield 关键字是生成器的核心。它有两个主要作用:
暂停执行并返回值 :当生成器遇到 yield value; 时,它会暂停当前函数的执行,并将 value 返回给调用者。
保存状态 :生成器在暂停时会保存其内部状态(包括局部变量、参数和当前执行位置),以便下次从中断点继续执行。
yield 也可以带有键值对,例如 yield $key => $value;。
1 2 3 4 5 6 7 8 9 function keyValueGenerator ( ): Generator { yield 'a' => 10 ; yield 'b' => 20 ; } foreach (keyValueGenerator () as $key => $value ) { echo "Key: {$key} , Value: {$value} \n" ; }
2.3 Generator 对象的方法 当调用生成器函数时,它返回一个 Generator 类的实例,该类实现了 Iterator 接口,因此可以直接在 foreach 循环中使用。此外,它还提供了一些额外的方法:
current(): mixed :返回当前 yield 的值。
key(): mixed :返回当前 yield 的键。
next(): void :恢复生成器的执行,直到下一个 yield 或函数结束。
valid(): bool :检查生成器是否还有更多值可生成。
rewind(): void :将生成器倒回到起始位置。注意:大多数生成器只能被遍历一次。尝试对已完成的生成器 rewind() 通常会导致错误。
send(mixed $value): mixed :向生成器内部发送一个值,该值将作为 yield 表达式的结果。
throw(Throwable $exception): mixed :在生成器暂停的地方抛出一个异常。
getReturn(): mixed :在生成器完成执行(即没有更多的 yield 且函数正常结束)后,获取生成器函数中的 return 语句返回的值。
2.4 send() 方法:双向通信 send() 方法允许向生成器内部发送数据,实现生成器与外部的双向通信。send() 传递的值会成为生成器内部 yield 表达式的返回值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function talkativeGenerator ( ): Generator { $input = yield "Please enter your name: " ; echo "Generator received: " . $input . "\n" ; $age = yield "Please enter your age: " ; echo "Generator received age: " . $age . "\n" ; return "Goodbye!" ; } $generator = talkativeGenerator ();echo $generator ->current (); $generator ->send ("Alice" ); echo $generator ->current (); $generator ->send (30 );echo "Generator finished with: " . $generator ->getReturn () . "\n" ;
输出:
1 2 3 Please enter your name: Generator received: Alice Please enter your age: Generator received age: 30 Generator finished with: Goodbye!
2.5 yield from 表达式 (PHP 7.0+) yield from 表达式允许将一个生成器(或任何 Traversable 对象)委托给另一个生成器。它会遍历委托的生成器,并将其所有值直接 yield 出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function innerGenerator ( ): Generator { yield 'a' ; yield 'b' ; return 'Inner done!' ; } function outerGenerator ( ): Generator { yield 1 ; $returnValue = yield from innerGenerator (); echo "Inner generator returned: " . $returnValue . "\n" ; yield 2 ; } foreach (outerGenerator () as $value ) { echo "Outer generator yielded: " . $value . "\n" ; }
输出:
1 2 3 4 5 Outer generator yielded: 1 Outer generator yielded: a Outer generator yielded: b Inner generator returned: Inner done! Outer generator yielded: 2
yield from 使得组合多个生成器变得非常方便,可以构建更复杂的迭代逻辑。
三、生成器的应用场景 3.1 处理大型文件 这是生成器最经典的用例。一次性读取大文件会导致内存溢出,而生成器可以逐行读取和处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function readLargeFile (string $filePath ): Generator { $handle = fopen ($filePath , 'r' ); if (!$handle ) { throw new Exception ("Could not open file: {$filePath} " ); } while (!feof ($handle )) { yield fgets ($handle ); } fclose ($handle ); } $filePath = 'large_data.txt' ; file_put_contents ($filePath , implode ("\n" , array_map (fn($i ) => "Line " . $i , range (1 , 100000 ))));$startTime = microtime (true );$memoryUsage = memory_get_usage ();foreach (readLargeFile ($filePath ) as $line ) { } echo "Memory usage after processing (MB): " . (memory_get_usage () - $memoryUsage ) / (1024 * 1024 ) . "\n" ;echo "Peak memory usage (MB): " . (memory_get_peak_usage () / (1024 * 1024 )) . "\n" ;echo "Time taken (s): " . (microtime (true ) - $startTime ) . "\n" ;unlink ($filePath );
对比不使用生成器的情况:
1 2 3 4 5 6 7 function readLargeFileNonGenerator (string $filePath ): array { return file ($filePath ); }
3.2 数据库查询 当从数据库查询大量记录时,ORM 框架通常会一次性将所有结果封装成对象数组,这同样会占用大量内存。许多 ORM 框架(如 Laravel 的 Eloquent)提供了 cursor() 方法,其底层就是使用生成器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function fetchLargeResults (PDO $pdo , string $query ): Generator { $stmt = $pdo ->query ($query ); while ($row = $stmt ->fetch (PDO::FETCH_ASSOC )) { yield $row ; } }
3.3 无限序列生成 生成器可以非常方便地创建无限序列,例如斐波那契数列、自然数序列等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function fibonacciSequence ( ): Generator { $a = 0 ; $b = 1 ; while (true ) { yield $a ; [$a , $b ] = [$b , $a + $b ]; } } $fib = fibonacciSequence ();for ($i = 0 ; $i < 10 ; $i ++) { echo $fib ->current () . " " ; $fib ->next (); } echo "\n" ;
3.4 任务调度/协程 (PHP 5.5 - PHP 8.1) 在 PHP 8.1 引入 Fibers 之前,生成器是 PHP 中实现用户空间协程和任务调度的主要方式。通过一个外部调度器,可以 send() 值给生成器并 next() 它,模拟上下文切换。虽然现在有了更原生的 Fibers,但理解生成器在此方面的应用有助于理解历史和复杂框架的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function taskA ( ): Generator { echo "Task A started.\n" ; $data = yield 'Waiting for data from A' ; echo "Task A received: " . $data . "\n" ; yield 'Task A finished' ; } function taskB ( ): Generator { echo "Task B started.\n" ; yield 'Waiting for data from B' ; yield 'Task B finished' ; } $tasks = [taskA (), taskB ()];while (!empty ($tasks )) { foreach ($tasks as $i => $task ) { $task ->next (); if ($task ->valid ()) { echo "Scheduler received from task {$i} : " . $task ->current () . "\n" ; if ($task === $tasks [0 ] && $task ->current () === 'Waiting for data from A' ) { $tasks [0 ]->send ('Hello from Scheduler!' ); } } else { echo "Task {$i} completed.\n" ; unset ($tasks [$i ]); } } }
四、总结 PHP 生成器是实现惰性求值和优化内存使用的强大工具。通过 yield 关键字,它们允许函数暂停执行、按需生成数据并保存内部状态,从而避免一次性加载所有数据到内存。这在处理大型文件、数据库查询结果、无限序列以及在 PHP 8.1 之前构建协程时都发挥了关键作用。
理解并熟练使用生成器,对于编写高性能、内存高效的 PHP 应用程序至关重要。