Structs And Interfaces「結構體與介面」

runstone發表於2021-01-24

原文連結

儘管我們可以使用Go自帶的資料型別來寫程式,但在有些時候它會非常的枯燥。考慮一個與形狀互動的程式:

package main

import ("fmt"; "math")

func distance(x1, y1, x2, y2 float64) float64 {
    a := x2 - x1
    b := y2 - y1
    return math.Sqrt(a*a + b*b)
}
func rectangleArea(x1, y1, x2, y2 float64) float64 {
    l := distance(x1, y1, x1, y2)
    w := distance(x1, y1, x2, y1)
    return l * w
}
func circleArea(x, y, r float64) float64 {
    return math.Pi * r*r
}
func main() {
    var rx1, ry1 float64 = 0, 0
    var rx2, ry2 float64 = 10, 10
    var cx, cy, cr float64 = 0, 0, 5

    fmt.Println(rectangleArea(rx1, ry1, rx2, ry2))
    fmt.Println(circleArea(cx, cy, cr))
}

追蹤所有的座標會使得我們很難看到程式在做什麼,並且可能會導致錯誤。

1. Structs

一種讓這段程式更好的簡單方式就是使用一個結構體struct。結構體是一種包含名字欄位的型別。例如我們可以像這樣表達一個圓:

type Circle struct {
    x float64
    y float64
    r float64
}

type關鍵字引入了一種新型別。它的後面是型別的名稱(圓Circle),關鍵字struct表示我們正在定義一個結構體struct型別和花括號內的欄位列表。每一個欄位都有一個名字和型別。像函式一樣,我們可以摺疊相同型別的欄位:

type Circle struct {
    x, y, r float64
}

1.1 Initialization「初始化」

我們可以透過多種方式建立新的Circle型別的例項:

var c Circle

像其他的資料型別一樣,這將會建立一個預設值為0的區域性Circle變數。
對一個struct來說,意味著欄位中的每一個被設定為它們對應的0值(0對應int0.0對應float""對應stringnil對應指標,…)我們也可以使用new函式:

c := new(Circle)

new會給所有的欄位分配記憶體,給它們設定對應的0值,並返回一個指標。(*Circle)通常我們想給每個欄位一個值。我們可以用兩種方式實現,如下:

c := Circle{x: 0, y: 0, r: 5}

或者也可以省略它們的名字,假如我們知道它們定義的順序:

c := Circle{0, 0, 5}

1.2 Fields「欄位」

我們可以使用.操作來訪問欄位。

fmt.Println(c.x, c.y, c.r)
c.x = 10
c.y = 5

讓我們修改一下circleArea函式以便於它可以使用Circle

func circleArea(c Circle) float64 {
    return math.Pi * c.r * c.r
}

main函式中,我們寫:

c := Circle{0, 0, 5}
fmt.Println(circleArea(c))

要記住的一點是,在Go中引數總是被複制的。如果我們嘗試去修改在circleArea函式里面的欄位的其中一個,並不會改動它的原始值。鑑於此,我們通常會這麼寫函式:

func circleArea(c *Circle) float64 {
  return math.Pi * c.r*c.r
}

然後修改一下main函式:

c := Circle{0, 0, 5}
fmt.Println(circleArea(&c))

1.3 Methods「方法」

儘管這比第一版的程式碼要好得多,我們仍舊可以透過使用函式中方法method這一特殊的型別來顯著的改善它:

func (c *Circle) area() float64 {
    return math.Pi * c.r*c.r
}

func關鍵字和函式名字我們已經新增了一個接收器"receiver"。接收器就像一個引數-它有名字和型別-但是透過這種方式建立的函式可以允許我們使用.操作來呼叫函式:

fmt.Println(c.area())

這變的更易讀,我們不再需要&運算子(Go可以透過這種方法自動知道傳給circle的是一個指標),而且因為這個函式只能隨著Circle用,我們可以重新命名該函式為area即可:

讓我們對長方形做同樣的事情:

type Rectangle struct {
    x1, y1, x2, y2 float64
}
func (r *Rectangle) area() float64 {
    l := distance(r.x1, r.y1, r.x1, r.y2)
    w := distance(r.x1, r.y1, r.x2, r.y1)
    return l * w
}

main中:

r := Rectangle{0, 0, 10, 10}
fmt.Println(r.area())

1.4 Embedded Types「嵌入式型別」

一個結構體的欄位通常表示has-a的關係。例如,Circleradius。假設我們有一個person結構體:

type Person struct {
    Name string
}
func (p *Person) Talk() {
    fmt.Println("Hi, my name is", p.Name)
}

然後我們想建立一個Android結構體。我們可以這麼做:

type Android struct {
    Person Person
    Model string
}

這樣是有效的,但是我們更願意說Android是一個Person,而不是Android有一個Person。Go透過使用嵌入式型別來支援這樣的關係。像已知的匿名欄位一樣,嵌入式欄位像這樣:

type Android struct {
    Person
    Model string
}

我們使用(Person)型別,並沒有給它名字。當定義了這種方式,Person結構體可以透過型別名字來訪問:

a := new(Android)
a.Person.Talk()

但是我們也可以在Android中更直接的呼叫Person任意方法:

a := new(Android)
a.Talk()

is-a關係是這樣直觀地運作的:Person可以講話,android是一個person,所以android可以講話。

2. Interfaces「介面」

你可能已經注意到我們可以將Rectanglearea方法命名為為與Circlearea方法相同的方法。這並不是偶然,在現實生活和程式設計中,像這樣的關係是司空見慣的。Go有一種方法可以透過一種稱為介面的型別來顯化這些偶然的相似之處。這有一個Shape介面的例子:

type Shape interface {
    area() float64
}

類似一個結構體,介面的建立使用了type關鍵字,後面緊跟著名字和關鍵字interface。但是並不是定義欄位,我們定義'方法集'。方法集是一個型別為了“實現”介面而必須擁有的方法列表。

在我們的例子中RectangleCircle都擁有一個返回float64area方法,所以兩種型別都是為了實現Shape介面。這本身並不是特別有用,但是我們使用介面型別來作為函式的實參:

func totalArea(shapes ...Shape) float64 {
    var area float64
    for _, s := range shapes {
        area += s.area()
    }
    return area
}

我們可以這樣呼叫這個函式:

fmt.Println(totalArea(&c, &r))

介面也可以被用來作為欄位:

type MultiShape struct {
    shapes []Shape
}

我們甚至可以透過area方法使MultiShape本身變成一個Shape

func (m *MultiShape) area() float64 {
    var area float64
    for _, s := range m.shapes {
        area += s.area()
    }
    return area
}

現在MultiShape可以包含CircleRectangle甚至其它的MultiShape了。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
Stay hungry, stay foolish.

相關文章