本文始發於個人公眾號:TechFlow,原創不易,求個關注
今天是golang專題的第11篇文章,我們一起來聊聊golang當中多型的這個話題。
如果大家系統的學過C++、Java等語言以及物件導向的話,相信應該對多型不會陌生。
多型是物件導向範疇當中經常使用並且非常好用的一個功能,如果你之前沒有學過的話也沒有關係,我們用一個簡單的例子來說明一下。多型主要是用在強型別語言當中,像是Python這樣的弱型別語言,變數的型別可以隨意變化,也沒有任何限制,其實區別不是很大。
多型的含義
對於Java或者是C++而言,我們在使用變數的時候,變數的型別是明確的。但是如果我們希望它可以寬鬆一點,比如說我們用父類指標或引用去呼叫方法,但是在執行的時候,能夠根據子類的型別去執行子類當中的方法。也就是說實現我們用相同的呼叫方式調出不同結果或者是功能的情況,這種情況就叫做多型。
舉個非常經典的例子,比如說貓、狗和人都是哺乳動物。這三個類都有一個say方法,大家都知道貓、狗以及人類的say是不一樣的,貓可能是喵喵叫,狗是汪汪叫,人類則是說話。
class Mammal {
public void say() {
System.out.println("do nothing")
}
}
class Cat extends Mammal{
public void say() {
System.out.println("meow");
}
}
class Dog extends Mammal{
public void say() {
System.out.println("woof");
}
}
class Human extends Mammal{
public void say() {
System.out.println("speak");
}
}
這段程式碼大家應該都不難看懂,這三個類都是Mammal的子類,假設這個時候我們有一系列例項,它們都是Mammal的子類的例項,但是這三種型別都有,我們希望用一個迴圈來一起全都呼叫了。雖然我們接收變數的時候是用的Mammal的父類型別去接收的,但是我們呼叫的時候卻會獲得各個子類的執行結果。
比如這樣:
class Main {
public static void main(String[] args) {
List<Mammal> mammals = new ArrayList<>();
mammals.add(new Human());
mammals.add(new Dog());
mammals.add(new Cat());
for (Mammal mammal : mammals) {
mammal.say();
}
}
}
不知道大家有沒有get到精髓,我們建立了一個父類的List,將它各個子類的例項放入了其中。然後通過了一個迴圈用父類物件來接收,並且呼叫了say方法。我們希望雖然我們用的是父類的引用來呼叫的方法,但是它可以自動根據子類的型別呼叫對應不同子類當中的方法。
也就是說我們得到的結果應該是:
speak
woof
meow
這種功能就是多型,說白了我們可以在父類當中定義方法,在子類當中建立不同的實現。但是在呼叫的時候依然還是用父類的引用去呼叫,編譯器會自動替我們做好內部的對映和轉化。
抽象類與介面
這樣實現當然是可行的,但其實有一個小小的問題,就是Mammal類當中的say方法多餘了。因為我們使用的只會是它的子類,並不會用到Mammal這個父類。所以我們沒必要實現父類Mammal中的say方法,做一個標記,表示有這麼一個方法,子類實現的時候需要實現它就可以了。
這就是抽象類和抽象方法的來源,我們可以把Mammal做成一個抽象類,宣告say是一個抽象方法。抽象類是不能直接建立例項的,只能建立子類的例項,並且抽象方法也不用實現,只需要標記好引數和返回就行了。具體的實現都在子類當中進行。說白了抽象方法就是一個標記,告訴編譯器凡是繼承了這個類的子類必須要實現抽象方法,父類當中的方法不能呼叫。那抽象類就是含有抽象方法的類。
我們寫出Mammal變成抽象類之後的程式碼:
abstract class Mammal {
abstract void say();
}
很簡單,因為我們只需要定義方法的引數就可以了,不需要實現方法的功能,方法的功能在子類當中實現。由於我們標記了say這個方法是一個抽象方法,凡是繼承了Mammal的子類都必須要實現這個方法,否則一定會報錯。
抽象類其實是一個擦邊球,我們可以在抽象類中定義抽象的方法也就是隻宣告不實現,也可以在抽象類中實現具體的方法。在抽象類當中非抽象的方法子類的例項是可以直接呼叫的,和子類呼叫父類的普通方法一樣。但假如我們不需要父類實現方法,我們提出提取出來的父類中的所有方法都是抽象的呢?針對這一種情況,Java當中還有一個概念叫做介面,也就是interface,本質上來說interface就是抽象類,只不過是只有抽象方法的抽象類。
所以剛才的Mammal也可以寫成:
interface Mammal {
void say();
}
把Mammal變成了interface之後,子類的實現沒什麼太大的差別,只不過將extends關鍵字換成了implements。另外,子類只能繼承一個抽象類,但是可以實現多個介面。早先的Java版本當中,interface只能夠定義方法和常量,在Java8以後的版本當中,我們也可以在介面當中實現一些預設方法和靜態方法。
介面的好處是很明顯的,我們可以用介面的例項來呼叫所有實現了這個介面的類。也就是說介面和它的實現是一種要寬泛許多的繼承關係,大大增加了靈活性。
以上雖然全是Java的內容,但是講的其實是物件導向的內容,如果沒有學過Java的小夥伴可能看起來稍稍有一點點吃力,但總體來說問題不大,沒必要細扣當中的語法細節,get到核心精髓就可以了。
講這麼一大段的目的是為了釐清物件導向當中的一些概念,以及介面的使用方法和理念,後面才是本文的重頭戲,也就是Go語言當中介面的使用以及理念。
Golang中的介面
Golang當中也有介面,但是它的理念和使用方法和Java稍稍有所不同,它們的使用場景以及實現的目的是類似的,本質上都是為了抽象。通過介面提取出了一些方法,所有繼承了這個介面的類都必然帶有這些方法,那麼我們通過介面獲取這些類的例項就可以使用了,大大增加了靈活性。
但是Java當中的介面有一個很大的問題就是侵入性,說白了就是會顛倒供需關係。舉個簡單的例子,假設你寫了一個爬蟲從各個網頁上爬取內容。爬蟲爬到的內容的類別是很多的,有圖片、有文字還有視訊。假設你想要抽象出一個介面來,在這個介面當中定義你規定的一些提取資料的方法。這樣不論獲取到的資料的格式是什麼,你都可以用這個介面來呼叫。這本身也是介面的使用場景,但問題是處理圖片、文字以及視訊的元件可能是開源或者是第三方的,並不是你開發的。你定義介面並沒有什麼卵用,別人的程式碼可不會繼承這個介面。
當然這也是可以解決的, 比如你可以在這些第三方工具庫外面自己封裝一層,實現你定義的介面。這樣當然是OK的,但是顯然比較麻煩。
Golang當中的介面解決了這個問題,也就是說它完全拿掉了原本弱化的繼承關係,只要介面中定義的方法能對應的上,那麼就可以認為這個類實現了這個介面。
我們先來建立一個interface,當然也是通過type關鍵字:
type Mammal interface {
Say()
}
我們定義了一個Mammal的介面,當中宣告瞭一個Say函式。也就是說只要是擁有這個函式的結構體就可以用這個介面來接收,我們和剛才一樣,定義Cat、Dog和Human三個結構體,分別實現各自的Say方法:
type Dog struct{}
type Cat struct{}
type Human struct{}
func (d Dog) Say() {
fmt.Println("woof")
}
func (c Cat) Say() {
fmt.Println("meow")
}
func (h Human) Say() {
fmt.Println("speak")
}
之後,我們嘗試使用這個介面來接收各種結構體的物件,然後呼叫它們的Say方法:
func main() {
var m Mammal
m = Dog{}
m.Say()
m = Cat{}
m.Say()
m = Human{}
m.Say()
}
出來的結果當然和我們預想的一樣:
總結
今天我們一起聊了物件導向中多型以及介面的概念,藉此進一步瞭解了為什麼golang中的介面設計非常出色,因為它解耦了介面和實現類之間的聯絡,使得進一步增加了我們編碼的靈活度,解決了供需關係顛倒的問題。但是世上沒有絕對的好壞,golang中的介面在方便了我們編碼的同時也帶來了一些問題,比如說由於沒了介面和實現類的強繫結,其實也一定程度上增加了開發和維護的成本。
總體來說這是一個仁者見仁的改動,有些寫慣了Java的同學可能會覺得沒有必要,這是過度解綁,有些人之前深受其害的同學可能覺得這個進步非常關鍵。但不論你怎麼看,這都不影響我們學習它,畢竟學習本身是不帶立場的。今天的內容當中包含一些Java和麵向物件的概念,只是用來引出後面golang的內容,如果存在部分不理解的地方,希望大家抓大放小,理解核心關鍵就好了,不需要細扣每一個細節。
今天的文章到這裡就結束了,如果喜歡本文的話,請來一波素質三連,給我一點支援吧(關注、轉發、點贊)。
本文使用 mdnice 排版