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% 的性能。
**先正确,再快。**