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 是坏味道。正经做法是把依赖当参数传进来,或者放进对象/容器里。全局变量让函数的输入不可见,测试也没法隔离。
常见坑
strict_types只影响"调用方文件",不是全局开关;每个文件都要写。- 闭包必须
use才能用外部变量,JS 的直觉在这里不成立。 - 默认参数必须是常量表达式(8.1 起允许
new)。 - 命名参数必须写在位置参数后面。
- 引用传参(
&)会让调用处的副作用不可见,滥用是维护债。 array_map传null才能做多数组的"拼元组"。static变量不跨请求保留。- 动态函数名/方法名不校验白名单,等于开了任意调用入口。