這差不多是一個關於陣列設計的風格指南,但把它加到物件設計風格指南感覺不太對,因為不是所有的物件導向語言都有動態數組。本文中的範例是用 PHP 寫的,因為 PHP 很像 Java(可能比較熟悉),但使用的是動態陣列而不是內建的集合類別和介面。
當使用一個陣列作為當一個列表(一個具有特定順序的值的集合)時,每個值應該是z 類型:
$goodList = [ 'a', 'b' ]; $badList = [ 'a', 1 ];
一個被普遍接受的註釋列表類型的風格是:@var array<TypeOfElement>
。確保不添加索引的類型(它總是int
)。
PHP 將自動為清單中的每個元素(0、1、2等)建立新索引。然而,你不應該依賴這些索引,也不應該直接使用它們。客戶端應該依賴的列表的唯一屬性是可迭代的和可計數的。
因此,可以隨意使用foreach
和count()
,但不要使用for
循環遍歷列表中的元素:
// 好的循环: foreach ($list as $element) { } // 不好的循环 (公开每个元素的索引): foreach ($list as $index => $element) { } // 也是不好的循环 (不应该使用每个元素的索引): for ($i = 0; $i < count($list); $i++) { }
(在PHP 中,for
循環甚至可能不起作用,因為列表中可能缺少索引,而且索引可能比列表中的元素數量還要多。)
你可能想要透過索引從清單中刪除元素(unset()
),但是,你應該使用array_filter( )
來建立一個新清單(沒有不需要的元素),而不是刪除元素。
同樣,你不應該依賴元素的索引,因此,在使用array_filter()
時,不應該使用flag
參數去根據索引過濾元素,甚至根據元素和索引過濾元素。
// 好的过滤: array_filter( $list, function (string $element): bool { return strlen($element) > 2; } ); // 不好的过滤器(也使用索引来过滤元素) array_filter( $list, function (int $index): bool { return $index > 3; }, ARRAY_FILTER_USE_KEY ); // 不好的过滤器(同时使用索引和元素来过滤元素) array_filter( $list, function (string $element, int $index): bool { return $index > 3 || $element === 'Include'; }, ARRAY_FILTER_USE_BOTH );
當鍵是相關的,而不是索引(0,1,2,等等)。你可以隨意使用數組作為映射(可以透過其唯一的鍵從其中檢索值)。
使用陣列作為映射的第一個規則是,數組中的所有鍵都應該具有相同的類型(最常見的是string
類型的鍵)。
$goodMap = [ 'foo' => 'bar', 'bar' => 'baz' ]; // 不好(使用不同类型的键) $badMap = [ 'foo' => 'bar', 1 => 'baz' ];
對應中的值也是如此:它們應該有相同的型別。
$goodMap = [ 'foo' => 'bar', 'bar' => 'baz' ]; // 不好(使用不同类型的值) $badMap = [ 'foo' => 'bar', 'bar' => 1 ];
一種普遍接受的映射類型註解樣式是: @var array<TypeOfKey, TypeOfValue>
。
列表可以安全地在物件之間傳遞,因為它們具有簡單的特徵。任何客戶端都可以使用它來循環其元素,或計數其元素,即使清單是空的。映射則更難處理,因為客戶端可能依賴沒有對應值的鍵。這意味著在一般情況下,它們應該對管理它們的物件保持私有。不允許客戶端直接存取內部映射,而是提供 getter (可能還有 setter )來檢索值。如果請求的鍵不存在值,則拋出異常。但是,如果您可以保持映射及其值完全私有,那麼就這樣做。
// 公开一个列表是可以的 /** * @return array<User> */ public function allUsers(): array { // ... } // 公开地图可能很麻烦 /** * @return array<string, User> */ public function usersById(): array { // ... } // 相反,提供一种方法来根据其键检索值 /** * @throws UserNotFound */ public function userById(string $id): User { // ... }
當你想要在一個映射中儲存不同類型的值時,請使用一個物件。定義一個類,並向其添加公共的類型化屬性,或添加建構函數和 getter。像這樣的對象的例子是配置對象,或者命令對象:
final class SillyRegisterUserCommand { public string $username; public string $plainTextPassword; public bool $wantsToReceiveSpam; public int $answerToIAmNotARobotQuestion; }
有時,庫或框架需要以更動態的方式使用數組。在這些情況下,不可能(也不希望)遵循前面的規則。例如數組數據,它將被儲存在一個資料庫表中,或Symfony 表單配置。
自定义集合类是一种非常酷的方法,最后可以和Iterator
、ArrayAccess
和其朋友一起使用,但是我发现大多数生成的代码令人很困惑。第一次查看代码的人必须在 PHP 手册中查找详细信息,即使他们是有经验的开发人员。另外,你需要编写更多的代码,你必须维护这些代码(测试、调试等)。所以在大多数情况下,我发现一个简单的数组,加上一些适当的类型注释,就足够了。到底什么是需要将数组封装到自定义集合对象中的强信号?
如果使用相同数组的多个客户端执行相同的任务(例如过滤、映射、减少、计数),则可以通过引入自定义集合类来消除重复。将重复的逻辑移到集合类的一个方法上,允许任何客户端使用对集合的简单方法调用来执行相同的任务:
$names = [/* ... */]; // 在几个地方发现: $shortNames = array_filter( $names, function (string $element): bool { return strlen($element) < 5; } ); // 变成一个自定义集合类: use Assert\Assert; final class Names { /** * @var array<string> */ private array $names; public function __construct(array $names) { Assert::that()->allIsString($names); $this->names = $names; } public function shortNames(): self { return new self( array_filter( $this->names, function (string $element): bool { return strlen($element) < 5; } ) ); } } $names = new Names([/* ... */]); $shortNames = $names->shortNames();
在集合的转换上使用方法的好处就是获得了一个名称。这使你能够向看起来相当复杂的array_filter()
调用添加一个简短而有意义的标签。
如果一个客户端使用特定的数组并循环,从选定的元素中取出一段数据,并对该数据进行处理,那么该客户端就与所有涉及的类型紧密耦合: 数组本身、数组中元素的类型、它从所选元素中检索的值的类型、选择器方法的类型,等等。这种深度耦合的问题是,在不破坏依赖于它们的客户端的情况下,很难更改所涉及类型的任何内容。因此,在这种情况下,你也可以将数组包装在一个自定义 的集合类中,让它一次性给出正确的答案,在内部进行必要的计算,让客户端与集合更加松散地耦合。
$lines = []; $sum = 0; foreach ($lines as $line) { if ($line->isComment()) { continue; } $sum += $line->quantity(); } // Turned into a custom collection class: final class Lines { public function totalQuantity(): int { $sum = 0; foreach ($lines as $line) { if ($line->isComment()) { continue; } $sum += $line->quantity(); } return $sum; } }
让我们看看在使用自定义集合类时应用的一些规则。
对集合实例的现有引用在运行某种转换时不应受到影响。因此,任何执行转换的方法都应该返回类的一个新实例,就像我们在上面的例子中看到的那样:
final class Names { /** * @var array<string> */ private array $names; public function __construct(array $names) { Assert::that()->allIsString($names); $this->names = $names; } public function shortNames(): self { return new self( /* ... */ ); } }
当然,如果要映射内部数组,则可能要映射到另一种类型的集合或简单数组。与往常一样,请确保提供适当的返回类型。
你不必扩展泛型集合库类,也不必自己在每个自定义集合类上实现泛型筛选器、映射和缩减方法,只实现真正需要的。如果某个方法在某一时刻不被使用,那么就删除它。
如果你使用 PHP,不用实现所有的Iterator
接口的方法(并保持一个内部指针,等等),只是实现IteratorAggregate
接口,让它返回一个ArrayIterator
实例基于内部数组:
final class Names implements IteratorAggregate { /** * @var array<string> */ private array $names; public function __construct(array $names) { Assert::that()->allIsString($names); $this->names = $names; } public function getIterator(): Iterator { return new ArrayIterator($this->names); } } $names = new Names([/* ... */]); foreach ($names as $name) { // ... }
为你的自定义集合类编写更多代码的好处是使客户端更容易地使用该集合(而不是仅仅使用一个数组)。如果客户端代码变得更容易理解,如果集合提供了有用的行为,那么维护自定义集合类的额外成本就是合理的。但是,因为使用动态数组非常容易(主要是因为你不必写出所涉及的类型),所以我不经常介绍自己的集合类。尽管如此,我知道有些人是它们的伟大支持者,所以我将确保继续寻找潜在的用例。
推荐教程:《PHP》
以上是PHP中數組規範和自訂集合的詳細內容。更多資訊請關注PHP中文網其他相關文章!