美文网首页
# Go 方法编程与面向对象

# Go 方法编程与面向对象

作者: PRE_ZHY | 来源:发表于2018-09-29 22:35 被阅读11次

    上一节,我们看到 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方法;重载,是指允许存在多个同名函数,而这些函数的参数表不同,能根据不同的运行调用,匹配合理的方法。

    相关文章

      网友评论

          本文标题:# Go 方法编程与面向对象

          本文链接:https://www.haomeiwen.com/subject/ixcioftx.html