# Go 方法编程与面向对象

上一节,我们看到 Go 函数特性及函数式编程风格。这一节,将会主要展示Go的方法特性及面向对象编程。什么是方法?当我们谈到具体的一个方法时,其实我们都默认包含了一个对象。例如司马光砸缸的故事,司马光救人的方法是砸缸,用的对象是石头,如果严格来说,司马光的方法是用石头砸缸,它用的不是手砸缸,也不是树枝,而是石头,那么砸这个方法是属于石头的,是因为石头有一些属性,支持它有该方法。软件编程其实是用抽象语言描述世界的过程,面向对象就是一种认识世界的方法,人们通过认识一个对象,认识该对象有一些与众不同的属性,随之有一些方法,那么编程的时候也按照该认识来编写。经过近半个世纪的软件开发实践和检验,面向对象是行之有效的认识和刻画现实的世界的方法之一。

定义一个对象(对象总体来说是一个泛指,既可以指实际的物体,也可以指抽象的东西,前提是已经定义该事务)。最为流行的几种语言Java,C++,Python,Javascript都用类这个术语来确定表达对象的,但如前所见,我们见到Go所有的类型,没有类这一种。Go 是通过数据类型及其内涵的值来表达对象的概念的,再将方法绑定到该类型就可以确定描述一个对象。

package main

import (
    "fmt"
    "math"
)

type point struct {
    x, y float64
}// 定义了一个点

type circle struct {
    center point
    radius float64
}// 圆是通过一个点及其半径而定义的

func distance(p1, p2 point) float64 {
    return math.Hypot(p1.x-p2.x, p1.y-p2.y)
} // 定义了一个点之间距离的求取一般函数,math.Hypot 是一个求平方和的开方的方法

func (c circle) distance(ca circle) float64 {
    return distance(c.center, ca.center)
}// 定义了一个圆距离方法,就是求取圆心距离

func (p point) distance(q point) float64 {
    return math.Hypot(p.x-q.x, p.y-q.y)
}// 定义了点与点之间距离的方法

func (c circle) distancefix(ca circle) float64 {
    return c.center.distance(ca.center)
}// 有了点与点之间的距离求取方法,就可以定义圆心到圆心的距离方法,而不必借助两个点之间的距离,含义更加明确

type circlef struct {
    point
    radius float64
}// 定义了另一类圆,虽然定义了圆心,但是并没有名称,还记得吗?Go 会提升字段。
//这实际上是继承了point,因为这种方式不仅继承了point的属性,还继承了point的方法

func (c circlef) cdistance(ca circlef) float64 {
    return c.point.distance(ca.point)
}

func main() {
    p := point{3.0, 4.0}
    q := point{0, 0}
    fmt.Println("distance(p,q):", distance(p, q))
    fmt.Println("p.distance(q):", p.distance(q))
    c1 := circle{
        center: p,
        radius: 2.0,
    }
    c2 := circle{
        center: point{x: 6, y: 8},
        radius: 3.0,
    }
    fmt.Println("distance(c1,c2):", c1.distance(c2))
    fmt.Println("distance(c1,c2):", c1.distancefix(c2))
    c3 := circlef{
        point:  point{x: 6, y: 8},
        radius: 4.0,
    }
    c4 := circlef{
        point:  point{x: 0, y: 0},
        radius: 2.0,
    }
    fmt.Println("c3.distance(c4.point):", c3.distance(c4.point))
    // 继承的point方法,circlef 并没有定义distance,直接用的point的方法,
    // 就会出现c3.distance(c4.point)这种尴尬的语义,一个圆到到另一个圆心的距离
    // 该方法可以使用,但是并不如上面定义center名称来的更好,更为深层的原因是 circle 不应该是继承了一个点,而是包含一个点的问题
    fmt.Println("c3.cdistance(c4):", c3.cdistance(c4))
    // 如果这里不明确,定义cdistance,仍然是 distance,那么上一条语句就会报错,
    //因为c3.distance有两种定义,一种是继承point而来的distance,一个自己定义的,既然有更明确的定义,就不会默认调用point的方法
    //这时候c3.distance(c4.point)参数c4.point就会与func (c circlef) cdistance(ca circlef) float64 不相符而报错
}

/* Result
distance(p,q): 5
p.distance(q): 5
distance(c1,c2): 5
distance(c1,c2): 5
c3.distance(c4.point): 10
c3.cdistance(c4): 10
*/

上面的例子通过一组二维坐标定义了一个点,并且为该点定义了一个求取点与点之间距离的方法,同时也声明了一个点与点之间距离的全局函数,这两种方式效果相当,但前者显然表达起来更加自然,并且容易理解和阅读。当我们建模并描述时,描述多种方法,通过对象来梳理方法,使得其更有章法。

该例子中还通过struct两种方式定义两种圆,第一种圆circle,将视为一个整体,当中包含一个点和一个半径,定义这个点叫center,实现了圆心与圆心的距离求取的一个方法,同时圆心也是点,同样可以使用该方法,但所有操作都必须明确指向点才能用;第二种圆是利用Go的字段提升定义的,从后面的例子看到,该圆可以直接调用point的方法,那就意味着该圆其实是一个多了半径的点,其实就是圆继承了点,形成了父子概念。这有什么区别呢?用相对准确且没有歧义的表达是:两者都属于继承,前者圆与点的关系是 has-a,后者圆与点的关系是 is-a,很显然,圆与点的关系应当时圆具有圆心这么一个点的属性,并非圆是一个点+半径。什么情况是is-a?举个例子:矩形是一个四边形,矩形继承四边形时,表达的概念就是矩形是一个四边形(is-a),所有四边形的方法和属性,矩形应该完全有并且还有其他。

再看一个例子,体会方法及对象操作的细节。

// 在上述文件中定义
func (c circle) moveto(p point) {
    c.center = p
}

func (c *circle) scaleby(n float64) *circle {
    c.radius *= n
    return c
}

func (c circle) String() string {
    return fmt.Sprintf("Circle: CenterLoc:(%g,%g), radius:%g", c.center.x, c.center.y, c.radius)
}

func (c *circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

// 并在main中添加如下语句
    fmt.Println(c1)
    c1.moveto(q)
    fmt.Println(c1)
    c1.center = q
    fmt.Println(c1)
    fmt.Println(c1.perimeter())
    fmt.Println(c1.scaleby(2))
    c1per := c1.perimeter
    fmt.Println(c1per())
    fmt.Println(c1.scaleby(2).scaleby(3))
    cper := (*circle).perimeter
    fmt.Println(cper(&c1))

/* Result
Circle: CenterLoc:(3,4), radius:2
Circle: CenterLoc:(3,4), radius:2
Circle: CenterLoc:(0,0), radius:2
12.566370614359172
Circle: CenterLoc:(0,0), radius:4
25.132741228718345
Circle: CenterLoc:(0,0), radius:24
150.79644737231007
*/

moveto的本意是将圆从一处移动到另一处,但在定义circle方法使用的值传递,那么在函数内部修改的圆心值只是原有c1的一个副本,而原始的值并没有改变,所以打印信息还是一样的。此时修改圆心只能通过更加显式的修改c1.center=p。但对于半径的修改(scalebby),使用的指针的方式,则成功修改了半径。

在求取圆的周长的时候,我们声明了func (c *circle) perimeter() float64,但在调用时,我们除了直接调用c1.perimeter()求取周长,还声明了c1percper两个函数语法糖,在Go中,前者称之为方法值,它是绑定了c1.perimeter,该方法只绑定c1,调用c1per()就会直接调用c1.perimeter();后者是方法表达式,它绑定了(*circle).perimeter,由于该方法只绑定对象,使用该方法时仍然需要确定一个具体实例,所以调用的时候cper(&c1)

同时注意到,打印circle的样式的改变,因为我们声明了一个func (c circle) String() string的函数,而 fmt.Println() 默认调用了该方法输出,其原理我们后续再探讨。除此之外,在修改半径的方法中,我们没有只修改半径,同时返回了circle的指针,使得该方法可以链式调用,c1.scaleby(2).moveto(q),同时返回指针在Go默认转换下,可以如同结构体定义一般调用。

面向对象编程,其主要思路是通过对象方法修改对象的属性或者说状态来实现目的的过程,由于面向对象目标认知简单且组织容易,使得该方法一直流行于软件行业。面向对象的主要工作是界定对象,定义其属性,并标记其状态转移条件才是难点。面向对象除了上述思想,还有三个基本特性:封装、继承、多态。

  1. 封装

封装的主要的思想是将部分信息隐藏隔离实现保护,同时暴露必须调用方法,使得该代码可以被外界合理利用,同时即使将来内部实现改变,只要接口方法不变,外部调用就可以保持不变。在Go中,封装只有一个层次就是包,即只有包内代码可以实现封装,对外信息不可见,无法像其他语言一样,实现类内的私有方法,对类外实现隔离;Go 对外暴露只有一种方法,不管是变量还是函数或方法,就是大写第一个字母。该内容具体在包一节会具体展开。

  1. 继承

继承的核心代码复用,通过对已有对象的聚合(has-a)或派生(is-a)的方式,实现一个新的对象,利用原有对象的属性或者方法实现新的描述。上面我们已经具体接触到了。

  1. 多态

多态,有二种方式:覆盖与重载。覆盖,就是指子类函数重新定义父类的函数的做法,例如上面的distance方法;重载,是指允许存在多个同名函数,而这些函数的参数表不同,能根据不同的运行调用,匹配合理的方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,929评论 18 399
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 175,294评论 25 709
  • 元旦。翌日,带着散散心的心情,从成都东乘坐开往乐山的C6301次动车。这一路风驰电掣,如入仙境,根本看不清窗外的风...
    lambeta阅读 3,440评论 0 1
  • 在Hello World中,我们对文字的绘制进行了简单的说明,这一篇文章就让我们看一下文字绘制中所需要的具体过程的...
    神经骚栋阅读 9,872评论 3 24
  • 这两天听了真经派的几节课,感觉只有听力和作文比较有感触,体力你一定要提前看选项,不求能全部翻译,多留意动词名词形容...
    高大雄阅读 1,092评论 0 0