This is almost a style guide for array design, but adding it to the Object design style guide doesn’t feel right Yes, because not all object-oriented languages have dynamic arrays. The examples in this article are written in PHP because PHP is a lot like Java (which you may be familiar with), but uses dynamic arrays instead of built-in collection classes and interfaces.
When using an array as a list When a list (a collection of values in a specific order), each value should be of type z:
$goodList = [ 'a', 'b' ]; $badList = [ 'a', 1 ];
A generally accepted style for annotating list types is:@var array<TypeOfElement>
. Make sure not to add the type of the index (it is always int
).
PHP will automatically create a new index for each element in the list (0, 1, 2, etc.). However, you should not rely on these indexes nor use them directly. The only properties of lists that clients should rely on are iterable and countable.
So, feel free to use foreach
and count()
, but do not use for
to loop over the elements in the list:
// 好的循环: foreach ($list as $element) { } // 不好的循环 (公开每个元素的索引): foreach ($list as $index => $element) { } // 也是不好的循环 (不应该使用每个元素的索引): for ($i = 0; $i < count($list); $i++) { }
(In PHP, the for
loop may not even work because there may be missing indices in the list and there may be more indices than there are elements in the list.)
You may want to remove elements from a list by index (unset()
), however, you should use array_filter( )
to create a new list (without unwanted elements) instead of removing elements.
Likewise, you should not rely on the index of an element, so when using array_filter()
, you should not use the flag
parameter to filter elements based on index, even Filter elements based on element and index.
// 好的过滤: 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 );
when the key is related, not the index (0, 1, 2, etc.). You are free to use arrays as maps (from which values can be retrieved by their unique keys).
The first rule of using arrays as maps is that all keys in the array should be of the same type (most Common ones are keys of type string
).
$goodMap = [ 'foo' => 'bar', 'bar' => 'baz' ]; // 不好(使用不同类型的键) $badMap = [ 'foo' => 'bar', 1 => 'baz' ];
The same goes for values in a map: they should be of the same type.
$goodMap = [ 'foo' => 'bar', 'bar' => 'baz' ]; // 不好(使用不同类型的值) $badMap = [ 'foo' => 'bar', 'bar' => 1 ];
A generally accepted mapping type annotation style is: @var array<TypeOfKey, TypeOfValue>
.
Lists can be safely passed between objects because of their simple characteristics. Any client can use it to loop over its elements, or to count its elements, even if the list is empty. Mappings are more difficult to handle because clients may rely on keys that don't have corresponding values. This means that in general, they should remain private to the object that manages them. Clients are not allowed to access the internal mapping directly, instead getters (and possibly setters) are provided to retrieve values. If a value does not exist for the requested key, an exception is thrown. However, if you can keep the map and its values completely private, then do so.
// 公开一个列表是可以的 /** * @return array<User> */ public function allUsers(): array { // ... } // 公开地图可能很麻烦 /** * @return array<string, User> */ public function usersById(): array { // ... } // 相反,提供一种方法来根据其键检索值 /** * @throws UserNotFound */ public function userById(string $id): User { // ... }
When you want to store values of different types in one mapping, Please use an object. Define a class and add public typed properties to it, or add constructors and getters. Examples of objects like this are configuration objects, or command objects:
final class SillyRegisterUserCommand { public string $username; public string $plainTextPassword; public bool $wantsToReceiveSpam; public int $answerToIAmNotARobotQuestion; }
Sometimes, a library or framework needs to use arrays in a more dynamic way . In these cases it is not possible (nor desirable) to follow the previous rules. For example, array data, which will be stored in a database table, or Symfony form configuration.
自定义集合类是一种非常酷的方法,最后可以和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》
The above is the detailed content of Array specifications and custom collections in PHP. For more information, please follow other related articles on the PHP Chinese website!