PHP 箭頭函數 (Arrow Function)

箭頭函數是 PHP 7.4 引入的新語法,提供了更簡潔的方式來撰寫簡單的匿名函數。箭頭函數使用 fn 關鍵字,並且會自動捕獲外部變數。

基本語法

fn(參數) => 表達式
<?php
// 傳統匿名函數
$double = function($n) {
    return $n * 2;
};

// 箭頭函數
$double = fn($n) => $n * 2;

echo $double(5);  // 10
?>

自動捕獲變數

箭頭函數最大的特點是會自動按值捕獲外部變數,不需要使用 use

<?php
$factor = 2;

// 傳統匿名函數需要 use
$multiply = function($n) use ($factor) {
    return $n * $factor;
};

// 箭頭函數自動捕獲
$multiply = fn($n) => $n * $factor;

echo $multiply(5);  // 10
?>

捕獲多個變數

<?php
$greeting = "Hello";
$punctuation = "!";

$greet = fn($name) => "$greeting, $name$punctuation";

echo $greet("Alice");  // Hello, Alice!
?>

捕獲的是定義時的值

<?php
$x = 10;

$getX = fn() => $x;

$x = 20;

echo $getX();  // 10(捕獲的是定義時的值)
?>
箭頭函數是按值捕獲,無法傳址捕獲。如果需要修改外部變數,請使用傳統匿名函數搭配 use (&$var)

型別宣告

箭頭函數支援參數和回傳值的型別宣告:

<?php
// 參數型別
$double = fn(int $n) => $n * 2;

// 回傳型別
$double = fn(int $n): int => $n * 2;

// 可為 null 的型別
$greet = fn(?string $name): string => "Hello, " . ($name ?? "Guest");

echo $greet(null);  // Hello, Guest
?>

作為回呼函數

箭頭函數非常適合用於需要回呼函數的場合:

<?php
$numbers = [1, 2, 3, 4, 5];

// array_map
$doubled = array_map(fn($n) => $n * 2, $numbers);
// [2, 4, 6, 8, 10]

// array_filter
$evens = array_filter($numbers, fn($n) => $n % 2 === 0);
// [2, 4]

// array_reduce
$sum = array_reduce($numbers, fn($carry, $n) => $carry + $n, 0);
// 15

// usort
$names = ['Charlie', 'Alice', 'Bob'];
usort($names, fn($a, $b) => strcmp($a, $b));
// ['Alice', 'Bob', 'Charlie']
?>

巢狀箭頭函數

箭頭函數可以巢狀使用:

<?php
$add = fn($a) => fn($b) => $a + $b;

echo $add(3)(5);  // 8

$add3 = $add(3);
echo $add3(7);  // 10
?>

限制

只能有單一表達式

箭頭函數的主體只能是單一表達式,不能有多行程式碼或陳述式:

<?php
// 正確:單一表達式
$double = fn($n) => $n * 2;

// 錯誤:不能有多行
// $process = fn($n) => {
//     $result = $n * 2;
//     return $result;
// };

// 需要多行時使用傳統匿名函數
$process = function($n) {
    $result = $n * 2;
    return $result;
};
?>

沒有回傳值時

如果不需要回傳值,箭頭函數仍會回傳表達式的結果:

<?php
$log = fn($msg) => print($msg);  // print 會回傳 1

$result = $log("Hello");
echo $result;  // 1

// 如果真的不需要回傳值,可能傳統匿名函數更合適
$log = function($msg): void {
    echo $msg;
};
?>

無法傳址捕獲

<?php
$count = 0;

// 箭頭函數無法傳址捕獲
$increment = fn() => $count++;  // 這不會修改外部的 $count

$increment();
echo $count;  // 0

// 需要使用傳統匿名函數
$increment = function() use (&$count) {
    $count++;
};

$increment();
echo $count;  // 1
?>

箭頭函數 vs 匿名函數

特性箭頭函數匿名函數
語法fn() => exprfunction() { ... }
主體單一表達式多行程式碼
變數捕獲自動按值捕獲需要 use
傳址捕獲不支援支援 use (&$var)
$this自動繼承自動繼承
PHP 版本7.4+5.3+

何時使用箭頭函數

適合使用

  • 簡單的回呼函數
  • 需要捕獲外部變數的單行函數
  • 資料轉換和過濾
<?php
// 資料轉換
$users = [
    ['name' => 'Alice', 'age' => 25],
    ['name' => 'Bob', 'age' => 30],
];

$names = array_map(fn($u) => $u['name'], $users);
$adults = array_filter($users, fn($u) => $u['age'] >= 18);
?>

不適合使用

  • 需要多行邏輯
  • 需要修改外部變數
  • 需要 void 回傳型別
<?php
// 需要多行邏輯 - 使用匿名函數
$process = function($data) {
    $result = [];
    foreach ($data as $item) {
        if ($item['active']) {
            $result[] = transform($item);
        }
    }
    return $result;
};

// 需要修改外部變數 - 使用匿名函數
$counter = 0;
$increment = function() use (&$counter) {
    $counter++;
};
?>

實際應用範例

排序

<?php
$products = [
    ['name' => 'Apple', 'price' => 100],
    ['name' => 'Banana', 'price' => 50],
    ['name' => 'Cherry', 'price' => 200],
];

// 按價格排序
usort($products, fn($a, $b) => $a['price'] <=> $b['price']);

// 按名稱排序
usort($products, fn($a, $b) => strcmp($a['name'], $b['name']));

// 價格降序
usort($products, fn($a, $b) => $b['price'] <=> $a['price']);
?>

資料提取

<?php
$users = [
    ['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com'],
    ['id' => 2, 'name' => 'Bob', 'email' => 'bob@example.com'],
    ['id' => 3, 'name' => 'Charlie', 'email' => 'charlie@example.com'],
];

// 提取 email
$emails = array_map(fn($u) => $u['email'], $users);

// 建立 id => name 對應
$nameById = array_combine(
    array_map(fn($u) => $u['id'], $users),
    array_map(fn($u) => $u['name'], $users)
);
// [1 => 'Alice', 2 => 'Bob', 3 => 'Charlie']
?>

條件過濾

<?php
$products = [
    ['name' => 'A', 'price' => 100, 'stock' => 5],
    ['name' => 'B', 'price' => 50, 'stock' => 0],
    ['name' => 'C', 'price' => 200, 'stock' => 10],
];

// 過濾有庫存的
$inStock = array_filter($products, fn($p) => $p['stock'] > 0);

// 過濾價格範圍
$minPrice = 60;
$maxPrice = 150;
$filtered = array_filter(
    $products,
    fn($p) => $p['price'] >= $minPrice && $p['price'] <= $maxPrice
);
?>

驗證

<?php
$validators = [
    'email' => fn($v) => filter_var($v, FILTER_VALIDATE_EMAIL) !== false,
    'phone' => fn($v) => preg_match('/^09\d{8}$/', $v),
    'age' => fn($v) => is_numeric($v) && $v >= 0 && $v <= 150,
];

function validate(array $data, array $validators): array {
    $errors = [];
    
    foreach ($validators as $field => $validator) {
        if (isset($data[$field]) && !$validator($data[$field])) {
            $errors[] = "$field 格式不正確";
        }
    }
    
    return $errors;
}

$data = ['email' => 'invalid', 'phone' => '0912345678', 'age' => 25];
$errors = validate($data, $validators);
// ['email 格式不正確']
?>

柯里化 (Currying)

<?php
// 柯里化函數
$curry = fn($a) => fn($b) => fn($c) => $a + $b + $c;

echo $curry(1)(2)(3);  // 6

// 部分應用
$add1 = $curry(1);
$add1and2 = $add1(2);
echo $add1and2(3);  // 6
?>

組合函數

<?php
$compose = fn($f, $g) => fn($x) => $f($g($x));

$addOne = fn($n) => $n + 1;
$double = fn($n) => $n * 2;

$addOneThenDouble = $compose($double, $addOne);

echo $addOneThenDouble(5);  // 12 ((5 + 1) * 2)

$doubleThenAddOne = $compose($addOne, $double);

echo $doubleThenAddOne(5);  // 11 ((5 * 2) + 1)
?>