Skip to content

Null 合并运算符 ??

概述

Null 合并运算符 ?? 是 PHP 7.0 引入的特性,用于简化 isset() 加三元表达式的常见模式。当左侧操作数存在且不为 null 时返回左侧值,否则返回右侧值。

版本信息

  • ??:PHP 7.0+ 引入
  • ??=(Null 合并赋值):PHP 7.4+ 引入

基础概念

基本语法

php
// 语法:$a ?? $b
// 等价于:isset($a) ? $a : $b
// 当 $a 存在且不为 null 时返回 $a,否则返回 $b

核心区别

php
// ??:仅当左侧为 null 或未定义时返回右侧
$a = 0;
echo $a ?? 'default'; // 0(0 不是 null)

// ?:(Elvis 运算符):当左侧为 falsy 时返回右侧
$a = 0;
echo $a ?: 'default';  // 'default'(0 是 falsy)

?? vs ?:

?? 只检查 null/未定义,?: 检查所有 falsy 值(0''falsenull[])。在处理可能为 0false 的值时,这个区别至关重要。

语法与代码示例

基本用法

php
<?php

declare(strict_types=1);

// 替代 isset() + 三元表达式
$username = $_GET['username'] ?? 'anonymous';
// PHP 5.x 写法:$username = isset($_GET['username']) ? $_GET['username'] : 'anonymous';

// 处理可能为 null 的返回值
function getUser(int $id): ?string
{
    if ($id === 1) {
        return 'Alice';
    }
    return null;
}

$name = getUser(1) ?? 'Unknown';  // 'Alice'
$name = getUser(2) ?? 'Unknown';  // 'Unknown'

// 链式 null 合并
$name = $_GET['user']['name'] ?? $_POST['user']['name'] ?? 'Guest';

???: 的对比

php
<?php

declare(strict_types=1);

$values = [
    'zero' => 0,
    'empty' => '',
    'false' => false,
    'null' => null,
    'string_zero' => '0',
    'array_empty' => [],
    'normal' => 'hello',
];

// ?? 只在 null 时使用默认值
echo "??:\n";
echo ($values['zero'] ?? 'default') . "\n";         // 0
echo ($values['empty'] ?? 'default') . "\n";        // (空)
echo ($values['false'] ?? 'default') . "\n";        // (空输出,false)
echo ($values['null'] ?? 'default') . "\n";         // default
echo ($values['normal'] ?? 'default') . "\n";      // hello

// ?: 在所有 falsy 值时使用默认值
echo "?:\n";
echo ($values['zero'] ?: 'default') . "\n";         // default
echo ($values['empty'] ?: 'default') . "\n";        // default
echo ($values['false'] ?: 'default') . "\n";        // default
echo ($values['null'] ?: 'default') . "\n";         // default
echo ($values['normal'] ?: 'default') . "\n";       // hello

??= Null 合并赋值(PHP 7.4+)

php
<?php

declare(strict_types=1);

// ??= 仅在左侧为 null 或未定义时赋值
$config = [];

$config['host'] ??= 'localhost';
$config['port'] ??= 3306;
$config['charset'] = 'utf8mb4'; // 已有值
$config['charset'] ??= 'latin1'; // 不赋值

print_r($config);
// ['host' => 'localhost', 'port' => 3306, 'charset' => 'utf8mb4']

// 对象属性
class Settings
{
    public ?string $theme = null;
    public ?string $language = 'en';
}

$settings = new Settings();
$settings->theme ??= 'light';    // theme 为 null,赋值为 'light'
$settings->language ??= 'zh';     // language 为 'en',不赋值

var_dump($settings->theme);    // string(5) "light"
var_dump($settings->language); // string(2) "en"

// 与 0、''、false 的关系
$score = 0;
$score ??= 100; // 仍然是 0,因为 0 不是 null
var_dump($score); // int(0)

$flag = false;
$flag ??= true;  // 仍然是 false,因为 false 不是 null
var_dump($flag); // bool(false)

嵌套使用

php
<?php

declare(strict_types=1);

// 嵌套 null 合并(多重回退)
$request = [
    'user' => [
        // 'name' 未设置
    ],
];

$name = $request['user']['name']
    ?? $request['session']['name']
    ?? $request['cookie']['name']
    ?? 'Guest';

echo $name . "\n"; // Guest

// 嵌套访问中的优先级
// 注意:?? 的优先级低于 .
// $a['key'] ?? 'default' 正常
// "prefix" . $a['key'] ?? 'default' 有问题(先拼接再合并)
$a = [];
echo "value: " . ($a['key'] ?? 'default') . "\n"; // value: default

优先级陷阱

?? 的优先级非常低(仅高于三元、赋值、and/or/xor)。与字符串拼接 . 混用时,必须使用括号:"prefix" . ($a['key'] ?? 'default')

详细说明

??isset() 的精确语义

php
<?php

declare(strict_types=1);

// ?? 的行为等价于 isset() 检查
// 但有一个关键区别:?? 不会触发未定义变量的 Notice

$undefined;

// isset 检查未定义变量
var_dump(isset($undefined));     // bool(false)
var_dump($undefined ?? 'ok');   // 'ok' —— 不触发 Notice

// 但直接访问未定义变量会触发 Notice
// echo $undefined; // Notice: Undefined variable

// 对于未定义的数组键
$arr = [];
var_dump(isset($arr['key']));    // bool(false)
var_dump($arr['key'] ?? 'ok');   // 'ok'

// 对于值为 null 的键
$arr['key'] = null;
var_dump(isset($arr['key']));    // bool(false) —— isset 对 null 返回 false
var_dump($arr['key'] ?? 'ok');   // 'ok' —— 值为 null,使用默认值

// 如果需要区分"未设置"和"值为 null"
// 使用 array_key_exists
$arr = [];
$arr['key'] = null;
var_dump(array_key_exists('key', $arr)); // bool(true)

???-> 的对比

php
<?php

declare(strict_types=1);

class User
{
    public ?string $name = null;
}

class Session
{
    public ?User $user = null;
}

// ?? 处理值层面的 null
$user = new User(name: null);
echo $user->name ?? 'Anonymous' . "\n"; // Anonymous

// ?-> 处理对象层面的 null
$session = new Session();
echo $session->user?->name ?? 'Guest' . "\n"; // Guest
// $session->user 为 null,?-> 短路返回 null
// null ?? 'Guest' = 'Guest'

// 组合使用
$session2 = new Session(user: new User(name: 'Alice'));
echo $session2->user?->name ?? 'Guest' . "\n"; // Alice

组合使用

$obj?->property ?? 'default' 是常见模式:先用 ?-> 安全访问可能为 null 的对象,再用 ?? 为结果提供默认值。

实战示例

配置管理

php
<?php

declare(strict_types=1);

class ConfigManager
{
    private array $config = [];
    private array $defaults;

    public function __construct(array $defaults)
    {
        $this->defaults = $defaults;
    }

    public function load(array $config): void
    {
        $this->config = $config;
    }

    public function get(string $key, mixed $default = null): mixed
    {
        return $this->config[$key] ?? $this->defaults[$key] ?? $default;
    }

    public function setDefaultIfMissing(string $key, mixed $value): void
    {
        $this->config[$key] ??= $value;
    }
}

$manager = new ConfigManager([
    'db_host' => 'localhost',
    'db_port' => 3306,
    'cache_ttl' => 3600,
]);

$manager->load([
    'db_host' => '192.168.1.100',
]);

echo $manager->get('db_host') . "\n";    // 192.168.1.100
echo $manager->get('db_port') . "\n";    // 3306(使用默认值)
echo $manager->get('cache_ttl') . "\n";  // 3600(使用默认值)
echo $manager->get('unknown', 'N/A') . "\n"; // N/A(最终默认值)

$manager->setDefaultIfMissing('timeout', 30);
$manager->setDefaultIfMissing('db_host', '127.0.0.1'); // 已有值,不覆盖

请求参数处理

php
<?php

declare(strict_types=1);

class Request
{
    private array $params;

    public function __construct(array $params)
    {
        $this->params = $params;
    }

    public function getString(string $key, string $default = ''): string
    {
        $value = $this->params[$key] ?? $default;
        return is_string($value) ? $value : $default;
    }

    public function getInt(string $key, int $default = 0): int
    {
        $value = $this->params[$key] ?? $default;
        return is_int($value) ? $value : $default;
    }

    public function getBool(string $key, bool $default = false): bool
    {
        return $this->params[$key] ?? $default;
    }

    public function getArray(string $key, array $default = []): array
    {
        $value = $this->params[$key] ?? $default;
        return is_array($value) ? $value : $default;
    }
}

$request = new Request([
    'page' => 2,
    'limit' => '20', // 字符串而非整数
    'keyword' => 'php',
]);

echo $request->getInt('page', 1) . "\n";       // 2
echo $request->getInt('limit', 10) . "\n";      // 10(字符串不是 int)
echo $request->getString('keyword') . "\n";       // php
echo $request->getString('sort', 'asc') . "\n";  // asc(未设置)

数据库查询结果处理

php
<?php

declare(strict_types=1);

// 模拟数据库查询结果
function fetchUser(int $id): ?array
{
    $users = [
        1 => ['name' => 'Alice', 'email' => 'alice@example.com'],
        2 => ['name' => 'Bob', 'email' => 'bob@example.com'],
    ];
    return $users[$id] ?? null;
}

function fetchUserProfile(int $userId): ?array
{
    $profiles = [
        1 => ['bio' => 'Developer', 'location' => 'Shanghai'],
    ];
    return $profiles[$userId] ?? null;
}

// 使用 ?? 安全获取数据
$userId = 1;
$user = fetchUser($userId);
$profile = fetchUserProfile($userId);

$bio = $user['bio'] ?? $profile['bio'] ?? 'No bio available';
$location = $profile['location'] ?? $user['location'] ?? 'Unknown';
$email = $user['email'] ?? 'No email';

echo "Email: {$email}\n";
echo "Bio: {$bio}\n";
echo "Location: {$location}\n";

// 不存在的用户
$userId = 3;
$user = fetchUser($userId);
$profile = fetchUserProfile($userId);

$name = $user['name'] ?? 'Guest';
echo "Name: {$name}\n"; // Guest

注意事项

  • ?? 仅检查 null 和未定义0''false[] 不会触发回退
  • ??= 同样仅处理 null$score = 0; $score ??= 100; 结果仍然是 0
  • ?? 不会触发未定义变量 Notice:与 isset() 行为一致
  • 优先级极低:与 .+- 混用时务必加括号
  • 不要混淆 ???::前者只看 null,后者看所有 falsy 值

最佳实践

  1. 替代 isset() + 三元$a ?? $bisset($a) ? $a : $b 更简洁
  2. 使用 ??= 初始化默认值:比 if (!isset($a)) $a = $b; 更清晰
  3. 多重回退链$a ?? $b ?? $c ?? $d 简洁优雅
  4. 处理 0/false 时小心选择:需要区分 0null 时用 ??,不区分时用 ?:
  5. ?-> 组合使用$obj?->prop ?? 'default' 处理深层嵌套

参考链接