
PHP和Java中都會(huì)有static這個(gè)關(guān)鍵字,用法也類似,當(dāng)問(wèn)及PHP中的static用法是,很容易想出static可以聲明類屬性或方法為靜態(tài),靜態(tài)屬性和方法都是屬于類的,靜態(tài)屬性不能通過(guò)對(duì)象訪問(wèn),但靜態(tài)方法可以通過(guò)對(duì)象訪問(wèn)。沒錯(cuò),是這樣的,但是在PHP中static還有另外的用處哦。
先從static變量的作用域開始
PHP中static變量只存在于本地函數(shù)中,但是當(dāng)程序執(zhí)行完本函數(shù)后,static變量還會(huì)一直存在,考慮如下代碼:
<?php
function test()
{
$a = 0;
echo $a . "\n";
$a++;
}
// 都會(huì)輸出0
for ($i=0; $i<5; $i++) {
test();
}
在每次調(diào)用這個(gè)函數(shù)的時(shí)候,函數(shù)都會(huì)將$a變量置0,再輸出,盡管每次輸出后,變量$a都加1了,為了每次都能將$a的值保存起來(lái),我們可以將它聲明為static:
<?php
function test()
{
static $a = 0;
echo $a . "\n";
$a++;
}
// 輸出
// 0
// 1
// 2
// 3
// 4
for ($i=0; $i<5; $i++) {
test();
}
現(xiàn)在,$a只被初始化了一次,每次調(diào)用test()函數(shù)時(shí),$a都會(huì)加1。
在遞歸函數(shù)中,同樣可以使用靜態(tài)變量,我們可以設(shè)置一個(gè)$count靜態(tài)變量的函數(shù)運(yùn)行計(jì)數(shù)器,保存運(yùn)行的次數(shù),當(dāng)$count到10時(shí),就退出遞歸函數(shù),如下:
<?php
function test()
{
static $count = 0;
$count++;
echo $count;
if ($count < 10) {
test();
}
}
test();
靜態(tài)變量初始化時(shí)只能是確定的一個(gè)值,不能是函數(shù)的返回值,下面的代碼中,將sqrt()函數(shù)的結(jié)果賦值給靜態(tài)變量$a會(huì)報(bào)錯(cuò):
function foo(){
static $a = 0; // correct
static $a = 1+2; // correct (as of PHP 5.6)
static $a = sqrt(121); // wrong (as it is a function)
echo $a;
}
foo();
程序沒有運(yùn)行前,再phpstorm中就已經(jīng)其實(shí)不能用表達(dá)式初始化靜態(tài)變量:
[圖片上傳失敗...(image-d1d019-1603612895553)]
程序運(yùn)行時(shí)也會(huì)報(bào)如下錯(cuò)誤:
[圖片上傳失敗...(image-7bb327-1603612895553)]
聲明類屬性或方法為靜態(tài)
聲明類屬性或方法為靜態(tài),就可以不實(shí)例化類而直接訪問(wèn)。靜態(tài)屬性不能通過(guò)一個(gè)類已實(shí)例化的對(duì)象來(lái)訪問(wèn)(但靜態(tài)方法可以)。為了兼容 PHP 4,如果沒有指定訪問(wèn)控制,屬性和方法默認(rèn)為公有。由于靜態(tài)方法不需要通過(guò)對(duì)象即可調(diào)用,所以偽變量 $this 在靜態(tài)方法中不可用。靜態(tài)屬性不可以由對(duì)象通過(guò) -> 操作符來(lái)訪問(wèn)。用靜態(tài)方式調(diào)用一個(gè)非靜態(tài)方法會(huì)導(dǎo)致一個(gè) E_STRICT 級(jí)別的錯(cuò)誤。就像其它所有的 PHP 靜態(tài)變量一樣,靜態(tài)屬性只能被初始化為文字或常量,不能使用表達(dá)式。所以可以把靜態(tài)屬性初始化為整數(shù)或數(shù)組,但不能初始化為另一個(gè)變量或函數(shù)返回值,也不能指向一個(gè)對(duì)象。自 PHP 5.3.0 起,可以用一個(gè)變量來(lái)動(dòng)態(tài)調(diào)用類。但該變量的值不能為關(guān)鍵字 self,parent 或 static。如下是靜態(tài)屬性的示例:
<?php
class Foo
{
public static $my_static = 'foo';
public function staticValue() {
return self::$my_static;
}
}
class Bar extends Foo
{
public function fooStatic() {
return parent::$my_static;
}
}
print Foo::$my_static . "\n";
$foo = new Foo();
print $foo->staticValue() . "\n";
print $foo->my_static . "\n"; // 對(duì)象不能使用->符號(hào)調(diào)用靜態(tài)變量
print $foo::$my_static . "\n";
$classname = 'Foo';
print $classname::$my_static . "\n"; // 5.3開始可以使用變量調(diào)用類
print Bar::$my_static . "\n";
$bar = new Bar();
print $bar->fooStatic() . "\n";
運(yùn)行結(jié)果如下:
[圖片上傳失敗...(image-b6ec82-1603612895553)]
作為靜態(tài)變量,還可以在多個(gè)對(duì)象之間共享數(shù)據(jù),創(chuàng)建好幾個(gè)對(duì)象的時(shí)候,因?yàn)槊看味际莕ew的,所以創(chuàng)建的對(duì)象都不同,如果想讓多個(gè)對(duì)象實(shí)例共享同一個(gè)變量,就可以用到靜態(tài)變量。假設(shè)要編寫一個(gè)類來(lái)跟蹤網(wǎng)頁(yè)瀏覽的人數(shù),肯定不希望每次實(shí)例化該類時(shí)都把訪問(wèn)者數(shù)量置0,只是就可以將該屬性設(shè)置為static:
<?php
class Visitor
{
private static $visitors = 0;
public function __construct()
{
self::$visitors++;
}
public static function getVisitors()
{
return self::$visitors;
}
}
// 實(shí)例化
$visitor1 = new Visitor();
echo Visitor::getVisitors() . "\n"; // 1
$visitor2 = new Visitor();
echo Visitor::getVisitors() . "\n"; // 2
延遲靜態(tài)綁定
PHP中的static關(guān)鍵字除了上述比較熟知的作用外,還可以作為延遲靜態(tài)綁定使用,這是在5.3版本后才加入的功能。
先看如下的代碼:
<?php
abstract class DomainObject
{
}
class User extends DomainObject
{
public static function create()
{
return new User();
}
}
class Document extends DomainObject
{
public static function create()
{
return new Document();
}
}
首先創(chuàng)建了一個(gè)抽象類,然后創(chuàng)建了兩個(gè)子類User和Document分別繼承自DomainObject抽象類,這個(gè)代碼運(yùn)行起來(lái)完全沒問(wèn)題,而且能很好的工作,如果你是一位懶惰的程序猿,看到這樣重復(fù)的代碼你會(huì)很惱火,尤其是重復(fù)代碼比較多的時(shí)候,就會(huì)想著如何重構(gòu)它。每個(gè)DomainObject子類都有一個(gè)相同的create()函數(shù),我們?cè)囍阉湃敫割惍?dāng)中去:
<?php
abstract class DomainObject
{
public static function create()
{
return new self();
}
}
class User extends DomainObject
{
}
class Document extends DomainObject
{
}
Document::create();
很明顯phpstorm會(huì)提示出錯(cuò),我們?cè)囍\(yùn)行會(huì)得到以下錯(cuò)誤:
[圖片上傳失敗...(image-17d2df-1603612895553)]
在上面的例子中,self對(duì)該類所起的作用與$this對(duì)對(duì)象所起的作用不完全相同,self被解析為定義create()的DomainObject,而不是解析為調(diào)用self的Document類。在PHP 5.3中延遲靜態(tài)綁定的概念,最明顯的標(biāo)志就是使用static關(guān)鍵字,它指向的是被調(diào)用的類而不是包含類。上面的代碼我們可以這么改:
<?php
abstract class DomainObject
{
public static function create()
{
return new static();
}
}
class User extends DomainObject
{
}
class Document extends DomainObject
{
}
print_r(Document::create());
輸出:
[圖片上傳失敗...(image-d947f4-1603612895553)]
static關(guān)鍵字不僅可以用于實(shí)例化,和self和parent一樣,static還可以作為靜態(tài)方法調(diào)用的標(biāo)識(shí)符,甚至是從非靜態(tài)上下文中調(diào)用。例如為DomainObject引入組的概念,默認(rèn)組為default,可以用static為繼承層次結(jié)構(gòu)的某些子類重寫組,代碼如下:
<?php
abstract class DomainObject
{
private $group;
public function __construct()
{
$this->group = static::getGroup();
}
public static function create()
{
return new static();
}
public static function getGroup()
{
return "default";
}
}
class User extends DomainObject
{
}
class Document extends DomainObject
{
public static function getGroup()
{
return "document";
}
}
class SpreadSheet extends Document
{
}
print_r(User::create());
print_r(SpreadSheet::create());
代碼中,DomainObject的構(gòu)造函數(shù)使用static調(diào)用靜態(tài)方法getGroup(),設(shè)置默認(rèn)組為default,在Document中重寫了getGroup()方法,重新設(shè)置了組,下面是輸出結(jié)果:
[圖片上傳失敗...(image-23c2a4-1603612895553)]
PHP靜態(tài)綁定的一個(gè)應(yīng)用
該例子來(lái)自簡(jiǎn)書:http://www.itdecent.cn/p/25a78620fa5c
需求
做的某項(xiàng)目有一個(gè)“轉(zhuǎn)賬”的功能,但是轉(zhuǎn)賬的類型有很多種,對(duì)應(yīng)每種轉(zhuǎn)賬需要的參數(shù)也不同,舉個(gè)例子一種轉(zhuǎn)賬是由系統(tǒng)轉(zhuǎn)賬給用戶,那么就只有接收方和金額兩個(gè)參數(shù),另一種轉(zhuǎn)賬是用戶之間的轉(zhuǎn)賬且支持留言,那么就有發(fā)送方接收方金額和留言四個(gè)參數(shù)。當(dāng)然最簡(jiǎn)單的思路就是采用四個(gè)參數(shù),對(duì)于第一種轉(zhuǎn)賬將不用的兩個(gè)參數(shù)留空,這種方法的問(wèn)題在于,考慮到未來(lái)可能增加的新的轉(zhuǎn)賬類型,可能會(huì)引入新的參數(shù),那么代碼很可能需要推倒重來(lái),有沒有更優(yōu)雅的解決方式呢?
一個(gè)例子
其實(shí)Laravel里就有實(shí)現(xiàn)類似需求的例子,那就是查詢構(gòu)造器(Query Builder),它的一個(gè)使用的例子如下:
$users = DB::table('users')
->select(DB::raw('count(*) as user_count, status'))
->where('status', '<>', 1)
->groupBy('status')
->get();
這個(gè)方法和我們的需求就很像了,對(duì)于查詢這一功能,傳入哪些參數(shù)是未知的,例如某次具體的查詢,可能需要調(diào)用groupBy也可能調(diào)用orderBy,也可能兩者需要同時(shí)調(diào)用或者都不調(diào)用。一個(gè)思路就是針對(duì)每一個(gè)參數(shù)都寫一個(gè)方法,需要時(shí)則調(diào)用,不需要時(shí)則不調(diào)用。
解決方案
整體的解決思路是寫兩個(gè)類,一個(gè)叫Transfer,一個(gè)叫Builder,每個(gè)參數(shù)對(duì)應(yīng)的方法寫在Builder里,由Transfer去調(diào)用Builder構(gòu)造我們需要的轉(zhuǎn)賬類型,完成相關(guān)操作。這樣當(dāng)需求更新時(shí)(要增加新的參數(shù)時(shí)),只要在Builder里添加相應(yīng)的方法即可,而不用改動(dòng)現(xiàn)有代碼。下面先貼一下對(duì)應(yīng)的代碼再做詳細(xì)解釋。
Transfer類代碼:
class Transfer
{
public function __call($method, $parameters)
{
$builder = new Builder();
return call_user_func_array([$builder, $method], $parameters);
}
public static function __callStatic($method, $parameters)
{
$instance = new static;
return call_user_func_array([$instance, $method], $parameters);
}
}
Builder類實(shí)際上只涉及到具體的功能實(shí)現(xiàn),就貼部分代碼意思意思看看就行:
class Builder
{
protected $from = 0; // 0 represents system
protected $to = 0;
protected $amount = 0;
protected $comments = '';
protected $related = [];
public function from($user)
{
if ($user instanceof User) {
$this->from = $user->getAuthIdentifier();
} elseif (is_int($user)) {
$this->from = $user;
} else {
throw new InvalidArgumentException(sprintf('%s excepts $user parameter to be \App\User or integer, %s given.', __METHOD__, gettype($user)));
}
return $this;
}
public function to($user){...}
public function amount(int $amount){...}
public function comments($comments){...}
public function related(int $type, int $id, $extra = null){...}
public function transfer(){...}
}
具體調(diào)用Transfer功能的代碼:
Transfer::from($sender)->to($receiver)->amount($amount)->comments($comments)->related($related_type, $related_id, $related_extra)->transfer();
下面我們來(lái)走一遍調(diào)用Transfer的流程來(lái)看看。首先調(diào)用了Transfer類中的靜態(tài)方法from,然而Transfer中并不存在這個(gè)靜態(tài)方法,則會(huì)自動(dòng)調(diào)用__callStatic()這個(gè)魔術(shù)方法。這個(gè)方法首先實(shí)例化了一個(gè)static對(duì)象。注意這里的static是一個(gè)類名,new出來(lái)的$instance是屬于static這個(gè)類的一個(gè)實(shí)例化對(duì)象,有點(diǎn)拗口然后返回時(shí)調(diào)用了call_user_func_array這個(gè)方法,這個(gè)方法具體可以參考php的手冊(cè),實(shí)際上它完成了類似$instance->method($parameters)這樣的操作,放到我們當(dāng)前的情境下實(shí)際執(zhí)行了$transfer_instance->from($user)這樣的操作。
然后發(fā)覺Transfer中并不存在這個(gè)動(dòng)態(tài)方法,于是又會(huì)自動(dòng)調(diào)用__call()這個(gè)魔術(shù)方法。這個(gè)方法首先創(chuàng)建了一個(gè)Builder類的實(shí)例,之后調(diào)用call_user_func_array這個(gè)方法,實(shí)際上相當(dāng)于執(zhí)行了$builder->from($user)方法,然后終于得Builder類里找到了這個(gè)from()方法,注意它的返回值是$this。
然后當(dāng)前這個(gè)Builder這個(gè)對(duì)象繼續(xù)調(diào)用to方法,發(fā)覺又不存在又去調(diào)用__call()這個(gè)魔術(shù)方法,之后的過(guò)程同上,反復(fù)調(diào)用Builder中的方法把所有需要的參數(shù)都處理過(guò)以后最后調(diào)用了transfer()方法最終完成轉(zhuǎn)賬操作。
參考: