go学习 6、方法

6、方法

面向对象编程(OOP),封装、组合。

6.1 方法声明

在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。

package geometry

import "math"

type Point struct{ X, Y float64 }

// traditional function
func Distance(p, q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

// same thing, but as a method of the Point type
func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

附加的参数p,叫做方法的接收器(receiver)。
在方法调用过程中,接收器参数一般会在方法名之前出现。这和方法声明是一样的,都是接收器参数在方法名字之前。

p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // "5", function call
fmt.Println(p.Distance(q))  // "5", method call

第一个Distance的调用 实际上用的是包级别的函数geometry.Distance,而第二个则是使用刚刚声明的Point,调用的 是Point类下声明的Point.Distance方法。
这种p.Distance的表达式叫做选择器,选择合适的对应p这个对象的Distance方法来执行。
选择器也会被用来选择一个struct类型的字段,比如p.X。由于方法和字段都是在同一命名空间,所以如果我们在这里声明一个X方法的话,编译器会报错,因为在调用p.X时会有歧义。

因为每种类型都有其方法的命名空间,我们在用Distance这个名字的时候,不同的Distance调 用指向了不同类型里的Distance方法。

// A Path is a journey connecting the points with straight lines.
type Path []Point
// Distance returns the distance traveled along the path.
func (path Path) Distance() float64 {
    sum := 0.0
    for i := range path {
        if i > 0 {
            sum += path[i-1].Distance(path[i])
		} 
	}
	return sum 
}

我们可以给同一个包内的任意命名类型定义方法,只要这个命名类型的底层类型(这个例子里,底层类型是指[]Point这个slice,Path就是命名类型)不是指针或者interface。

perim := Path{
    {1, 1},
    {5, 1},
    {5, 4},
    {1, 1},
}
fmt.Println(perim.Distance()) // "12"

对于一个给定的类型,其内部的方法都必须有唯一的方法名,但是不同的类型却可以有同样的方法名。
方法比之函数的一些好处:方法名可以简短。

import "gopl.io/ch6/geometry"
perim := geometry.Path{{1, 1}, {5, 1}, {5, 4}, {1, 1}} fmt.Println(geometry.PathDistance(perim)) // "12", standalone function fmt.Println(perim.Distance()) // "12", method of geometry.Path

6.2 基于指针对象的方法

当这个接受者变量本身比较大时,我们就可以用其指针而不是对象来声明方法。

func (p *Point) ScaleBy(factor float64) {
    p.X *= factor
    p.Y *= factor
}

方法的名字是 (*Point).ScaleBy。括号是必须的
为了避免歧义,在声明方法时,如果一个类型名本身是一个指针的话,是不允许 其出现在接收器中的:

type P *int
func (P) f() { /* ... */ } // compile error: invalid receiver type

想要调用指针类型方法 (*Point).ScaleBy ,只要提供一个Point类型的指针即可:

r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r) // "{2, 4}"
p := Point{1, 2}
pptr := &p
pptr.ScaleBy(2)
fmt.Println(p) // "{2, 4}"
p := Point{1, 2}
(&p).ScaleBy(2)
fmt.Println(p) // "{2, 4}"

go:如果接收器p是一个Point类型的变量,并且其方法需要一个Point指针作为接收器,我们可以用下面这种简短的写法:

 p.ScaleBy(2)

编译器会隐式地帮我们用&p去调用ScaleBy这个方法。这种简写方法只适用于“变量”,我们不能通过一个无法取到地址的接收器来调用指针方法,比如临时变量的内存地址就无法获取得到:

 Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal

我们可以用一个 *Point 这样的接收器来调用Point的方法,因为我们可以通过地址来找到这个变量,只要用解引用符号来取到该变量即可。

pptr.Distance(q)
(*pptr).Distance(q)

总结

  • 不论是接收器的实际参数和其接收器的形式参数相同,比如两者都是类型T或者都是类 型 *T :
Point{1, 2}.Distance(q) //  Point
pptr.ScaleBy(2)         // *Point
  • 接收器实参是类型T,但接收器形参是类型 *T ,这种情况下编译器会隐式地为我们取变量的地址:
 p.ScaleBy(2) // implicit (&p)
  • 接收器实参是类型 *T ,形参是类型T。编译器会隐式地为我们解引用,取到指针指向的实际变量:
 pptr.Distance(q) // implicit (*pptr)

6.2.1 Nil也是一个合法的接收器类型

package url

type Values map[string][]string

func (v Values) Get(key string) string{
	if vs:=v[key];len(vs)>0{
		return vs[0]
	}
	return ""
}

func (v Values) Add(key,value string){
	v[key]=append(v[key],value)
}
m := url.Values{"lang": {"en"}} // direct construction
m.Add("item", "1")
m.Add("item", "2")
fmt.Println(m.Get("lang")) // "en"
fmt.Println(m.Get("q"))    // ""
fmt.Println(m.Get("item")) // "1"      (first value)
fmt.Println(m["item"])     // "[1 2]"  (direct map access)
m = nil
fmt.Println(m.Get("item")) // ""
m.Add("item", "3")         // panic: assignment to entry in nil map

nil的字面量编译器无法判断其准备类型。最后的那行m.Add的调用就会产生一 个panic,因为他尝试更新一个空map。

6.3 通过嵌入结构体来扩展类型

import "image/color"
type Point struct{ X, Y float64 }
type ColoredPoint struct {
    Point
    Color color.RGBA
}

我们可以直接认为通过嵌入的字段就是 ColoredPoint自身的字段,而完全不需要在调用时指出Point

var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X) // "1"
cp.Point.Y = 2
fmt.Println(cp.Y) // "2"

可以把ColoredPoint类型当作接收器来调用 Point里的方法,即使ColoredPoint里没有声明这些方法:

red := color.RGBA{255, 0, 0, 255}
blue := color.RGBA{0, 0, 255, 255}
var p = ColoredPoint{Point{1, 1}, red}
var q = ColoredPoint{Point{5, 4}, blue}
fmt.Println(p.Distance(q.Point)) // "5"
p.ScaleBy(2)
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) // "10"

Point类的方法也被引入了ColoredPoint
尽管q有着Point这个内嵌类型,我们也必须要显式地选择它。尝试直接传q的话你会看到下面这样的错误::

 p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point

一个ColoredPoint并不是一个Point,但他"has a"Point,并且它有从Point类里引入的Distance 和ScaleBy方法。如果你喜欢从实现的角度来考虑问题,内嵌字段会指导编译器去生成额外的包装方法来委托已经声明好的方法,和下面的形式是等价的:

func (p ColoredPoint) Distance(q Point) float64 {
    return p.Point.Distance(q)
}
func (p *ColoredPoint) ScaleBy(factor float64) {
    p.Point.ScaleBy(factor)
}

在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中(译注:访问需要通过该指针指向的对象去取)。添加这一层间接关系让我 们可以共享通用的结构并动态地改变对象之间的关系。下面这个ColoredPoint的声明内嵌了一 个*Point的指针。

type ColoredPoint struct {
    *Point
    Color color.RGBA
}
p := ColoredPoint{&Point{1, 1}, red}
q := ColoredPoint{&Point{5, 4}, blue}
fmt.Println(p.Distance(*q.Point)) // "5"
q.Point = p.Point                 // p and q now share the same Point
p.ScaleBy(2)
fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"

一个struct类型也可能会有多个匿名字段。我们将ColoredPoint定义为下面这样:

type ColoredPoint struct {
    Point
	color.RGBA 
}

这种类型的值便会拥有Point和RGBA类型的所有方法,以及直接定义在ColoredPoint中的方法。

var (
    mu sync.Mutex // guards mapping
    mapping = make(map[string]string)
)
func Lookup(key string) string {
    mu.Lock()
    v := mapping[key]
    mu.Unlock()
    return v
}
var cache = struct {
    sync.Mutex
    mapping map[string]string
}{
    mapping: make(map[string]string),
}

func Lookup(key string) string {
    cache.Lock()
    v := cache.mapping[key]
    cache.Unlock()
    return v
}

6.4 方法值和方法表达式

p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance
fmt.Println(distanceFromP(q))
var origin Point
fmt.Println(distanceFromP(origin)) // "2.23606797749979", sqrt(5)
scaleP := p.ScaleBy // method value
scaleP(2)
scaleP(3)
scaleP(10)
// p becomes (2, 4)
//      then (6, 12)
//      then (60, 120)

time.AfterFunc这个函数的功能是 在指定的延迟时间之后来执行一个(译注:另外的)函数。且这个函数操作的是一个Rocket对象 r

type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }
r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() })

直接用方法"值"传入AfterFunc的话可以更为简短:

 time.AfterFunc(10 * time.Second, r.Launch)

当调用一个方法时,我们必须要用选择器(p.Distance)语法来指定方法的接收器。
当T是一个类型时,方法表达式可能会写作 T.f 或者 (*T).f ,会返回一个函数"值",这种函 数会将其第一个参数用作接收器,所以可以用通常(译注:不写选择器)的方式来对其进行调用:

p := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance   // method expression
fmt.Println(distance(p, q))  // "5"
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"
scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p)            // "{2 4}"
fmt.Printf("%T\n", scale) // "func(*Point, float64)"
// 译注:这个Distance实际上是指定了Point对象为接收器的一个方法func (p Point) Distance(), // 但通过Point.Distance得到的函数需要比实际的Distance方法多一个参数,
// 即其需要用第一个额外参数指定接收器,后面排列Distance方法的参数。
// 看起来本书中函数和方法的区别是指有没有接收器,而不像其他语言那样是指有没有返回值。
type Point struct{ X, Y float64 }

func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} }
func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} }

type Path []Point

func (path Path) TranslateBy(offset Point, add bool) {
    var op func(p, q Point) Point
    if add {
        op = Point.Add
    } else {
        op = Point.Sub
    }
    for i := range path {
        // Call either path[i].Add(offset) or path[i].Sub(offset).
        path[i] = op(path[i], offset)
	} 
}

6.5 封装

一个对象的变量或者方法如果对调用方是不可见的话,一般就被定义为“封装”。封装有时候也
被叫做信息隐藏,同时也是面向对象编程最关键的一个方面。
Go语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种限制包内成员的方式同样适用于struct或者一个类型的方法。因而如果我们想要封装一个对象,我们必须将其定义为一个struct。
这也就是IntSet被定义为struct类型的原因,尽管它只有一个字段:

type IntSet struct {
    words []uint64
}

一个struct类型的字段对同一个包的所有代码都有可见性,无论你的代码是写在一个函数还是一个方法里。
封装的优点:

  • 因为调用方不能直接修改对象的变量值,其只需要关注少量的语句并且只要弄懂少量变量的可能的值即可。
  • 隐藏实现的细节,可以防止调用方依赖那些可能变化的具体实现,这样使设计包的程序员在不破坏对外的api情况下能得到更大的自由。
  • 阻止了外部调用方对对象内部的值任意地进行修改。
    下面的Counter类型允许调用方来增加counter变量的值,并且允许将这个值reset为0,但是不允许随便设置这个值:
type Counter struct { n int }
func (c *Counter) N() int     { return c.n }
func (c *Counter) Increment() { c.n++ }
func (c *Counter) Reset()     { c.n = 0 }

只用来访问或修改内部变量的函数被称为setter或者getter,例子如下,比如log包里的Logger 类型对应的一些函数。在命名一个getter方法时,我们通常会省略掉前面的Get前缀。这种简洁上的偏好也可以推广到各种类型的前缀比如Fetch,Find或者Lookup。

package log
type Logger struct {
    flags  int
    prefix string
    // ...
}
func (l *Logger) Flags() int
func (l *Logger) SetFlags(flag int)
func (l *Logger) Prefix() string
func (l *Logger) SetPrefix(prefix string)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值