浪白的博客の小站
首页项目归档照片墙音乐灵境说说杂谈友链关于
封面

PHP 里数组和对象该怎么选

写作时间:2026-07-27 19:50:00
# PHP
# 性能
# 设计

PHP 里表示一条数据,可以用关联数组,也可以定义类。选哪个?我经历过从"全用数组"到"尽量用对象"的转变。

先看性能数据

我做了个简单的基准测试(PHP 8.2):

// 数组
$start = microtime(true);
$data = [];
for ($i = 0; $i < 1000000; $i++) {
    $item = [
        'id' => $i,
        'name' => 'user_' . $i,
        'score' => $i % 100,
    ];
    $data[] = $item['id'];
}
echo microtime(true) - $start;   // ~0.18s

// 对象(stdClass)
$start = microtime(true);
$data = [];
for ($i = 0; $i < 1000000; $i++) {
    $item = new stdClass();
    $item->id = $i;
    $item->name = 'user_' . $i;
    $item->score = $i % 100;
    $data[] = $item->id;
}
echo microtime(true) - $start;   // ~0.35s

数组快大概一倍。内存占用数组也少一些(大概 30%)。

但这是在只做赋值和读取的情况下。 真实的业务逻辑里,时间基本花在数据库、模板渲染、序列化上,这点差异占比很小。

我实测过一个真实接口:把 DTO 从数组改成对象,响应时间从 42ms 变成 44ms。多了 5%。但那个接口的错误率从每周两三次变成了零——因为类型错误在开发期就被发现了。

所以我的结论是:除非在极热的循环里,否则按可读性选,不按性能选。

数组的问题

一、拼写错误不会报错

$user = ['name' => 'x', 'email' => 'y'];
echo $user['nmae'];   // Warning,返回 null

PHP 8 会给 Warning,但如果 display_errors 关了、日志没看,就是静默的 null。

更糟的是写入:

$user['nmae'] = 'z';   // 静默创建了一个新键,不报错

如果这是要更新数据库的字段,会更新到错误的地方或者干脆丢失。

二、嵌套深了没法看

$order['items'][$i]['product']['attributes']['color']

这种代码写出来,谁也不知道中间的键是不是都存在。得加一堆 ?? 保护:

$color = $order['items'][$i]['product']['attributes']['color'] ?? null;

但这样又掩盖了"数据缺失"这个真问题。

三、没有类型信息

函数签名 function process(array $order) 完全没说这个数组里有什么。得读实现、看文档、或者打断点。

新同事接手的时候,这个成本很高。

四、数组函数不区分语义

array_filter、array_map 对列表和字典一视同仁。而这两者的语义完全不同,混用容易出错。

比如一个"用户 ID => 用户对象"的 map,用 array_map 处理之后键保留了,但如果是列表,键就重排了。行为不一致。

对象的优势

用个简单的 DTO:

final readonly class User
{
    public function __construct(
        public int $id,
        public string $name,
        public string $email,
        public ?string $avatar = null,
    ) {}

    public static function fromRow(array $row): self
    {
        return new self(
            id: (int) $row['id'],
            name: (string) $row['name'],
            email: (string) $row['email'],
            avatar: $row['avatar'] ?? null,
        );
    }
}

好处:

1. IDE 补全和跳转能用。 打 $user-> 会列出所有属性。数组做不到。

2. 类型确定。 $user->id 一定是 int,不用防御。

3. 拼写错误立即报错。 $user->nmae 是 Error(不是 Warning),而且 PHPStan 静态检查就能抓到。

4. readonly 防止意外修改。 上面用了 final readonly class,创建之后不可变。这消除了一整类 bug——某个函数悄悄改了传进去的参数。

5. 可以带行为。

public function displayName(): string
{
    return $this->name !== '' ? $this->name : '匿名用户';
}
```

这个逻辑有了明确的归属,不用散落在各个地方重复写。

## 我现在的具体做法

**一、领域对象用类。**

用户、订单、商品这些核心概念,定义成类。用了 `readonly` 属性、构造器提升、命名参数。

**二、和数据库/API 的边界做转换。**

```php
// 数据库返回数组,进业务层之前转成对象
$row = $db->fetchAssoc($sql);
$user = User::fromRow($row);

// 出 API 的时候转回数组
return $user->toArray();
```

转换代码有点啰嗦,但它把"外部数据格式可能变"这件事限制在了一个地方。

**三、简单的一次性数据还是用数组。**

```php
// 就是给模板传几个变量,没必要建类
return view('dashboard', [
    'total' => $total,
    'today' => $today,
]);
```

**四、列表用数组,映射用 `array` + 注释,或者 `SplObjectStorage`。**

PHP 没有泛型,所以 `array<int, User>` 只能写在 docblock 里给静态分析工具看:

```php
/** @var array<int, User> $users */
$users = array_map(fn($r) => User::fromRow($r), $rows);
```

PHPStan 会检查这个注解,效果还不错。

**五、热路径上可以破例。**

如果 profile 显示某个循环是瓶颈,那里可以用数组。但要加注释说明为什么,并且要有 benchmark 证明。

## 一个具体例子

改之前(数组):

```php
function calculateDiscount(array $order): float
{
    $total = $order['total'] ?? 0;
    $level = $order['user']['level'] ?? 'normal';
    $coupon = $order['coupon']['value'] ?? 0;

    if ($level === 'vip') {
        return $total * 0.1 + $coupon;
    }
    // ...
}
```

问题:`$order` 里到底该有什么完全不知道。`$coupon['value']` 和 `$total` 的单位一致吗?是元还是分?

改之后:

```php
final readonly class Money
{
    private function __construct(public int $cents) {}

    public static function fromCents(int $cents): self
    {
        return new self($cents);
    }

    public static function fromYuan(float $yuan): self
    {
        return new self((int) round($yuan * 100));
    }

    public function plus(self $other): self
    {
        return new self($this->cents + $other->cents);
    }

    public function percent(float $p): self
    {
        return new self((int) round($this->cents * $p));
    }

    public function toYuan(): float
    {
        return $this->cents / 100;
    }
}

function calculateDiscount(Order $order): Money
{
    $base = $order->total->percent(0.1);

    if ($order->user->level === UserLevel::Vip) {
        return $base->plus($order->coupon?->value ?? Money::fromCents(0));
    }
    // ...
}
```

代码长了,但:

- 单位问题从类型上解决了(都是 `Money`,内部是分)
- `$order->coupon` 可能是 null,用 `?->` 和 `??` 显式处理
- 等级用 enum,不会出现 `'vip'` 拼成 `'Vip'` 的问题
- IDE 补全能用

## 关于性能的最后一句

如果真的很在意性能,PHP 里更有效的优化是:

1. **减少数据库查询**(N+1 是最大的浪费)
2. **用 Opcache 并正确配置**
3. **减少序列化/反序列化**(比如别在缓存里存大对象)
4. **用 JIT 优化热点计算**

数组 vs 对象那点差异,排在很后面。

我见过为了"性能"把代码全写成数组,结果因为字段拼错导致数据错误,排查花的钱远超那 5% 的性能。

**先正确,再快。**
avatar

浪白

邪恶的浪白

RECOMMENDED

Composer 用了这么多年,几个我认为值得坚持的习惯

2026-04-27 19:15:00

context 传参这件事,我踩过的三个坑

2026-05-06 20:05:00

Go 的 error 处理,用久了反而觉得还行

2026-04-19 22:15:00

Table of Contents