PHP 基本语法 06|函数:类型声明、引用、闭包与命名参数

最基本的样子

<?php
function add(int $a, int $b): int
{
    return $a + $b;
}

echo add(1, 2);   // 3

PHP 5.6 之后才有的 ...,PHP 7.0 才有的标量类型声明和返回类型,PHP 8.0 才有的联合类型。所以看老代码时,没有类型声明的函数很常见。

函数名不区分大小写(add() 和 ADD() 是同一个函数),但变量名区分。函数一旦定义就不能重复定义,重复声明会抛 Fatal error。

参数默认值

<?php
function greet(string $name = "世界", string $greeting = "你好"): string
{
    return "{$greeting},{$name}";
}

echo greet();            // 你好,世界
echo greet("张三");       // 你好,张三
echo greet("张三", "Hi"); // Hi,张三

默认值的限制:必须是常量表达式。不能写函数调用(除了少数特定情况),不能写变量。

<?php
function f(string $t = time()) { }    // ✗ 非法,time() 不是常量表达式

PHP 8.1 放开了 new 的限制,可以在默认值里创建对象:

<?php
function log(string $msg, Logger $logger = new NullLogger()) { }

类型声明与严格模式

<?php
declare(strict_types=1);

function setAge(int $age): void
{
    echo $age;
}

setAge("28");   // ✗ TypeError: Argument #1 must be of type int, string given

strict_types 是"调用方"的属性,不是"被调用方"的属性。这条非常关键:

  • a.php 里开了 strict_types=1,调用了 b.php 里定义的 foo(int $x),传字符串 → 报错。
  • c.php 里没开,同样调用 foo(int $x),传 "28" → 自动转成 28。

所以能不能严格,取决于"发起调用的那个文件"。这意味着一个开了严格模式的项目,如果被一个没开的入口文件调用,防护就失效了。做法是在每个文件开头都写 declare(strict_types=1),一个不落。

各种类型写法

<?php
function f(
    int $a,                 // 整数
    ?string $b,             // 可空字符串(等价 string|null)
    int|string $c,          // 联合类型(PHP 8.0)
    mixed $d,               // 任意(PHP 8.0)
    iterable $e,            // 数组或 Traversable
    callable $cb,           // 可调用
    object $o,              // 任意对象
    Countable&ArrayAccess $x, // 交集类型(PHP 8.1)
): void {}

function alwaysThrow(): never   // PHP 8.1,表示永不正常返回
{
    throw new RuntimeException("x");
}

null/false/true 作为独立类型在 PHP 8.2 起被允许(之前只能作为联合类型的一部分)。

可变参数

<?php
function sum(int ...$nums): int
{
    return array_sum($nums);
}

echo sum(1, 2, 3);            // 6

// 把数组展开成参数
$list = [1, 2, 3];
echo sum(...$list);           // 6

... 收集后面所有参数到一个数组;调用时 ... 把数组展开。注意可变参数之后的参数在 PHP 里不再允许(没有 Python 那种 *args, key=None 的写法)。

命名参数(PHP 8.0)

<?php
function createUser(
    string $name,
    string $email,
    bool $sendWelcome = false,
    bool $isAdmin = false
): void {}

// 位置参数要跳过中间两个
createUser("张三", "z@example.com", true, false);

// 命名参数:只传需要的(PHP 8.0)
createUser(
    name: "张三",
    email: "z@example.com",
    isAdmin: true,
);

命名参数最大的价值是自解释和跳过默认值。代价是"参数名变成 API 的一部分"——改名就破坏兼容性,所以公共库要谨慎。

一条硬规则:命名参数必须写在位置参数之后。

<?php
createUser("张三", email: "z@example.com");   // ✅
// createUser(name: "张三", "z@example.com"); // ✗ 语法错误

引用传参

<?php
function appendItem(array &$list, string $item): void
{
    $list[] = $item;      // 直接改调用方的数组,不复制
}

$arr = [];
appendItem($arr, "a");
print_r($arr);            // ['a']

什么时候该用引用传参:

  • 需要修改调用方的变量(如上面这种"往数组里追加"的辅助函数)。
  • 大数组/大字符串避免复制——但 PHP 有写时复制,只读不写其实不需要引用,加了反而更慢。
  • 需要返回多个值(不过用数组返回或对象更清楚)。

不要把引用当习惯。加了 & 的函数,调用方看代码时根本不知道变量会被改,这是"隐式副作用",是维护性的灾难。

返回值与返回引用

<?php
function findUser(int $id): ?array
{
    if ($id <= 0) return null;
    return ['id' => $id, 'name' => '张三'];
}

返回引用极少用,典型场景是"返回数组的某个元素以便后续修改":

<?php
class Registry
{
    private array $data = [];

    public function &get(string $key): mixed
    {
        return $this->data[$key];
    }
}

$reg = new Registry();
$val = &$reg->get('x');
$val = 123;      // 直接改了内部数据

这个用法不建议学。它绕过了封装,除了一些极端性能场景,看不出价值。

静态变量:函数内的持久状态

<?php
function counter(): int
{
    static $n = 0;
    return ++$n;
}

echo counter();   // 1
echo counter();   // 2
echo counter();   // 3

static 变量在函数调用结束后仍然保留值,只初始化一次。它在"单次请求内的缓存/单例"里有用,但要清楚:PHP-FPM 下每个请求都是独立的进程上下文,所以 static 状态不会跨请求保留。不要在 static 里存用户数据然后指望下次请求还能读到。

闭包与 use

<?php
$prefix = "ID-";

$fn = function (string $id) use ($prefix): string {
    return $prefix . $id;          // 默认按值捕获
};

echo $fn("001");    // ID-001

闭包必须显式 use 才能访问外部变量,这是 PHP 与 JS 的重要差别(JS 由词法作用域自动捕获)。

按引用捕获:

<?php
$count = 0;
$inc = function () use (&$count) {
    $count++;
};
$inc();
$inc();
echo $count;     // 2

按值捕获时,变量在定义那一刻被"快照",之后外部改动不影响闭包;按引用则同步。

箭头函数 fn(PHP 7.4)

<?php
$factor = 2;

$double = fn($x) => $x * $factor;    // 自动按值捕获外层变量
echo $double(5);                     // 10

$list = array_map(fn($n) => $n * $factor, [1, 2, 3]);

箭头函数的规则:只有一个表达式、自动继承外层变量(按值)、不能有 use 子句。做简单映射、过滤、排序回调时极其方便,能省掉一大段样板。

但要注意它不能按引用捕获。需要修改外部变量时,还得用 function ... use (&$x)。

可调用(callable)与"一等函数"

<?php
// 各种能当回调的写法
$cb1 = 'strtoupper';                       // 函数名
$cb2 = [$obj, 'method'];                   // 对象方法
$cb3 = ['ClassName', 'staticMethod'];      // 静态方法
$cb4 = function ($x) { return $x; };       // 闭包
$cb5 = fn($x) => $x;                       // 箭头函数
$cb6 = strlen(...);                        // 一等可调用语法(PHP 8.1)

strlen(...) 这个语法的好处:它立刻检查函数是否存在,而且在类里可以配合 $this 使用:

<?php
class Formatter
{
    public function run(array $items): array
    {
        return array_map($this->format(...), $items);   // PHP 8.1
    }

    private function format(string $s): string
    {
        return strtoupper($s);
    }
}

以前这里得写 [$this, 'format'],字符串形式 IDE 提示不了、改名会静默出错。$this->format(...) 解决了这个老问题,是 8.1 里最实用的一处改动。

调用一个"变量函数"

<?php
$fn = 'strtoupper';
echo $fn('abc');      // ABC

$method = 'save';
$obj->$method();      // 动态方法名

动态调用一定要校验,否则等于给了外部一个任意函数执行的入口:

<?php
$allowed = ['edit', 'view'];
if (!in_array($action, $allowed, true)) {
    throw new InvalidArgumentException();
}
$obj->$action();

递归

<?php
function factorial(int $n): int
{
    return $n <= 1 ? 1 : $n * factorial($n - 1);
}

PHP 没有尾调用优化,深递归会爆栈(默认受 memory_limit 和栈大小限制)。处理深层树结构时,优先用迭代 + 显式栈:

<?php
function walkTree(array $nodes): array
{
    $stack = [$nodes];
    $result = [];
    while ($stack) {
        $cur = array_pop($stack);
        foreach ($cur as $node) {
            $result[] = $node['id'];
            if (!empty($node['children'])) {
                $stack[] = $node['children'];
            }
        }
    }
    return $result;
}

函数里的作用域

<?php
$globalVar = "外面的";

function bad() {
    echo $globalVar;        // ✗ 未定义(函数内不自动继承全局变量)
}

function useGlobal() {
    global $globalVar;      // 声明才能用(本质是引用 $GLOBALS)
    echo $globalVar;
}

function better() {
    echo $GLOBALS['globalVar'];   // 直白但仍然不推荐
}

用 global 是坏味道。正经做法是把依赖当参数传进来,或者放进对象/容器里。全局变量让函数的输入不可见,测试也没法隔离。

常见坑

  1. strict_types 只影响"调用方文件",不是全局开关;每个文件都要写。
  2. 闭包必须 use 才能用外部变量,JS 的直觉在这里不成立。
  3. 默认参数必须是常量表达式(8.1 起允许 new)。
  4. 命名参数必须写在位置参数后面。
  5. 引用传参(&)会让调用处的副作用不可见,滥用是维护债。
  6. array_map 传 null 才能做多数组的"拼元组"。
  7. static 变量不跨请求保留。
  8. 动态函数名/方法名不校验白名单,等于开了任意调用入口。