Countable 接口
概述
Countable 接口允许对象与 PHP 内置的 count() 函数配合使用,使自定义类能够像数组一样被计数。当对象实现了 Countable 接口后,count($object) 将调用对象上的 count() 方法返回元素数量。
这是 PHP 集合类(Collection)最常实现的接口之一,也是构建类型安全、语义清晰的集合类的基础。
版本说明
Countable 接口从 PHP 5.1.0 起可用。PHP 8.0+ 中 count() 方法支持指定模式参数,与原生 count() 函数的 mode 参数保持一致。
基础概念
为什么需要 Countable
在没有 Countable 接口的情况下,对对象调用 count() 函数会返回 1(因为对象本身算作一个元素)。通过实现 Countable 接口,可以自定义对象的计数逻辑。
php
<?php
declare(strict_types=1);
$std = new stdClass();
echo count($std); // 输出: 1(始终为 1,因为是一个对象)
$arr = [1, 2, 3];
echo count($arr); // 输出: 3(数组元素数量)接口定义
php
<?php
declare(strict_types=1);
// Countable 接口定义
// interface Countable
// {
// public function count(int $mode = COUNT_NORMAL): int;
// }语法与代码
基本实现
php
<?php
declare(strict_types=1);
class ShoppingCart implements Countable
{
/** @var array<string, array{product: string, price: float, qty: int}> */
private array $items = [];
public function add(string $product, float $price, int $qty = 1): void
{
if (isset($this->items[$product])) {
$this->items[$product]['qty'] += $qty;
} else {
$this->items[$product] = [
'product' => $product,
'price' => $price,
'qty' => $qty,
];
}
}
public function count(int $mode = COUNT_NORMAL): int
{
return count($this->items, $mode);
}
public function totalItems(): int
{
return array_sum(array_column($this->items, 'qty'));
}
}
$cart = new ShoppingCart();
$cart->add('苹果', 5.99, 3);
$cart->add('香蕉', 3.49, 5);
echo count($cart); // 输出: 2(商品种类数)
echo $cart->totalItems(); // 输出: 8(总商品件数)count 模式参数
php
<?php
declare(strict_types=1);
class MultiDimensionalCollection implements Countable
{
/** @var array<int, array<int, int>> */
private array $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function count(int $mode = COUNT_NORMAL): int
{
return count($this->data, $mode);
}
}
$collection = new MultiDimensionalCollection([
[1, 2, 3],
[4, 5, 6],
[7, 8],
]);
echo count($collection, COUNT_NORMAL); // 输出: 3(第一维度元素数)
echo count($collection, COUNT_RECURSIVE); // 输出: 8(递归计算所有元素)详细说明
在集合类中的典型使用
php
<?php
declare(strict_types=1);
class UserCollection implements Countable, IteratorAggregate
{
/** @var array<int, User> */
private array $users = [];
public function add(User $user): void
{
$this->users[$user->id()] = $user;
}
public function remove(int $userId): void
{
unset($this->users[$userId]);
}
public function count(int $mode = COUNT_NORMAL): int
{
return count($this->users, $mode);
}
public function getIterator(): Traversable
{
return new ArrayIterator($this->users);
}
public function isEmpty(): bool
{
return count($this) === 0;
}
public function findByEmail(string $email): ?User
{
foreach ($this as $user) {
if ($user->email() === $email) {
return $user;
}
}
return null;
}
}
// 用法
$users = new UserCollection();
$users->add(new User(1, 'alice@example.com'));
$users->add(new User(2, 'bob@example.com'));
if (!$users->isEmpty()) {
echo "共 {$users->count()} 个用户\n"; // 共 2 个用户
}与其他预定义接口的配合
php
<?php
declare(strict_types=1);
class ProductCatalog implements Countable, ArrayAccess, IteratorAggregate
{
/** @var array<string, Product> */
private array $products = [];
public function add(string $sku, Product $product): void
{
$this->products[$sku] = $product;
}
// Countable
public function count(int $mode = COUNT_NORMAL): int
{
return count($this->products, $mode);
}
// ArrayAccess
public function offsetExists(mixed $offset): bool
{
return isset($this->products[$offset]);
}
public function offsetGet(mixed $offset): ?Product
{
return $this->products[$offset] ?? null;
}
public function offsetSet(mixed $offset, mixed $value): void
{
if (!$value instanceof Product) {
throw new InvalidArgumentException('只能添加 Product 对象');
}
$this->products[$offset] = $value;
}
public function offsetUnset(mixed $offset): void
{
unset($this->products[$offset]);
}
// IteratorAggregate
public function getIterator(): Traversable
{
return new ArrayIterator($this->products);
}
}empty() 函数的兼容
php
<?php
declare(strict_types=1);
class Items implements Countable
{
/** @var array<int, string> */
private array $data;
public function __construct(array $data = [])
{
$this->data = $data;
}
public function count(int $mode = COUNT_NORMAL): int
{
return count($this->data);
}
}
$items = new Items([]);
var_dump(empty($items)); // bool(false) — 注意!
// empty() 不会调用 Countable::count()
// empty() 只检查对象是否存在,不会认为 count 为 0 的对象为 empty
// 建议使用自定义方法
class BetterItems implements Countable
{
/** @var array<int, string> */
private array $data = [];
public function count(int $mode = COUNT_NORMAL): int
{
return count($this->data);
}
public function isEmpty(): bool
{
return $this->count() === 0;
}
}empty() 陷阱
empty() 函数不会调用 Countable::count() 方法,即使对象的 count() 返回 0,empty($object) 仍然返回 false。应使用自定义 isEmpty() 方法或在条件中直接使用 count($obj) === 0。
实战示例
分页集合类
php
<?php
declare(strict_types=1);
class PaginatedCollection implements Countable
{
private int $totalItems;
private int $itemsPerPage;
private int $currentPage;
/** @var array<int, mixed> */
private array $items;
public function __construct(
array $items,
int $totalItems,
int $itemsPerPage,
int $currentPage = 1
) {
$this->items = $items;
$this->totalItems = $totalItems;
$this->itemsPerPage = $itemsPerPage;
$this->currentPage = $currentPage;
}
public function count(int $mode = COUNT_NORMAL): int
{
// 返回当前页的元素数
return count($this->items, $mode);
}
public function totalCount(): int
{
return $this->totalItems;
}
public function totalPages(): int
{
return (int) ceil($this->totalItems / $this->itemsPerPage);
}
public function currentPage(): int
{
return $this->currentPage;
}
public function hasMorePages(): bool
{
return $this->currentPage < $this->totalPages();
}
public function items(): array
{
return $this->items;
}
}缓存计数集合
php
<?php
declare(strict_types=1);
class LazyCountableCollection implements Countable
{
/** @var callable */
private $countResolver;
private ?int $cachedCount = null;
public function __construct(callable $countResolver)
{
$this->countResolver = $countResolver;
}
public function count(int $mode = COUNT_NORMAL): int
{
if ($this->cachedCount === null) {
$this->cachedCount = ($this->countResolver)();
}
return $this->cachedCount;
}
public function invalidateCache(): void
{
$this->cachedCount = null;
}
}
// 数据库查询场景:count 可能是昂贵的操作
$collection = new LazyCountableCollection(
fn (): int => (int) $db->query('SELECT COUNT(*) FROM orders')->fetchColumn()
);
// 第一次调用执行实际查询
echo count($collection);
// 后续调用使用缓存值
echo count($collection);注意事项
count() 方法必须返回非负整数
php
<?php
declare(strict_types=1);
class BadCountable implements Countable
{
public function count(int $mode = COUNT_NORMAL): int
{
return -1; // 违反语义,count 不应为负数
// PHP 不会强制报错,但违反接口契约
}
}
// 正确做法:空集合返回 0
class GoodCountable implements Countable
{
/** @var array<int, mixed> */
private array $items = [];
public function count(int $mode = COUNT_NORMAL): int
{
return count($this->items); // 最小返回 0
}
}count() 的性能考量
php
<?php
declare(strict_types=1);
class SlowCountable implements Countable
{
/** @var array<int, mixed> */
private array $items = [];
public function add(mixed $item): void
{
$this->items[] = $item;
}
public function count(int $mode = COUNT_NORMAL): int
{
// 避免在 count() 中执行耗时操作
return count($this->items); // O(1) 操作,直接返回数组大小
}
}
// 如果计数需要复杂计算,考虑缓存
class CachedCountable implements Countable
{
private bool $dirty = true;
private int $count = 0;
/** @var array<int, mixed> */
private array $items = [];
public function add(mixed $item): void
{
$this->items[] = $item;
$this->dirty = true;
}
public function count(int $mode = COUNT_NORMAL): int
{
if ($this->dirty) {
$this->count = count($this->items);
$this->dirty = false;
}
return $this->count;
}
}最佳实践
- count() 方法应该快速:避免在
count()中执行数据库查询或复杂计算,考虑使用缓存 - 返回值语义明确:
count()应返回集合中元素的数量,而非其他语义值 - 与 Iterator 配合使用:集合类同时实现
Countable和Iterator/IteratorAggregate - 添加 isEmpty() 辅助方法:因为
empty()不兼容Countable - 添加 totalSize() 区分语义:当
count()和总数量概念不同时,提供独立方法
php
<?php
declare(strict_types=1);
// 最佳实践模板
class Collection implements Countable, IteratorAggregate
{
/** @var array<string|int, mixed> */
private array $items = [];
public function count(int $mode = COUNT_NORMAL): int
{
return count($this->items, $mode);
}
public function isEmpty(): bool
{
return $this->count() === 0;
}
public function getIterator(): Traversable
{
return new ArrayIterator($this->items);
}
}