Skip to content

Nullsafe 运算符 ?->

概述

Nullsafe 运算符 ?->(Null-safe Operator)是 PHP 8.0 引入的特性,用于安全地链式访问对象属性和方法,当链中某个环节为 null 时,整个表达式的求值会短路并返回 null,而不会抛出错误。

版本要求

Nullsafe 运算符 ?-> 需要 PHP 8.0+。在 PHP 7.x 中需要使用冗长的 if 检查或 ?? 运算符来实现类似功能。

基础概念

核心语法

php
// 语法:object?->property
// 语法:object?->method()
// 语法:object?->property?->method()?->property

// 当左侧为 null 时,短路返回 null
// 当左侧不为 null 时,正常访问

解决的问题

在没有 Nullsafe 运算符之前,深层嵌套的属性访问需要层层检查:

php
// PHP 7.x 中的写法(冗长)
$country = null;
if ($user !== null) {
    $address = $user->getAddress();
    if ($address !== null) {
        $country = $address->getCountry();
    }
}

// PHP 8.0+ Nullsafe 写法(简洁)
$country = $user?->getAddress()?->getCountry();

语法与代码示例

基本用法

php
<?php

declare(strict_types=1);

class Address
{
    public function __construct(
        public ?string $country = null,
        public ?string $city = null
    ) {}

    public function getCountry(): ?string
    {
        return $this->country;
    }

    public function getFullAddress(): string
    {
        return "{$this->city}, {$this->country}";
    }
}

class User
{
    public ?Address $address = null;

    public function getAddress(): ?Address
    {
        return $this->address;
    }
}

// 有值的场景
$user = new User();
$user->address = new Address('China', 'Shanghai');

echo $user?->getAddress()?->getCountry() . "\n"; // China
echo $user?->getAddress()?->getFullAddress() . "\n"; // Shanghai, China

// 中间值为 null 的场景
$user2 = new User();
echo $user2?->getAddress()?->getCountry() . "\n"; // (无输出,返回 null)
var_dump($user2?->getAddress()?->getCountry()); // NULL

链式空安全调用

php
<?php

declare(strict_types=1);

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

class User
{
    public ?Profile $profile = null;
}

class Profile
{
    public ?string $avatar = null;
}

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

class Profile
{
    public ?string $avatar = null;
    public ?Company $company = null;
}

// 多层嵌套调用
$session = new Session();
$session->user = new User();
$session->user->profile = new Profile();
$session->user->profile->company = new Company();

// 安全获取嵌套属性
$companyName = $session->user?->profile?->company?->name;
var_dump($companyName); // NULL(Company 的 name 未设置)

// 当某一层为 null 时短路
$session2 = new Session();
$companyName2 = $session2->user?->profile?->company?->name;
var_dump($companyName2); // NULL —— user 为 null,直接短路

Nullsafe 与方法调用

php
<?php

declare(strict_types=1);

class OrderProcessor
{
    private ?Logger $logger = null;

    public function process(string $orderId): void
    {
        echo "Processing order: {$orderId}\n";
    }
}

class Logger
{
    public function log(string $message): void
    {
        echo "[LOG] {$message}\n";
    }
}

// 使用 nullsafe 调用方法
$processor = new OrderProcessor();

// logger 为 null,不会调用 log(),也不会报错
$processor->logger?->log("Starting process");

// 设置 logger 后正常调用
$processor->logger = new Logger();
$processor->logger?->log("Starting process");
// 输出: [LOG] Starting process

// 链式调用中混合使用 nullsafe
function processOrder(?OrderProcessor $processor, string $orderId): void
{
    $processor?->process($orderId);
}

详细说明

?->?? 的区别

php
<?php

declare(strict_types=1);

class Config
{
    public ?string $value = null;
}

// ?? 处理的是值层面的 null
// ?-> 处理的是对象层面的 null(访问链路)

$config = new Config();

// ?->:当对象可能为 null 时
var_dump($config?->value);  // null

// ??:当值可能为 null 时,提供默认值
var_dump($config->value ?? 'default');  // 'default'

// 组合使用
var_dump($config?->value ?? 'default');  // 'default'

// 另一个区别:?? 检查值是否为 null
// ?-> 检查对象是否为 null
$nullable = null;
var_dump($nullable?->toString()); // null —— 对象为 null
// 如果 $nullable 是一个对象但 toString() 返回 null
// $nullable->toString() ?? 'default' 会返回 'default'

选择指南

  • 对象本身可能为 null → 使用 ?->
  • 对象存在但属性的值可能为 null → 使用 ??
  • 两者可以链式组合:$obj?->getProperty() ?? 'default'

Nullsafe 不是写入操作

php
<?php

declare(strict_types=1);

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

$user = null;

// ?-> 可以用于读取
var_dump($user?->name); // null

// ?-> 不能用于写入赋值!
// $user?->name = 'Alice'; // 这会在 PHP 8.0+ 中报错
// 实际上这是合法的但无意义:null?->name = 'Alice' 不会做任何事

// 如果需要写入,使用显式检查
if ($user !== null) {
    $user->name = 'Alice';
}

只读语义

Nullsafe 运算符仅适用于读取,不适用于写入。不要尝试使用 $obj?->property = $value,这样的赋值要么无意义,要么可能导致 Bug。

Nullsafe 的短路求值

php
<?php

declare(strict_types=1);

$callCount = 0;

function sideEffect(): string
{
    global $callCount;
    $callCount++;
    return 'result';
}

$obj = null;

// nullsafe 短路:一旦遇到 null,后续的链式调用不会执行
$result = $obj?->method(sideEffect())?->anotherMethod();
var_dump($result);    // null
var_dump($callCount); // 0 —— sideEffect() 根本没有被调用!

$obj2 = new stdClass();
$obj2->method = function(string $arg): ?stdClass {
    return new stdClass();
};

// 当对象不为 null 时,链式调用正常执行
// 但注意:这里 method 是属性,不是方法
// 实际中 $obj?->method() 是方法调用

实战示例

深层配置访问

php
<?php

declare(strict_types=1);

class DatabaseConfig
{
    public function __construct(
        public ?string $host = null,
        public ?int $port = null
    ) {}
}

class CacheConfig
{
    public function __construct(
        public ?string $driver = null,
        public ?int $ttl = null
    ) {}
}

class AppConfig
{
    public ?DatabaseConfig $database = null;
    public ?CacheConfig $cache = null;

    public function getDatabaseConfig(): ?DatabaseConfig
    {
        return $this->database;
    }

    public function getCacheConfig(): ?CacheConfig
    {
        return $this->cache;
    }
}

// 安全获取深层配置值
function getDbHost(AppConfig $config): string
{
    return $config->getDatabaseConfig()?->host ?? 'localhost';
}

function getCacheTtl(AppConfig $config): int
{
    return $config->getCacheConfig()?->ttl ?? 3600;
}

$config = new AppConfig();
$config->database = new DatabaseConfig('192.168.1.100', 3306);

echo getDbHost($config) . "\n";  // 192.168.1.100
echo getCacheTtl($config) . "\n"; // 3600(cache 未设置)

API 响应处理

php
<?php

declare(strict_types=1);

class ApiResponse
{
    public function __construct(
        public ?DataWrapper $data = null,
        public ?ErrorInfo $error = null
    ) {}

    public function getData(): ?DataWrapper
    {
        return $this->data;
    }
}

class DataWrapper
{
    public function __construct(
        public ?UserInfo $user = null
    ) {}

    public function getUser(): ?UserInfo
    {
        return $this->user;
    }
}

class UserInfo
{
    public function __construct(
        public ?string $name = null,
        public ?string $email = null
    ) {}
}

class ErrorInfo
{
    public function __construct(public string $message) {}
}

// 安全解析 API 响应
function extractUserName(?ApiResponse $response): string
{
    return $response?->getData()?->getUser()?->name ?? 'Unknown';
}

// 错误响应(data 为 null)
$errorResponse = new ApiResponse(error: new ErrorInfo('Not found'));
echo extractUserName($errorResponse) . "\n"; // Unknown

// 成功响应
$successResponse = new ApiResponse(
    data: new DataWrapper(user: new UserInfo(name: 'Alice', email: 'alice@example.com'))
);
echo extractUserName($successResponse) . "\n"; // Alice

// null 响应
echo extractUserName(null) . "\n"; // Unknown

模板渲染中的安全访问

php
<?php

declare(strict_types=1);

class ViewContext
{
    public function __construct(
        public ?User $currentUser = null,
        public ?Navigation $navigation = null
    ) {}
}

class User
{
    public function __construct(public ?string $displayName = null) {}
}

class Navigation
{
    public function __construct(public ?string $activeMenu = null) {}
}

function renderBreadcrumb(ViewContext $context): string
{
    $userName = $context->currentUser?->displayName ?? 'Guest';
    $activeMenu = $context->navigation?->activeMenu ?? 'Home';

    return "User: {$userName} | Menu: {$activeMenu}";
}

echo renderBreadcrumb(new ViewContext()) . "\n";
// User: Guest | Menu: Home

echo renderBreadcrumb(new ViewContext(
    currentUser: new User('Alice'),
    navigation: new Navigation('Dashboard')
)) . "\n";
// User: Alice | Menu: Dashboard

注意事项

  • 仅适用于 PHP 8.0+:PHP 7.x 不支持此运算符
  • 仅用于读取:不能用于写入赋值操作
  • 短路求值:遇到 null 后,后续链式调用不会执行
  • 不要滥用:并非所有 null 检查都需要 ?->,简单的 isset()/?? 可能更合适
  • 方法调用返回 null?->method() 返回 null 时可以继续链式调用

最佳实践

  1. 链式属性访问使用 ?->:替代层层嵌套的 if 检查
  2. 提供默认值使用 ??$obj?->prop ?? 'default' 组合使用
  3. 不要在循环中滥用:大量链式 nullsafe 调用可能影响性能
  4. 数据结构设计时减少 nullable:考虑使用 Null Object 模式
  5. 保持链式深度合理:超过 3-4 层时,考虑重构数据结构

参考链接