閉包函數和 lambda 函數絕對不是新出現的概念;它們均來自函數編程領域。函數編程 是一種編程風格,它將關注點從執行命令轉移到表達式計算。這些表達式是使用函數構成的,結合這些函數能夠獲得咱們要查找的結果。這種編程風格最經常使用於學術目的,可是也能夠在人工智能與數學領域中見到,而且能夠在用 Erlang、Haskell 及 Scheme 等語言編寫的商業應用程序中找到。 php
閉包 最初是在 20 世紀 60 年代做爲 Scheme 的一部分開發的,Scheme 是最著名的函數編程語言之一。Lambda 函數和閉包一般出如今容許將函數處理爲第一類值(First-class value)的語言中,這意味着函數能夠動態建立並做爲參數傳遞給其餘語言。 mysql
從那時起,閉包及 lambda 函數已經找到了走出函數編程世界並進入 JavaScript、Python 和 Ruby 等語言的方法。JavaScript 是支持閉包和 lambda 函數的最多見語言之一。JavaScript 實際使用這些函數做爲支持面向對象的編程方法,把函數嵌套到其餘函數中以用做私有成員。清單 1 提供了 JavaScript 如何使用閉包的示例。 sql
var Example = function() { this.public = function() { return "This is a public method"; }; var private = function() { return "This is a private method"; }; }; Example.public() // returns "This is a public method" Example.private() // error - doesn't work |
如清單 1 中所示,Example 對象的成員函數被定義爲閉包。因爲私有方法做用於局部變量(與綁定到使用此關鍵字的 Example 對象的公共方法相反),所以從外部看不到它。 編程
如今咱們已經瞭解了這些概念的歷史,讓咱們查看 PHP 中的 lambda 函數。lambda 函數的概念是閉包的基礎,而且提供了一種比 PHP 中已有的 create_function() 函數改進了不少的動態建立函數的方法。 數組
Lambda 函數 緩存
Lambda 函數(或者一般所謂的 「匿名函數」)是能夠隨時定義的簡單拋棄型函數,而且一般都與變量綁定。函數自己僅存在於定義函數的變量範圍內,所以當該變量超出範圍時,函數也超出範圍。lambda 函數的理念源於 20 世紀 30 年代的數學研究。它被稱爲 lambda 演算,用於研究函數定義與應用程序以及遞歸概念。lambda 演算用於開發函數編程語言,例如 Lisp 和 Scheme。 閉包
對於大多數實例來講,尤爲對於接受回調函數的許多 PHP 函數來講,Lambda 函數很是方便。array_map() 就是這樣一種函數,它容許咱們遍歷數組並將回調函數應用到數組的每一個元素上。在早期版本的 PHP 中,這些函數的最大問題是沒有一種清晰的方式定義回調函數;咱們堅持使用如下三種解決方法的其中一種: app
function quoteWords() { if (!function_exists ('quoteWordsHelper')) { function quoteWordsHelper($string) { return preg_replace('/(\w)/','"$1"',$string); } } return array_map('quoteWordsHelper', $text); } |
雖然接受回調函數的函數功能十分強大,可是沒有一種好方法能夠執行一次性回調函數,而無需執行一些很是笨拙的工做。使用 PHP V5.3,咱們可使用 lambda 函數以更規則的方法從新執行以上示例。 編程語言
function quoteWords() { return array_map('quoteWordsHelper', function ($string) { return preg_replace('/(\w)/','"$1"',$string); }); } |
咱們看到了定義這些函數的更規則的語法,這能夠經過操做碼緩存來優化性能。咱們也已經獲得了改進的可讀性以及與字符串高亮顯示功能的兼容性。讓咱們在此基礎上了解如何在 PHP 中使用閉包。 函數
閉包
Lambda 函數自己並無添加之前不能執行的功能。正如咱們所見,咱們可使用 create_function() 執行全部這項操做,儘管後果是使用更糟糕的語法而且性能更加不理想。可是它們仍然是拋棄型函數而且不維護任何類型的狀態,這限制了咱們能夠用它們執行的操做。所以出現了閉包並使 lambda 函數獲得加強。
閉包是在它本身的環境中執行計算的函數,它有一個或多個綁定變量能夠在調用函數時訪問。它們來自函數編程世界,其中涉及大量概念。閉包相似於 lambda 函數,可是在與定義閉包的外部環境中的變量進行交互方面更加智能。
讓咱們看一看如何在 PHP 中定義閉包。清單 4 顯示了從外部環境導入變量並將其簡單地輸出到屏幕上的閉包示例。
$string = "Hello World!"; $closure = function() use ($string) { echo $string; }; $closure(); Output: Hello World! |
從外部環境中導入的變量是在閉包函數定義的 use 子句中指定的。默認狀況下,它們是由值傳遞的,意味着若是要更新傳遞到閉包函數定義內的值,則不更新外部值。可是,咱們能夠經過在變量前放置 & 運算符來完成此操做,這種方法在函數定義中用於表示按引用傳遞。清單 5 顯示了這種方法的示例。
$x = 1 $closure = function() use (&$x) { ++$x; } echo $x . "\n"; $closure(); echo $x . "\n"; $closure(); echo $x . "\n"; Output: 1 2 3 |
能夠看到,閉包使用外部變量 $x 並在每次調用閉包時遞增該變量。咱們能夠將按值和按引用傳遞的變量輕鬆地混合到 use 子句中,而且能夠順利地處理這些變量。咱們也能夠擁有直接返回閉包的函數,如清單 6 所示。在本例中,閉包的生命週期實際上比定義閉包的方法長。
function getAppender($baseString) { return function($appendString) use ($baseString) { return $baseString . $appendString; }; } |
閉包與對象
閉包不可是過程式編程的有用工具,並且是面向對象編程的有用工具。在這種狀況下使用閉包與在類外部使用閉包實現的目的相同:包含在小範圍內綁定的特定函數。在對象外部與在對象內部使用同樣簡單。
在對象內定義時,很是方便的一點是閉包經過 $this 變量擁有對對象的徹底訪問權,而無需顯式導入。清單 7 演示了該示例。
class Dog { private $_name; protected $_color; public function __construct($name, $color) { $this->_name = $name; $this->_color = $color; } public function greet($greeting) { return function() use ($greeting) { echo "$greeting, I am a {$this->_color} dog named {$this->_name}."; }; } } $dog = new Dog("Rover","red"); $dog->greet("Hello"); Output: Hello, I am a red dog named Rover. |
在這裏,咱們在閉包內顯式使用提供給 greet() 方法的歡迎詞,閉包在該方法內定義。咱們還在閉包內獲取狗的顏色和名字,傳遞到構造函數中並存儲到對象中。
在類中定義的閉包基本上與在對象外部定義的閉包相同。唯一的不一樣之處在於經過 $this 變量自動導入對象。咱們能夠經過將閉包定義爲靜態閉包禁用此行爲。
class House { public function paint($color) { return static function() use ($color) { echo "Painting the house $color...."; }; } } $house = new House(); $house->paint('red'); Output: Painting the house red.... |
此示例相似於清單 5 中定義的 Dog 類。最大的差異是在閉包內不使用對象的任何屬性,由於它被定義爲靜態類。
在對象內使用靜態閉包與使用非靜態閉包相比的最大優勢是節省內存。因爲無需將對象導入閉包中,所以能夠節省大量內存,尤爲是在擁有許多不須要此功能的閉包時。
針對對象的另外一個優勢是添加名爲 __invoke() 的魔術方法,此方法容許對象自己被調用爲閉包。若是定義了此方法,則在該上下文中調用對象時將使用此方法。清單 9 演示了示例。
class Dog { public function __invoke() { echo "I am a dog!"; } } $dog = new Dog(); $dog(); |
將清單 9 中所示的對象引用調用爲變量將自動調用 __invoke() 魔術方法,使類自己用做閉包。
閉包能夠很好地與面向對象的代碼以及面向過程的代碼整合。讓咱們看一看閉包如何與 PHP 的強大 Reflection API 交互。
閉包與反射
PHP 有一個有用的反射 API,它容許咱們對類、接口、函數和方法執行反向工程。按照設計,閉包是匿名函數,這意味着它們不顯示在反射 API 中。
可是,新 getClosure() 方法已經添加到 PHP 中的 ReflectionMethod 和 ReflectionFunction 類中,能夠從指定的函數或方法動態建立閉包。它在此上下文中用做宏,經過閉包調用函數方法將在定義它的上下文中執行函數調用。清單 10 顯示了此方法如何運行。
class Counter { private $x; public function __construct() { $this->x = 0; } public function increment() { $this->x++; } public function currentValue() { echo $this->x . "\n"; } } $class = new ReflectionClass('Counter'); $method = $class->getMethod('currentValue'); $closure = $method->getClosure() $closure(); $class->increment(); $closure(); Output: 0 1 |
這種方法的一個有趣的反作用是容許經過閉包訪問類的私有成員和受保護成員,這有利於對類執行單元測試。清單 11 展現了對類的私有方法的訪問。
class Example { .... private static function secret() { echo "I'm an method that's hiding!"; } ... } $class = new ReflectionClass('Example'); $method = $class->getMethod('secret'); $closure = $method->getClosure() $closure(); Output: I'm an method that's hiding! |
此外,可使用反射 API 來內省(introspect)閉包自己,如清單 12 所示。只需將對閉包的變量引用傳遞到 ReflectionMethod 類的構造函數中。
$closure = function ($x, $y = 1) {}; $m = new ReflectionMethod($closure); Reflection::export ($m); Output: Method [ <internal> public method __invoke ] { - Parameters [2] { Parameter #0 [ <required> $x ] Parameter #1 [ <optional> $y ] } } |
關於向後兼容性值得注意的一點是,PHP 引擎如今保留類名 Closure 並用於存儲閉包,所以使用該名稱的全部類都須要重命名。
正如咱們所見,反射 API 可以經過現有函數和方法動態建立閉包,從而爲閉包提供強大的支持。它們還能夠像普通函數同樣內省到閉包中。
爲何使用閉包?
如在 lambda 函數的示例中所示,閉包的最明顯用法之一是少數 PHP 函數接受回調函數做爲參數。可是,在須要把邏輯封裝到本身的範圍內的狀況下,閉包會十分有用。重構舊代碼以進行簡化並提升可讀性就是這樣一個例子。查看如下示例,該示例顯示了在運行一些 SQL 查詢時使用的記錄程序。
$db = mysqli_connect("server","user","pass"); Logger::log('debug','database','Connected to database'); $db->query('insert into parts (part, description) values ('Hammer','Pounds nails'); Logger::log('debug','database','Insert Hammer into to parts table'); $db->query('insert into parts (part, description) values ('Drill','Puts holes in wood'); Logger::log('debug','database','Insert Drill into to parts table'); $db->query('insert into parts (part, description) values ('Saw','Cuts wood'); Logger::log('debug','database','Insert Saw into to parts table'); |
從清單 13 中能夠看出執行操做的重複程度。對 Logger::log() 執行的每次調用都有相同的前兩個實參。爲了解決此問題,咱們能夠把該方法調用放入閉包並轉而針對該閉包執行調用。獲得的代碼以下所示:
$logdb = function ($string) { Logger::log('debug','database',$string); }; $db = mysqli_connect("server","user","pass"); $logdb('Connected to database'); $db->query('insert into parts (part, description) values ('Hammer','Pounds nails'); $logdb('Insert Hammer into to parts table'); $db->query('insert into parts (part, description) values ('Drill','Puts holes in wood'); $logdb('Insert Drill into to parts table'); $db->query('insert into parts (part, description) values ('Saw','Cuts wood'); $logdb('Insert Saw into to parts table'); |
代碼不但在外觀上更加規則,並且更易於更改 SQL 查詢日誌的日誌級別,由於如今只須要在一個位置進行更改。
結束語
本文演示了閉包在 PHP V5.3 代碼中做爲函數編程構造時多麼有用。咱們討論了 lambda 函數及閉包與這些函數相比的優勢。對象與閉包能夠很好地結合使用,好比咱們在面向對象的代碼內對閉包的特殊處理。咱們看到了如何使用反射 API 建立動態閉包,以及如何內省現有的閉包。