Skip to content

合并与差集

概述

PHP 提供了多种数组合并和集合运算函数,包括 array_mergearray_combinearray_diff 系列和 array_intersect 系列。理解这些函数的区别和适用场景,对于正确处理数组数据至关重要。

基础概念

函数分类

分类函数说明
合并array_merge合并数组(字符串键覆盖,数字键重建)
array_merge_recursive递归合并(字符串键递归合并)
array_combine用一个数组做键,一个做值
差集array_diff值差集(忽略键)
array_diff_key键差集
array_diff_assoc键值对差集
交集array_intersect值交集(忽略键)
array_intersect_key键交集
array_intersect_assoc键值对交集

语法与代码

array_merge — 合并数组

php
<?php

declare(strict_types=1);

// 合并索引数组 - 数字键重建
$arr1 = ['a', 'b'];
$arr2 = ['c', 'd'];
$merged = array_merge($arr1, $arr2);
// ['a', 'b', 'c', 'd']

// 合并关联数组 - 后面的覆盖前面的
$defaults = ['timeout' => 30, 'retries' => 3, 'debug' => false];
$userConfig = ['timeout' => 60, 'debug' => true];
$config = array_merge($defaults, $userConfig);
// ['timeout' => 60, 'retries' => 3, 'debug' => true]

// 数字键的特殊行为
$arr1 = [0 => 'a', 1 => 'b'];
$arr2 = [0 => 'c', 1 => 'd'];
$merged = array_merge($arr1, $arr2);
// ['a', 'b', 'c', 'd'](数字键重建,不是覆盖!)

// 使用 + 运算符合并(不同的行为)
$merged = $arr1 + $arr2;
// [0 => 'a', 1 => 'b'](相同键保留第一个)

// + 运算符对关联数组
$defaults = ['a' => 1, 'b' => 2];
$override = ['b' => 20, 'c' => 30];
$merged = $defaults + $override;
// ['a' => 1, 'b' => 2, 'c' => 30](相同键保留第一个)

array_merge_recursive — 递归合并

php
<?php

declare(strict_types=1);

$arr1 = ['user' => ['name' => 'Alice', 'age' => 30]];
$arr2 = ['user' => ['name' => 'Bob', 'email' => 'b@b.com']];

// array_merge - 后者覆盖前者
$merged = array_merge($arr1, $arr2);
// ['user' => ['name' => 'Bob', 'email' => 'b@b.com']]

// array_merge_recursive - 递归合并
$merged = array_merge_recursive($arr1, $arr2);
// ['user' => ['name' => ['Alice', 'Bob'], 'age' => 30, 'email' => 'b@b.com']]

// 注意:相同字符串键的值会被合并为数组
$arr1 = ['color' => 'red', 'size' => 'M'];
$arr2 = ['color' => 'blue'];
$merged = array_merge_recursive($arr1, $arr2);
// ['color' => ['red', 'blue'], 'size' => 'M']

array_combine — 创建关联数组

php
<?php

declare(strict_types=1);

$keys = ['name', 'age', 'email'];
$values = ['Alice', 30, 'a@b.com'];

$combined = array_combine($keys, $values);
// ['name' => 'Alice', 'age' => 30, 'email' => 'a@b.com']

// 键值数量不同时产生 ValueError(PHP 8.0+)
try {
    array_combine(['a', 'b'], [1, 2, 3]);  // ValueError
} catch (\ValueError $e) {
    echo $e->getMessage();
}

array_diff — 值差集

php
<?php

declare(strict_types=1);

// array_diff - 值差集(忽略键,只比较值)
$arr1 = [1, 2, 3, 4, 5];
$arr2 = [3, 4, 5, 6, 7];
$diff = array_diff($arr1, $arr2);
// [0 => 1, 1 => 2](在 $arr1 中但不在 $arr2 中的值)

// array_diff_key - 键差集
$config1 = ['debug' => true, 'cache' => true, 'log' => false];
$config2 = ['cache' => true, 'timeout' => 30];
$diff = array_diff_key($config1, $config2);
// ['debug' => true, 'log' => false]

// array_diff_assoc - 键值对差集
$arr1 = ['a' => 1, 'b' => 2, 'c' => 3];
$arr2 = ['a' => 1, 'c' => 30, 'd' => 4];
$diff = array_diff_assoc($arr1, $arr2);
// ['b' => 2, 'c' => 3](键值对不完全相同的)

array_intersect — 值交集

php
<?php

declare(strict_types=1);

// array_intersect - 值交集(忽略键)
$arr1 = [1, 2, 3, 4, 5];
$arr2 = [3, 4, 5, 6, 7];
$inter = array_intersect($arr1, $arr2);
// [2 => 3, 3 => 4, 4 => 5](保留 $arr1 的键)

// array_intersect_key - 键交集
$conf1 = ['debug' => true, 'cache' => true, 'log' => false];
$conf2 = ['cache' => true, 'debug' => false];
$inter = array_intersect_key($conf1, $conf2);
// ['debug' => true, 'cache' => true](保留 $conf1 的值)

// array_intersect_assoc - 键值对交集
$arr1 = ['a' => 1, 'b' => 2, 'c' => 3];
$arr2 = ['a' => 1, 'b' => 20, 'c' => 3];
$inter = array_intersect_assoc($arr1, $arr2);
// ['a' => 1, 'c' => 3](键值完全相同的)

详细说明

array_merge vs + 运算符

操作数字键字符串键
array_merge重建索引后者覆盖前者
+保留第一个保留第一个
php
<?php

declare(strict_types=1);

// 配置合并推荐使用 array_merge(后者覆盖前者)
$defaults = ['a' => 1, 'b' => 2, 'c' => 3];
$override = ['b' => 20, 'd' => 40];
echo array_merge($defaults, $override)['b'];  // 20
echo ($defaults + $override)['b'];             // 2

// 确保默认值存在推荐使用 + 或 ??

选择建议

需要后者覆盖前者(配置合并)用 array_merge。需要保留第一个值(确保键存在)用 +??

实战示例

深度合并配置

php
<?php

declare(strict_types=1);

function deepMerge(array $base, array $override): array
{
    foreach ($override as $key => $value) {
        if (is_array($value) && isset($base[$key]) && is_array($base[$key])) {
            $base[$key] = deepMerge($base[$key], $value);
        } else {
            $base[$key] = $value;
        }
    }
    return $base;
}

$defaults = [
    'db' => ['host' => 'localhost', 'port' => 3306, 'charset' => 'utf8'],
    'cache' => ['enabled' => true, 'ttl' => 3600],
];

$custom = [
    'db' => ['host' => '192.168.1.1', 'port' => 3307],
    'cache' => ['ttl' => 7200],
];

$config = deepMerge($defaults, $custom);
// db: host=192.168.1.1, port=3307, charset=utf8
// cache: enabled=true, ttl=7200

多维数组合并

php
<?php

declare(strict_types=1);

// 多维数组深度合并
function deepMerge(array $base, array $override): array
{
    foreach ($override as $key => $value) {
        if (is_array($value) && isset($base[$key]) && is_array($base[$key])) {
            $base[$key] = deepMerge($base[$key], $value);
        } else {
            $base[$key] = $value;
        }
    }
    return $base;
}

$defaults = [
    'logging' => [
        'driver' => 'daily',
        'path' => '/var/log/app.log',
        'level' => 'debug',
        'days' => 14,
    ],
    'cache' => [
        'default' => 'redis',
        'stores' => [
            'redis' => ['host' => '127.0.0.1', 'port' => 6379],
            'file' => ['path' => '/tmp/cache'],
        ],
    ],
];

$custom = [
    'logging' => [
        'level' => 'warning',
        'path' => '/var/log/custom.log',
    ],
    'cache' => [
        'stores' => [
            'redis' => ['host' => '192.168.1.100', 'password' => 'secret'],
        ],
    ],
];

$config = deepMerge($defaults, $custom);
print_r($config);

差集与交集的实际应用

php
<?php

declare(strict_types=1);

// 数据库迁移:对比新增和删除的字段
$oldColumns = ['id', 'name', 'email', 'phone', 'fax'];
$newColumns = ['id', 'name', 'email', 'address', 'zip', 'phone'];

$added = array_diff($newColumns, $oldColumns);
$removed = array_diff($oldColumns, $newColumns);
$common = array_intersect($oldColumns, $newColumns);

echo "新增字段: " . implode(', ', $added) . "\n";     // address, zip
echo "删除字段: " . implode(', ', $removed) . "\n";   // fax
echo "保留字段: " . implode(', ', $common) . "\n";     // id, name, email, phone

// 权限对比
$oldPermissions = ['read', 'write', 'delete', 'admin'];
$newPermissions = ['read', 'write', 'execute', 'admin'];

$granted = array_diff($newPermissions, $oldPermissions);  // execute
$revoked = array_diff($oldPermissions, $newPermissions);  // delete

echo "新增权限: " . implode(', ', $granted) . "\n";
echo "移除权限: " . implode(', ', $revoked) . "\n";

array_combine 的实际应用

php
<?php

declare(strict_types=1);

// CSV 头部映射
$header = ['id', 'name', 'email', 'department'];
$row = ['1', 'Alice', 'alice@example.com', 'Engineering'];

$record = array_combine($header, $row);
// ['id' => '1', 'name' => 'Alice', 'email' => 'alice@example.com', 'department' => 'Engineering']

// 类型转换
$typed = array_map(function (string $value, string $key): mixed {
    return match ($key) {
        'id' => (int)$value,
        default => $value,
    };
}, $row, $header);
// [0 => 1, 1 => 'Alice', 2 => 'alice@example.com', 3 => 'Engineering']

// 构建查找表
$ids = [101, 102, 103, 104];
$names = ['Alice', 'Bob', 'Charlie', 'Diana'];
$lookup = array_combine($ids, $names);
// [101 => 'Alice', 102 => 'Bob', 103 => 'Charlie', 104 => 'Diana']

array_diff 比较规则

array_diff 使用松散比较(==)。字符串 '1' 和整数 1 被视为相等。如果需要严格比较,使用 array_uintersectarray_filter 配合 ===

实际应用:数据同步

php
<?php

declare(strict_types=1);

// 数据同步:比较本地和远程数据
function syncData(array $local, array $remote): array
{
    $localIds = array_column($local, 'id');
    $remoteIds = array_column($remote, 'id');

    return [
        'to_create' => array_values(array_filter($remote,
            fn(array $r): bool => !in_array($r['id'], $localIds, true))),
        'to_update' => array_values(array_filter($remote,
            fn(array $r): bool => in_array($r['id'], $localIds, true))),
        'to_delete' => array_values(array_filter($local,
            fn(array $l): bool => !in_array($l['id'], $remoteIds, true))),
    ];
}

$local = [['id' => 1, 'name' => 'A'], ['id' => 2, 'name' => 'B']];
$remote = [['id' => 2, 'name' => 'B-updated'], ['id' => 3, 'name' => 'C']];

print_r(syncData($local, $remote));
// to_create: [[id=>3, name=>C]]
// to_update: [[id=>2, name=>B-updated]]
// to_delete: [[id=>1, name=>A]]

注意事项

array_merge 数字键行为

array_merge 对数字键总是重建索引(从 0 开始)。如果需要保留数字键,使用 + 运算符。

array_diff 比较规则

array_diff 使用松散比较(==),'1'1 被视为相等。PHP 8.0+ 的比较行为可能略有不同。

最佳实践

  1. 配置合并用 array_merge:后者覆盖前者
  2. 确保键存在用 +:保留第一个值
  3. 深度合并用自定义函数array_merge_recursive 的行为可能不符合预期
  4. 差集用 array_diff_key:比 array_diff 更精确
  5. 交集用 array_intersect_assoc:键值完全匹配

array_replace 递归替换

php
<?php

declare(strict_types=1);

// array_replace - 替换数组元素(保持键名)
$base = ['a' => 1, 'b' => 2, 'c' => 3, 10 => 'ten'];
$replace = ['b' => 20, 10 => 'hundred'];

$result = array_replace($base, $replace);
// ['a' => 1, 'b' => 20, 'c' => 3, 10 => 'hundred']
// 注意:array_replace 保持数字键(不像 array_merge 重建索引)

// array_replace_recursive - 递归替换
$base = ['a' => ['x' => 1, 'y' => 2], 'b' => ['z' => 3]];
$replace = ['a' => ['y' => 20, 'w' => 99]];

$result = array_replace_recursive($base, $replace);
// ['a' => ['x' => 1, 'y' => 20, 'w' => 99], 'b' => ['z' => 3]]

array_replace vs array_merge

array_replace 保持键名(包括数字键),array_merge 对数字键重建索引。关联数组两者行为相同。

参考链接