PHP繼承竟然也需要顯性基因?

雖然寫php已經是將近8年的功底了,但因為工作關系,經常需要涉及前后端的各種代碼,容易精分,也總會記岔。最近發生的一件事情讓我覺得,或許寫下來能夠讓自己清醒一點。

網上經常流傳出php是語言鄙視鏈最低端的那個,曾經大學學java,畢設用java,剛出來培訓用java的我,在最初工作的2、3年時對php的面向對象也是頗有意見,總覺得【不倫不類】,更別提對js的看法了。但是這些觀點都在經歷越來越多的項目之后逐漸的淡化,甚至改觀。這里面包含著自己對項目、技術有著更多的理解,同時,在這些年里,Web環境、技術也在不停的更新。不過今天不是來聊這些東西的,對于以上的問題,我的觀點可以總結為:技術是工具、手段,不合適就升級、換,就這么簡單。

話歸原題。雖然寫php已經是將近8年的功底了,但因為工作關系,經常需要涉及前后端的各種代碼,容易精分,也總會記岔。最近發生的一件事情讓我覺得,或許寫下來能夠讓自己清醒一點。

在某一年寫某個模塊時用到了static成員,在實現子類的過程中發現他們也共享著父類這個成員的值,具體來說就是我在某個子類A中改變了那個成員值,在另外一個子類B使用的時候結果意外的得到了A覆蓋后的值。當時以為,原來static成員是在從聲明的地方開始的整個類別樹中共享的。后來一直隱約記得這個結論,在平常的代碼里面更謹慎的使用static成員,除非確認寫的類是個獨立的工具類,不然不輕易使用static。

直到有一天我的老大跟我商量升級我之前寫的一個BaseModel,他無意中問我:好像你不喜歡用static成員?我說沒有啊,因為考慮到BaseModel會被經常繼承成各種Model,如果我在這里用了static的話,將來容易踩坑。他表示不理解,然后過來與我辯論。我很義正言辭的說明了因為static成員會被共享,如果要調用兩個不同的子類的時候,那個static成員的變量的值就會像一個全局變量一樣不可控。他不同意。于是本著科學的精神,我們寫下了一個簡短的代碼來驗證:


 
  1. classA{
  2. protectedstatic$var1=null;
  3. publicstaticfunctiontest(){
  4. echoget_called_class().''.static::$var1.'<br/>';
  5. }
  6. }
  7. classBextendsA{
  8. protectedstatic$var1='b';
  9. }
  10. classCextendsA{
  11. protectedstatic$var1='c';
  12. }
  13. B::test();
  14. C::test();

很顯然,這次是我敗了。我期待的結果是c c,不過其實是b c。那么這樣看起來其實子類的static成員是只在子類這一層共享的。但是我總覺得不對勁,明明在寫BaseModel的時候我已經又栽過跟頭了,為什么這個驗證出來并不支持我那個時候遇到的問題呢?于是我發現我記岔了。年輕多好。后來想起來,原來我這里不用static的原因僅僅是因為設計需要。

我以為我錯了。直到前幾天又寫了幾個父子類(不是BaseModel了),大膽的用上了static成員,結果是轟轟烈烈的在自測中又摔了一跤。怎么回事!然后我仔細留意了一下自己這次的用法,將上面的例子改了一下運行:


 
  1. classA{
  2. protectedstatic$var1=null;
  3. protectedstatic$var2=null;
  4. publicstaticfunctiontest(){
  5. if(!static::$var2){
  6. static::$var2=static::$var1;
  7. }
  8. echoget_called_class().''.static::$var2.'<br/>';
  9. }
  10. }
  11. classBextendsA{
  12. protectedstatic$var1='b';
  13. }
  14. classCextendsA{
  15. protectedstatic$var1='c';
  16. }
  17. B::test();
  18. C::test();

結果是


 
  1. Bb
  2. Cb

如果說上次的結論是對了,那么這次又怎么解釋?這里明明就是表示$var2是A,B,C共享的。$var1和$var2的差別這樣看起來僅僅是有聲明和沒聲明的區別。于是我又改成這樣:


 
  1. classA{
  2. protectedstatic$var1=null;
  3. protectedstatic$var2=null;
  4. publicstaticfunctiontest(){
  5. if(!static::$var2){
  6. static::$var2=static::$var1;
  7. }
  8. echoget_called_class().''.static::$var2.'<br/>';
  9. }
  10. }
  11. classBextendsA{
  12. protectedstatic$var1='b';
  13. protectedstatic$var2=null;
  14. }
  15. classCextendsA{
  16. protectedstatic$var1='c';
  17. protectedstatic$var2=null;
  18. }
  19. B::test();
  20. C::test();

結果是


 
  1. Bb
  2. Cc

我當時內心是崩潰的。于是我上了Stack Overflow,發現栽坑的不止我一個。

只有顯式的聲明出來的static成員才會被視為是只從屬于子類的。

只有顯式的聲明出來的static成員才會被視為是只從屬于子類的。

只有顯式的聲明出來的static成員才會被視為是只從屬于子類的。

重要的事情說三遍!不過如果子類很多的話,動態決定值的成員每個都這樣去聲明,就從寫代碼這件事上失去了用static的意義。一個更好的方法是,把$var2變成一個數組,每個類要用的值放在$var[__CLASS__]里面使用。

不過不管怎么說,如非必要,還是盡量不用static成員繼承吧。

還有一個有點類似的“坑”。我們說到private成員的時候,都知道private是指私有的,不會被子類繼承。但是有時候寫代碼的時候會忘記,直到載跟頭了才想起來原來是private導致子類找不到該有的成員,或者說是private都在子類聲明了,但是因為調用函數時是調用父類函數,結果得到的是父類這個private的值而不是子類的。遇到這種情況不可能又將函數原樣的重寫在子類里。所以使用private要特別小心。

曾經在使用Rackspace的SDK的時候就看到有些類里面使用了private成員,但是由于他們給出了不必要的打開文件權限,導致代碼在我們的服務器上運行不了。那么這個時候本想寫個子類覆蓋一下這個成員的初始值就好了,結果就因為這是個private成員,而最后需要把所有引用到的地方都拷到自己寫的子類里面。為什么我們不直接改SDK,讓成員變成protected?因為開發包也許下次就升級了呢?修正之后我們把子類移除就好了。如果修改庫代碼成了習慣,想升級的時候就沒這么歡了。所以說,private成員的使用一定要慎之又慎,如果你也在開發SDK,就更需要考慮使用者是不是需要繼承?如果你必須寫private,你是不是能夠保證代碼能夠適應各種場景的使用?