never 返回类型
概述
never 是 PHP 8.1 引入的返回类型,表示函数永远不会正常返回。声明为 never 的函数要么抛出异常,要么调用 exit()/die() 终止程序,要么进入无限循环。never 是类型理论中的最底层类型(bottom type),是所有其他类型的子类型。
核心要点
never只能用于返回类型声明,不能用于参数类型或属性类型。never函数必须通过抛出异常、调用exit()或无限循环来终止执行。never不能作为联合类型的一部分(如never|int是非法的)。never是所有类型的子类型,可以替换任何返回类型(协变)。- PHP 8.1+ 可用。
基础概念
什么是 never
never 表示"这个函数永远不会正常返回"。如果一个函数被声明为 never,PHP 将确保:
- 函数内部调用了
exit()、die() - 函数抛出了异常(
throw) - 函数进入了无限循环
如果 never 函数执行到了末尾(即正常返回),PHP 会抛出 TypeError。
never 与 void 的区别
这是最容易混淆的概念,需要特别注意:
| 特性 | void | never |
|---|---|---|
| 可用版本 | PHP 7.1+ | PHP 8.1+ |
| 含义 | 函数不返回值,但会正常结束 | 函数永远不正常结束 |
| 可以省略 return | 可以 | 可以,但必须有 exit/throw/循环 |
return; 是否合法 | 合法 | 非法(Fatal Error) |
return null; 是否合法 | 非法 | 非法 |
| 正常执行到函数末尾 | 合法 | 非法(TypeError) |
| 在联合类型中 | 不允许 | 不允许 |
| 类型层次 | 无返回值 | 所有类型的子类型 |
语法与代码示例
抛出异常
<?php
declare(strict_types=1);
function throwInvalidArgumentException(string $message): never
{
throw new InvalidArgumentException($message);
}
function assertNotNull(mixed $value, string $name = 'value'): never
{
if ($value === null) {
throw new InvalidArgumentException(
sprintf('Expected %s not to be null', $name)
);
}
}
// assertNotNull() 后面的代码永远不会执行
assertNotNull($data, '$data');
echo "This line will never be reached if $data is null";调用 exit()
<?php
declare(strict_types=1);
function terminate(int $statusCode): never
{
echo "Shutting down with status {$statusCode}" . PHP_EOL;
exit($statusCode);
}
function handleCliCommand(array $argv): never
{
$command = $argv[1] ?? null;
match ($command) {
'start' => startServer(),
'stop' => stopServer(),
default => terminate(1),
};
}在 match 表达式中使用 never
<?php
declare(strict_types=1);
function handleHttpMethod(string $method): string
{
return match ($method) {
'GET' => 'Read resource',
'POST' => 'Create resource',
'PUT' => 'Update resource',
'DELETE' => 'Delete resource',
default => throw new BadRequestException(
"Unsupported HTTP method: {$method}"
),
};
}
// match 的 default 分支使用 never 确保穷举所有情况
function processStatus(string $status): string
{
return match ($status) {
'active' => 'User is active',
'inactive' => 'User is inactive',
'banned' => 'User is banned',
default => throw new LogicException("Unknown status: {$status}"),
};
}never 作为 throw 表达式(PHP 8.0+)
<?php
declare(strict_types=1);
function ensureDirectoryExists(string $path): void
{
if (!is_dir($path)) {
throw new RuntimeException("Directory not found: {$path}");
}
}
// never 函数可以作为表达式的一部分使用
function getConfigValue(string $key): mixed
{
return $GLOBALS['config'][$key]
?? throw new InvalidArgumentException("Config key not found: {$key}");
}never 函数执行完毕的错误
<?php
declare(strict_types=1);
function myNeverFunction(): never
{
echo "Hello";
// 函数在这里正常结束 —— 错误!
}
// Fatal Error: Uncaught TypeError: myNeverFunction():
// Return value must be of type never, none returned致命错误
如果 never 函数正常执行完毕而没有抛出异常或调用 exit(),PHP 将抛出 TypeError。这实际上是一个有用的安全机制,确保你在代码审查时不会遗漏分支处理。
详细说明
never 是最底层类型(Bottom Type)
在类型理论中,never 是最底层类型(也称为空类型或底部类型)。这意味着:
never是所有类型的子类型:在继承场景中,never可以替换任何返回类型。never没有实例:不存在属于never类型的值。- 联合类型中的
never是冗余的:int|never等价于int,因此 PHP 不允许这样做。
<?php
declare(strict_types=1);
interface ProcessorInterface
{
public function process(): string;
}
class AlwaysThrows implements ProcessorInterface
{
public function process(): never
{
// never 是 string 的子类型,因此协变合法
throw new RuntimeException("Always throws");
}
}never 在代码分析中的价值
never 返回类型不仅是运行时的约束,更是静态分析工具的信号。IDE 和静态分析器可以利用 never 来推断代码的可达性(reachability):
<?php
declare(strict_types=1);
function validateAge(int $age): never
{
if ($age < 0 || $age > 150) {
throw new ValueError("Invalid age: {$age}");
}
}
function processAge(int $age): string
{
validateAge($age);
// 静态分析器知道:执行到这里 $age 一定合法(0~150)
return "Age is {$age}";
}never 不能用于参数或属性
<?php
declare(strict_types=1);
// 错误:never 不能用于参数
function badParam(never $x): void {} // Fatal Error
// 错误:never 不能用于属性
class BadClass
{
public never $property; // Fatal Error
}实战示例
参数验证器
<?php
declare(strict_types=1);
class Assert
{
public static function that(mixed $value, string $name = 'value'): self
{
return new self($value, $name);
}
public function __construct(
private mixed $value,
private string $name
) {
}
public function isString(): self
{
if (!is_string($this->value)) {
throw new TypeError(
sprintf('Expected %s to be string, got %s',
$this->name,
get_debug_type($this->value)
)
);
}
return $this;
}
public function isNotEmpty(): self
{
if ($this->value === '' || $this->value === null || $this->value === []) {
throw new InvalidArgumentException(
sprintf('Expected %s not to be empty', $this->name)
);
}
return $this;
}
public function notNull(): self
{
if ($this->value === null) {
throw new InvalidArgumentException(
sprintf('Expected %s not to be null', $this->name)
);
}
return $this;
}
public function intRange(int $min, int $max): self
{
if ($this->value < $min || $this->value > $max) {
throw new OutOfRangeException(
sprintf('Expected %s to be between %d and %d, got %d',
$this->name, $min, $max, $this->value)
);
}
return $this;
}
}
// 使用示例
Assert::that($username, 'username')->isString()->isNotEmpty();
Assert::that($age, 'age')->intRange(0, 150);命令行控制器
<?php
declare(strict_types=1);
class CliApplication
{
private array $routes = [];
public function register(string $command, callable $handler): void
{
$this->routes[$command] = $handler;
}
public function run(array $argv): never
{
$command = $argv[1] ?? 'help';
if (!isset($this->routes[$command])) {
$this->printError("Unknown command: {$command}");
$this->showHelp();
exit(1);
}
($this->routes[$command])(array_slice($argv, 2));
exit(0);
}
private function showHelp(): never
{
echo "Usage: php app.php <command> [options]" . PHP_EOL;
echo "Available commands:" . PHP_EOL;
foreach (array_keys($this->routes) as $cmd) {
echo " - {$cmd}" . PHP_EOL;
}
exit(0);
}
private function printError(string $message): void
{
fwrite(STDERR, "Error: {$message}" . PHP_EOL);
}
}枚举穷举检查
<?php
declare(strict_types=1);
enum Status: string
{
case Active = 'active';
case Inactive = 'inactive';
case Banned = 'banned';
}
function getStatusLabel(Status $status): string
{
return match ($status) {
Status::Active => 'Active',
Status::Inactive => 'Inactive',
Status::Banned => 'Banned',
};
}
// 如果新增了枚举值但忘记在 match 中处理,
// never 分支会在运行时抛出未处理异常
function handleStatus(Status $status): never
{
match ($status) {
Status::Active => doActive(),
Status::Inactive => doInactive(),
Status::Banned => doBanned(),
};
// 如果枚举新增了值而 match 没有覆盖,
// PHP 会报 TypeError(never 函数正常返回)
}穷举检查技巧
在 match 表达式后面添加一个永远不会到达的代码路径,可以用于检测未来新增枚举值时是否遗漏了分支处理。never 返回类型可以强化这一点。
注意事项
never 与 void 的选择指南
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| 函数执行完毕,不返回数据 | void | 函数正常结束 |
| 函数抛出异常表示错误 | never | 函数不会正常返回 |
| 函数调用 exit() 终止程序 | never | 函数不会正常返回 |
| 函数进入无限循环 | never | 函数不会正常返回 |
| 函数可能返回值或抛出异常 | 具体类型(如 string) | 异常不是"正常返回" |
常见错误
| 错误写法 | 正确写法 | 说明 |
|---|---|---|
function f(): never { return; } | function f(): never { exit(); } | never 函数不能有 return |
function f(never $x): void | function f(int $x): void | never 不能用于参数 |
function f(): never|int | function f(): never | never 不能在联合类型中 |
function f(): never { echo "ok"; } | function f(): void { echo "ok"; } | 如果函数正常结束,用 void |
never 的继承与协变
由于 never 是所有类型的子类型,它可以在子类中替换任何返回类型:
<?php
declare(strict_types=1);
abstract class Controller
{
abstract public function execute(): mixed;
}
class ExitController extends Controller
{
public function execute(): never
{
echo "Application shutting down..." . PHP_EOL;
exit(0);
// never 是 mixed 的子类型,协变合法
}
}最佳实践
用于"守卫函数":参数验证失败时抛出异常的函数应该声明为
never,明确告知调用者这些函数不会正常返回。用于 CLI 入口点:命令行应用的入口函数通常会调用
exit(),适合声明为never。用于 match 的 default 分支:在 match 表达式中,如果 default 分支用于抛出异常表示"不应该到达这里",整个函数可以是
never。配合静态分析工具:
never可以帮助 PHPStan、Psalm 等工具检测死代码和未覆盖的分支。不要过度使用:只有函数确实永远不会正常返回时才使用
never。如果一个函数可能正常结束,应使用void或具体类型。与 throw 表达式结合:PHP 8.0 引入的 throw 表达式使得
never在 match 和三元表达式中更加自然地使用。