Go 是一種面向物件的語言嗎?
要真正理解“面向物件”的含義,我們需要回顧一下這個概念的起源。第一個面向物件的語言 simula 出現在 1960 年代。它介紹了物件、類、繼承和子類、虛擬方法、協程等等。也許最重要的是,它引入了資料和邏輯完全獨立的思維正規化轉變。
雖然您可能不熟悉 Simula,但您無疑熟悉將 Simula 稱為靈感的語言,包括 Java、C++、C# 和 Smalltalk,它們反過來又是 Objective C、Python、Ruby、Javascript、Scala 的靈感來源、PHP、Perl……當今使用的幾乎所有流行語言的名副其實的列表。這種思維轉變已經佔據了主導地位,以至於今天大多數程式設計師從未以任何其他方式編寫程式碼。
由於不存在標準定義,為了我們討論的目的,我們將提供一個定義。
面向物件的系統不是將程式構建為程式碼和資料,而是使用“物件”的概念將兩者整合在一起。物件是具有狀態(資料)和行為(程式碼)的抽象資料型別。
也許作為初始實現具有繼承性和多型性的結果,幾乎所有派生類都採用了這一特性,面向物件程式設計的定義通常也包括這些特性作為需求。
我們將看看 Go 是如何處理物件、多型和繼承的,並讓您做出自己的結論。
Go 中的物件
Go 沒有一個叫做“物件”的東西,但“物件”只是一個表示意義的詞。 重要的是意義,而不是術語本身。
雖然 Go 沒有稱為“物件”的型別,但它確實有一個型別,它與集成了程式碼和行為的資料結構的相同定義相匹配。 在 Go 中,這稱為
“struct”是一種包含命名欄位和方法的型別。
讓我們用一個例子來說明這一點:
type rect struct { width int height int } func (r *rect) area() int { return r.width * r.height } func main() { r := rect{width: 10, height: 5} fmt.Println("area: ", r.area()) }
點選執行示例,可以線上執行上面程式碼檢視結果。
我們可以在這裡談論很多。最好逐行瀏覽程式碼並解釋發生了什麼。
第一個塊定義了一種稱為“rect”的新型別。這是一個結構體型別。該結構體有兩個欄位,都是 int 型別。
下一個塊是定義一個繫結到這個結構體的方法。這是通過定義一個函式並將其附加(繫結)到一個矩形來實現的。從技術上講,在我們的示例中,它實際上附加到一個指向 rect 的指標。雖然該方法繫結到該型別,但 Go 要求我們使用該型別的值來進行呼叫,即使該值是該型別的零值(在結構體的情況下,零值是 nil)。
最後一個塊是我們的 main 函式。第一行建立一個 rect 型別的值。我們可以使用其他語法來做到這一點,但這是最慣用的方式。第二行將在我們的 rect 'r' 上呼叫 area 函式並列印結果。
我們還沒有做什麼?在大多數面向物件的語言中,我們將使用“class”關鍵字來定義我們的物件。使用繼承時,最好為這些類定義介面。在這樣做時,我們將定義一個繼承層次樹(在單繼承的情況下)。
另外值得注意的是,在 Go 中,任何命名型別都可以有方法,而不僅僅是結構體。例如,我可以定義一個型別為 int 的新型別“Counter”並在其上定義方法。
繼承與多型
有幾種不同的方法來定義物件之間的關係。 雖然它們彼此有很大不同,但作為程式碼重用的機制,它們都有一個共同的目的。
- 繼承
- 多重繼承
- 子型別(多型)
- 物件組合
單繼承和多繼承
繼承是指一個物件基於另一個物件,使用相同的實現。存在兩種不同的繼承實現。它們之間的根本區別在於一個物件是可以從單個物件繼承還是從多個物件繼承。這是一個看似很小的區別,但具有很大的影響。單繼承的層次結構是一棵樹,而多繼承的層次結構是一個格子。單繼承語言包括 PHP、C#、Java 和 Ruby。多繼承語言包括 Perl、Python 和 C++。
子型別(多型)
在某些語言中,子型別和繼承是交織在一起的,以至於如果您的特定觀點來自一種它們緊密耦合的語言,那麼這對於上一節來說似乎是多餘的。子型別建立 is-a 關係,而繼承只重用實現。子型別定義了兩個(或多個)物件之間的語義關係。繼承只定義了句法關係。
物件組合
物件組合是通過包含其他物件來定義一個物件。物件不是從它們繼承,而是包含它們。與子型別的 is-a 關係不同,物件組合定義了 has-a 關係。
Go 中的繼承
Go 是有意設計的,完全沒有任何繼承。這並不意味著物件(結構體)之間沒有關係,而是 Go 作者選擇使用替代機制來暗示關係。對於許多第一次接觸 Go 的人來說,這個決定似乎會削弱 GO。實際上,它是 Go 最好的屬性之一,它解決了圍繞繼承的十年前的問題和爭論。
Go 中的多型和組合
Go 嚴格遵循組合優於繼承的原則,而不是繼承。 Go 通過結構體和介面之間的子型別 (is-a) 和物件組合 (has-a) 關係來實現這一點。
Go 中的物件組合
Go 用來實現物件組合原理的機制稱為嵌入型別。 Go 允許你在一個結構體中嵌入另一個結構體,給它們一個 has-a 關係。
一個很好的例子是 Person 和 Address 之間的關係。
type Person struct { Name string Address Address } type Address struct { Number string Street string City string State string Zip string } func (p *Person) Talk() { fmt.Println("Hi, my name is", p.Name) } func (p *Person) Location() { fmt.Println("I’m at", p.Address.Number, p.Address.Street, p.Address.City, p.Address.State, p.Address.Zip) } func main() { p := Person{ Name: "Steve", Address: Address{ Number: "13", Street: "Main", City: "Gotham", State: "NY", Zip: "01313", }, } p.Talk() p.Location() }
點選執行示例,可以線上執行上面程式碼檢視結果。
上面程式碼執行結果
從這個例子中要意識到的重要事情是 Address 仍然是一個獨特的實體,同時存在於 Person 中。 在 main 函式中,我們演示了您可以將 p.Address 欄位設定為地址,或者通過點符號訪問它們來簡單地設定這些欄位。
Go 中的偽子型別
偽 is-a 關係以類似且直觀的方式工作。 通過對我們上面的例子進行擴充套件。 讓我們使用以下語句。 一個人可以說話。 公民是人,因此公民可以說話。
此程式碼依賴並新增到上面示例中的程式碼。
type Citizen struct { Country string Person } func (c *Citizen) Nationality() { fmt.Println(c.Name, "is a citizen of", c.Country) } func main() { c := Citizen{} c.Name = "Steve" c.Country = "America" c.Talk() c.Nationality() }
輸出結果如下
我們在 go 中使用所謂的匿名欄位來完成這個偽 is-a 關係。 在我們的示例中,Person 是 Citizen 的匿名欄位。 只給出型別,不給出欄位名稱。 它假定了 Person 的所有屬性和方法,並且可以自由使用它們或對它們進行擴充套件。
Go 中的真正子型別化
正如我們上面寫的,子型別是 is-a 關係。 在 Go 中,每種型別都是不同的,沒有什麼可以充當另一種型別,但兩者都可以遵循相同的介面。 介面可以用作函式(和方法)的輸入和輸出,從而在型別之間建立 is-a 關係。
Go 中對介面的依從性不是通過像“using”這樣的關鍵字來定義的,而是通過在型別上宣告的實際方法來定義的。 在 Efficient Go 中,它將這種關係稱為“如果某事可以做到這一點,那麼它可以在這裡使用。”這非常重要,因為它使人們能夠建立一個介面,該介面定義在外部包中的型別可以遵守。
繼續上面的示例,我們新增一個新函式 SpeakTo 並修改主函式來嘗試對一個公民和一個人說話。
func SpeakTo(p *Person) { p.Talk() } func main() { p := Person{Name: "Dave"} c := Citizen{Person: Person{Name: "Steve"}, Country: "America"} SpeakTo(&p) SpeakTo(&c) }
上面示例的輸出結果如下
# command-line-arguments ./main.go:47:13: cannot use &c (type *Citizen) as type *Person in argument to SpeakTo
正如預期的那樣,這失敗了。 在我們的程式碼中,Citizen 不是 Person,儘管它們共享許多相同的屬性,但它們被視為不同的型別。
但是,如果我們新增一個名為 Human 的介面並將其用作 SpeakTo 函式的輸入,它將按預期工作。
type Human interface { Talk() } func SpeakTo(h Human) { h.Talk() } func main() { p := Person{Name: "Dave"} c := Citizen{Person: Person{Name: "Steve"}, Country: "America"} SpeakTo(&p) SpeakTo(&c) }
執行結果如下
Hi, my name is Dave Hello, my name is Steve and I'm from America
我們可以點選上面的執行示例檢視完整程式碼及其執行結果
關於 Go 中的子型別,有兩個關鍵點:
- 我們可以使用匿名欄位來遵守介面。 我們也可以實現很多介面。 通過使用匿名欄位和介面,我們非常接近真正的子型別。
- Go 確實提供了適當的子型別功能,但僅限於使用型別。 介面可用於確保各種不同的型別都可以作為函式的輸入被接受,甚至可以作為函式的返回值,但實際上它們保留了不同的型別。 這清楚地顯示在主函式中,我們不能直接在 Citizen 上設定 Name,因為 Name 實際上不是 Citizen 的屬性,它是 Person 的屬性,因此在 Citizen 的初始化期間還沒有出現。
Go,沒有物件或繼承的面向物件程式設計
正如我們在這裡展示的那樣,儘管存在一些術語差異,面向物件的基本概念在 Go 中仍然存在並且使用也情況也很好。 術語差異是必不可少的,因為所使用的機制實際上與大多數面向物件的語言不同。
Go 使用結構體作為資料和邏輯的結合。 通過組合,可以在 Structs 之間建立 has-a 關係,以最大限度地減少程式碼重複,同時避免繼承的脆弱混亂。 Go 使用介面在型別之間建立 is-a 關係,而無需不必要的和反作用的宣告。
歡迎使用新的“無物件”面向物件程式設計模型。