Structs And Interfaces「结构体与接口」

原文链接

尽管我们可以使用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.
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!