php-expressive / iterators
一组迭代器类,用于编写更具表达力的PHP代码
Requires (Dev)
- phpunit/phpunit: ^10
README
PHP Expressive: 迭代器
介绍
如果可以以减少或甚至消除“解释性”注释的方式来编写代码,会怎么样?你知道,那些类似于// 这段代码是做什么的
的注释。如果代码复杂到需要解释,也许代码本身就需要重新审视。据估计,软件工程师在阅读代码上花费的时间是他们在编写代码时间的十倍。我们难道不应该使我们的代码更容易理解且更有趣吗?
这个库(以及最终整个PHP Expressive项目)旨在帮助提高我们代码的可读性和可理解性;特别是与使用迭代器和可迭代对象(如array
)相关。它受到了Rust中的Iterator
特质的高度启发(甚至“借鉴”了一些单元测试和文档),但它是为PHP受众编写的。
这个库的目标不是阻止你编写for
或foreach
循环(虽然你可能会发现你写的这些循环要少得多),而是提高你代码的可读性。即使是最复杂的循环通常也可以分解成更小的、更容易理解的片段。通过使用这个库的功能,你可以减少样板代码,提高可读性,并从预先编写的、预先测试过的算法中受益。
激励示例
假设你有一个电子邮件地址数组($input_array
),并希望只保留这组中前10个以<something>@gmail.com
结尾的电子邮件地址。此外,你只需要@
左边的名字(因为它们都是@gmail.com
),并且希望这些名字都是小写的。实现这一目标的最明显的方法之一是使用foreach
循环
$num_chars_to_trim = strlen('@gmail.com'); $gmail_addresses = []; foreach ($input_array as $address) { $lower_address = strtolower($address); // We are assuming PHP 8.2, and ignoring RegEx functions for now if (str_ends_with($lower_address, '@gmail.com')) { $gmail_addresses[] = substr($lower_address, 0, -$num_chars_to_trim); if (count($gmail_addresses) == 10) { break; } } }
虽然这是一个完全可接受的解决方案,但如果你用新的眼光再次审视它(想象几个月后遇到这段代码),理解其中的内容需要一些心理努力。为了使代码更具表达性(即,更易读),让我们将一些功能分解成命名函数。
$is_gmail_address = fn($address) => str_ends_with($address, '@gmail.com'); $num_chars_to_trim = strlen('@gmail.com'); $trim_gmail_com = fn($address) => substr($address, 0, -$num_chars_to_trim); $gmail_addresses = []; foreach ($input_array as $address) { $lower_address = strtolower($address); if ($is_gmail_address($lower_address)) { $gmail_addresses[] = $trim_gmail_com($lower_address); if (count($gmail_addresses) == 10) { break; } } }
这已经好多了,但我们还能做得更好吗?它仍然看起来在foreach
循环中有很多操作。我们可以重构最内层的if
检查,将逻辑移出循环,但可能会牺牲一些不必要的处理。
// ... As before $gmail_addresses = []; foreach ($input_array as $address) { $lower_address = strtolower($address); if ($is_gmail_address($lower_address)) { $gmail_addresses[] = $trim_gmail_com($lower_address); } } $gmail_addresses = array_slice($gmail_addresses, 0, 10);
现在foreach
的阅读性略好,但如果我们的原始列表有数百或数千个项目,我们会处理所有这些项目,即使我们只想要其中的10个。这是浪费的。
让我们看看PHP的Iterator
类是否可以帮助我们
// ... As before $input_iterator = new \ArrayIterator($input_array); $lowercased = new \TransformIterator($filtered, fn($str) => strtolower($str)); // Uh oh! $filtered = new \CallbackFilterIterator($input_iterator, $is_gmail_address); $transformed = new \TransformIterator($filtered, $trim_gmail_com); // Uh oh, again! $gmail_addresses = new \LimitIterator($transformed, limit: 10); $gmail_addresses = iterator_to_array($gmail_addresses);
这看起来很有希望,但PHP没有TransformIterator
。让我们快速写一个
class TransformIterator implements Iterator { public function __construct(private iterable $iterator, private \Closure $f) {} public function current(): mixed { return ($this->f)($this->iterator->current()); } public function key(): mixed { return $this->iterator->key(); } public function next(): void { $this->iterator->next(); } public function rewind(): void { $this->iterator->rewind(); } public function valid(): bool { return $this->iterator->valid(); } }
好吧,这并不真的那么快!
回顾之前的代码块,它确实看起来更具有表达性。从上到下阅读,我们可以查看重要部分:我们正在对输入数组进行过滤,以获取gmail地址
,然后通过去除gmail.com
进行转换
,将它们限制为只包含10
个,然后将它们转换为数组。
让我们跳出思维定势
上述段落中的最后一句话肯定比用foreach
和if
来描述循环更简洁地解释了我们的目标。然而,如果我们能让代码看起来更像是描述呢?想象一下以下内容。
// ... Same helper functions $gmail_addresses = // Note: This is *almost* correct $input_array ->transform(fn($str) => strtolower($str)) ->filter($is_gmail_address) ->transform($trim_gmail_com) ->take(10) ->to_array();
哇!这段代码几乎就像一句句子一样!现在这才是表达性强的代码!实际上,这正是这个库中的Iterators
能做的事情,只需进行一个小修改。我们首先需要将$input_array
转换成一个Iterator
,可以像这样操作
$gmail_addresses = // We just wrap $input_array in iter() to make it an Iterator iter($input_array) ->transform(fn($str) => strtolower($str)) ->filter($is_gmail_address) ->transform($trim_gmail_com) ->take(10) ->to_array();
版本
这个库遵循语义化版本2.0,目前处于初期开发阶段(预v1.0.0)。然而,已经尽力使其可使用且有帮助。如果您想帮助使其变得更好,请参阅贡献指南了解如何提供帮助!
当前(和计划)的功能
待办事项
变更日志
请参阅变更日志以查看每个版本的新的功能、错误修复和弃用功能。
文档
您可以通过查看API文档来了解当前实现的所有功能以及许多如何在您的代码中使用它们的示例。