PHP 匿名函數與閉包 (Anonymous Function & Closure)

匿名函數(又稱 Lambda 函數)是沒有名稱的函數,可以儲存在變數中或作為參數傳遞。閉包 (Closure) 是可以存取其定義範圍外變數的匿名函數。

基本語法

<?php
// 匿名函數指派給變數
$greet = function($name) {
    return "Hello, $name!";
};

// 呼叫匿名函數
echo $greet("Alice");  // Hello, Alice!
?>

作為回呼函數

匿名函數最常見的用途是作為回呼函數 (callback):

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

// array_map 使用匿名函數
$doubled = array_map(function($n) {
    return $n * 2;
}, $numbers);

print_r($doubled);  // [2, 4, 6, 8, 10]

// array_filter 使用匿名函數
$evens = array_filter($numbers, function($n) {
    return $n % 2 === 0;
});

print_r($evens);  // [2, 4]

// usort 使用匿名函數
$names = ['Charlie', 'Alice', 'Bob'];
usort($names, function($a, $b) {
    return strcmp($a, $b);
});

print_r($names);  // ['Alice', 'Bob', 'Charlie']
?>

閉包與 use 關鍵字

使用 use 關鍵字可以讓匿名函數存取外部變數:

<?php
$message = "Hello";

// 使用 use 捕獲外部變數
$greet = function($name) use ($message) {
    return "$message, $name!";
};

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

傳值捕獲(預設)

變數的值會被複製到閉包中:

<?php
$count = 0;

$increment = function() use ($count) {
    $count++;
    echo "閉包內:$count\n";
};

$increment();  // 閉包內:1
$increment();  // 閉包內:1(每次都是從 0 開始)

echo "閉包外:$count";  // 閉包外:0(原變數不受影響)
?>

傳址捕獲

使用 & 可以傳址捕獲,閉包內的修改會影響原變數:

<?php
$count = 0;

$increment = function() use (&$count) {
    $count++;
    echo "閉包內:$count\n";
};

$increment();  // 閉包內:1
$increment();  // 閉包內:2
$increment();  // 閉包內:3

echo "閉包外:$count";  // 閉包外:3(原變數被修改)
?>

捕獲多個變數

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

$greet = function($name) use ($greeting, $punctuation) {
    return "$greeting, $name$punctuation";
};

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

閉包作為回傳值

函數可以回傳閉包,實現工廠模式:

<?php
// 建立乘法器
function createMultiplier($factor) {
    return function($number) use ($factor) {
        return $number * $factor;
    };
}

$double = createMultiplier(2);
$triple = createMultiplier(3);

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

函數產生器

<?php
// 建立計數器
function createCounter($start = 0) {
    $count = $start;

    return function() use (&$count) {
        return $count++;
    };
}

$counter = createCounter(1);

echo $counter();  // 1
echo $counter();  // 2
echo $counter();  // 3
?>

Closure 類別

所有匿名函數都是 Closure 類別的實例:

<?php
$fn = function() {};

var_dump($fn instanceof Closure);  // bool(true)
echo get_class($fn);  // Closure
?>

Closure::bind() 和 Closure::bindTo()

可以動態改變閉包的 $this 和作用域:

<?php
class User {
    private string $name = "Alice";
}

$getName = function() {
    return $this->name;
};

$user = new User();

// 綁定閉包到物件
$boundGetName = Closure::bind($getName, $user, User::class);
echo $boundGetName();  // Alice

// 或使用 bindTo
$boundGetName2 = $getName->bindTo($user, User::class);
echo $boundGetName2();  // Alice
?>

Closure::call() (PHP 7+)

臨時綁定並執行閉包:

<?php
class User {
    private string $name = "Alice";
}

$getName = function() {
    return $this->name;
};

$user = new User();
echo $getName->call($user);  // Alice
?>

型別宣告

callable 型別

<?php
function apply(callable $func, $value) {
    return $func($value);
}

echo apply(function($n) { return $n * 2; }, 5);  // 10
echo apply('strtoupper', 'hello');  // HELLO
?>

Closure 型別

如果只接受匿名函數,可以使用 Closure 型別:

<?php
function process(Closure $callback) {
    return $callback();
}

process(function() { echo "執行中"; });
// process('strlen');  // TypeError:只接受 Closure
?>

立即執行函數 (IIFE)

匿名函數可以定義後立即執行:

<?php
$result = (function() {
    $a = 10;
    $b = 20;
    return $a + $b;
})();

echo $result;  // 30

// 帶參數的 IIFE
$result = (function($x, $y) {
    return $x * $y;
})(5, 3);

echo $result;  // 15
?>

實際應用範例

資料轉換

<?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'],
];

// 取出所有名稱
$names = array_map(function($user) {
    return $user['name'];
}, $users);

// 過濾特定使用者
$filtered = array_filter($users, function($user) {
    return $user['id'] > 1;
});

// 建立 id => name 的對應
$map = array_reduce($users, function($acc, $user) {
    $acc[$user['id']] = $user['name'];
    return $acc;
}, []);
?>

事件處理器

<?php
class EventEmitter {
    private array $listeners = [];

    public function on(string $event, callable $callback): void {
        $this->listeners[$event][] = $callback;
    }

    public function emit(string $event, ...$args): void {
        if (isset($this->listeners[$event])) {
            foreach ($this->listeners[$event] as $callback) {
                $callback(...$args);
            }
        }
    }
}

$emitter = new EventEmitter();

$emitter->on('user.created', function($user) {
    echo "發送歡迎郵件給 {$user['name']}\n";
});

$emitter->on('user.created', function($user) {
    echo "記錄日誌:使用者 {$user['name']} 已建立\n";
});

$emitter->emit('user.created', ['name' => 'Alice']);
?>

中介軟體模式

<?php
class Pipeline {
    private array $middlewares = [];

    public function pipe(callable $middleware): self {
        $this->middlewares[] = $middleware;
        return $this;
    }

    public function process($input) {
        return array_reduce(
            $this->middlewares,
            function($carry, $middleware) {
                return $middleware($carry);
            },
            $input
        );
    }
}

$pipeline = new Pipeline();

$result = $pipeline
    ->pipe(function($value) {
        return $value * 2;
    })
    ->pipe(function($value) {
        return $value + 10;
    })
    ->pipe(function($value) {
        return "結果:$value";
    })
    ->process(5);

echo $result;  // 結果:20
?>

延遲執行

<?php
function lazy(callable $factory): callable {
    $value = null;
    $evaluated = false;

    return function() use ($factory, &$value, &$evaluated) {
        if (!$evaluated) {
            $value = $factory();
            $evaluated = true;
        }
        return $value;
    };
}

$expensive = lazy(function() {
    echo "計算中...\n";
    return array_sum(range(1, 1000000));
});

// 還沒執行
echo "準備取值\n";

// 第一次呼叫時才執行
echo $expensive();  // 計算中... 500000500000

// 後續呼叫使用快取的值
echo $expensive();  // 500000500000(不會再顯示「計算中」)
?>

記憶化 (Memoization)

<?php
function memoize(callable $fn): callable {
    $cache = [];

    return function(...$args) use ($fn, &$cache) {
        $key = serialize($args);

        if (!isset($cache[$key])) {
            $cache[$key] = $fn(...$args);
        }

        return $cache[$key];
    };
}

$fibonacci = memoize(function($n) use (&$fibonacci) {
    if ($n <= 1) {
        return $n;
    }
    return $fibonacci($n - 1) + $fibonacci($n - 2);
});

echo $fibonacci(40);  // 快速計算
?>

匿名函數 vs 箭頭函數

PHP 7.4+ 引入了箭頭函數,是匿名函數的簡化寫法:

<?php
// 匿名函數
$double = function($n) {
    return $n * 2;
};

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

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

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

更多關於箭頭函數的說明,請參考 箭頭函數