泛型與繼承


泛型和繼承是現代編程語言中兩種比較重要的特性,對提高語言的表達能力,增強軟件的質量、健壯性、可維護性有重要作用。前者常見於函數式編程語言,如Haskell;后者則是面向對象(OO)語言的基礎。泛型對類型的描述更細化,表達能力更強,然而,泛型是編譯期的信息,無法提供像繼承中的動態綁定功能,這也許是過去二十年中OO語言得到廣泛使用的原因。

之所以說泛型不能實現動態的效果,主要原因在於:

1) 泛型信息僅僅在編譯期起作用
2) 泛型不支持類型的向下轉換(Down Casting),即子類對象轉換成作為父類型使用

這使得泛型在實現類似虛函數的多態時,無法實現或者極為麻煩。考慮如下的情況

class Animal:
    method eat ...

class Cat inherite Animal:
    method eat ...

class Dog inherite Animal:
    method eat ...

這在OO中是很常見、很基本的。然而如果用泛型實現,則很麻煩。比如用Pattern Matching

func eat animal:
    Cat cat = ...
    Dog dog = ...

調用時

eat(cat, ...)
eat(dog, ...)

似乎也可以。但是,如果catdog是由某個Factory根據配置文件動態產生的,也就是

cat = Factory.create_animal ...

現在create_animal的返回值類型如何寫?顯然,由於泛型中不能將CatDog都轉換成父類Animal來處理,就無法在運行時依據animal的實際類型,調用對應的函數;而必須在編譯期確定所有的類型和應當調用的函數。

無法進行動態綁定,也就無法進行軟件的動態擴展。比如,在Java中,上述代碼已經打包成Jar包,現在要增加一個類型Pig,我們只需讓Pig繼承Animal,將新代碼打成Jar包即可使用,無需對原有的代碼進行改動和編譯。而對於泛型的版本而言,不但無法實現動態擴展,而且還要修改原始的eat函數,加入Pig對應的Pattern代碼。

這一特性使得OO語言能夠很好的支持“開閉”原則,即代碼對擴展開放,對修改封閉。通常人們認為,OO的優勢在於對現實世界中“對象”的模擬,但是我更認同松本行弘的觀點,即:如同結構化編程一樣,OO是一種代碼組織的方式,使得軟件開發中的復雜度能夠得到更好的控制,至於它是否模擬了世界,並不重要。

在我開來,OO的作用是把接口和實現分離,並且將實現的函數體拆分到了多處。在上述例子中,增加了Pig類型,實際上是在eat函數中增加了功能,能夠處理Pig類型的變量。從另一個角度說,多態的eat函數等價於

eat animal, ...:

    if animal instance of Cat:
        ...
    elif animal instance of Dog:
        ...

加入了Pig類型,等於增加了一個if分支:

    elif animal instance of Pig:
        ...

而這種增加,既沒有改變接口,也不需要修改原來的代碼,而是通過類的繼承。所以,通過類型繼承實現的多態,在不改變接口的前提下,把實現函數的函數體根據具體類型拆分到了多處,並且可以增加新的部分,而不影響原有部分。這顯然就是對“開閉”原則的實踐。

“開閉”原則對提高軟件的可擴展性,控制復雜性有重要作用,在大型軟件開發中尤為明顯。過去20年間,C++、Java等OO語言獲得了廣泛使用。而Haskell、Lisp、Erlang等語言盡管更清晰、簡潔,有更好的數學基礎,或者並發的效率更高,但是卻沒有獲得廣泛的普及。僅僅把原因歸咎於曲高和寡是不夠的。在作者看來,這些函數式語言因為無法提供類型繼承和動態綁定的功能,導致不能很好的支持開閉原則,在大型軟件開發中不能很好的控制復雜度,是它們沒有獲得廣泛應用的主要原因,尤其是在應對需求復雜多變的場合方面。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM