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、''、false、null、[])。在处理可能为 0 或 false 的值时,这个区别至关重要。
语法与代码示例
基本用法
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 值
最佳实践
- 替代
isset() + 三元:$a ?? $b比isset($a) ? $a : $b更简洁 - 使用
??=初始化默认值:比if (!isset($a)) $a = $b;更清晰 - 多重回退链:
$a ?? $b ?? $c ?? $d简洁优雅 - 处理
0/false时小心选择:需要区分0和null时用??,不区分时用?: - 与
?->组合使用:$obj?->prop ?? 'default'处理深层嵌套