Skip to content

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;
    }
}

最佳实践

  1. count() 方法应该快速:避免在 count() 中执行数据库查询或复杂计算,考虑使用缓存
  2. 返回值语义明确count() 应返回集合中元素的数量,而非其他语义值
  3. 与 Iterator 配合使用:集合类同时实现 CountableIterator/IteratorAggregate
  4. 添加 isEmpty() 辅助方法:因为 empty() 不兼容 Countable
  5. 添加 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);
    }
}

参考链接