• Go: 方法


    方法

    在面向对象编程的编程思想里,类、对象、方法是基础。类比到Golang中

    // 类
    type Point struct {X, Y int}
    // 对象
    p := Point{1, 2}
    // 方法 即绑定在struct上的函数
    // ...
    

    方法声明

    方法和函数类似,区别在于它在函数名前多了一个参数(接收器),用来将方法绑定在参数对应的类型上

    package main
    
    import (
    	"fmt"
    	"math"
    )
    
    type Point struct {
    	X, Y float64
    }
    
    func (p Point) Distance(q Point) float64 {
    	return math.Hypot(q.X-p.X, q.Y-p.Y)
    }
    
    func main() {
    	p := Point{1, 2}
    	q := Point{4, 6}
    	fmt.Println(p.Distance(q))  // 5
    }
    

    每个类型都有自己的命令空间,在同一个命名空间里不能有相同名称的方法和成员

    type Line struct {
    	Start  Point
    	End    Point
    	// Length float64
        // 如果取消上面这行的注释 编译报错:type Line has both field and method named Length
    }
    
    func (L Line) Length() float64 {
    	return L.Start.Distance(L.End)
    }
    
    func main() {
        p := Point{1, 2}
    	q := Point{4, 6}
    	fmt.Println(p.Distance(q))  // 5
    	line := Line{p, q}
    	fmt.Println(line.Length())  // 5
    }
    

    不同类型的命名空间是独立的,可以在不同类型中使用相同名字的方法

    type Path []Point
    
    func (path Path) Distance() float64 {
    	sum := 0.0
    	for i := range path {
    		if i > 0 {
    			sum += path[i-1].Distance(path[i])
    		}
    	}
    	return sum
    }
    
    func main() {
    	perim := Path{
    		{1, 1},
    		{5, 1},
    		{5, 4},
    		{1, 1},
    	}
    	fmt.Println(perim.Distance())  // 12
    }
    

    指针接收者的方法

    函数调用实参变量是以复制一份的方式传递的,如果我们想在函数中进行更改会很麻烦;如果一个实参太大,我们希望避免复制整个实参,我们可以通过指针的方式传递变量地址。这也同样使用与方法

    func (p *Point) ScaleBy(factor float64) {
    	p.X *= factor
    	p.Y *= factor
    }
    
    func main() {
        p := Point{1, 2}
        p.ScaleBy(200)
        fmt.Printf("%+v", p) // {X:200 Y:400}
    }
    

    习惯上,如果Point上任何一个方法绑定指针接收者,那么所有的Point方法都应该使用指针接收者。方法的接收者只能是类型(Point)或者类型指针(Point)。

    为了防止混淆,不允许本身是指针的类型进行方法声明:

    type p *int
    func (p) f() {/*...*/}  // 编译错误:非法的接收者类型
    

    以下几种写法都是合法的:

    // case1
    r := &Point{1, 2}
    r.ScaleBy(2)
    fmt.Println(*r)  // {2, 4}
    
    // case2
    p1 := Point{1, 2}
    pptr := &p1
    pptr.ScaleBy(2)
    fmt.Println(p1)  // {2, 4}
    
    // case3
    p2 := Point{1, 2}
    (&p2).ScaleBy(2)
    fmt.Println(p2)  // {2, 4}
    

    注意,不能对一个不能取地址的Point接收者参数调用*Point方法,因为无法获得临时变量的地址。

    Piont{1,2}.ScaleBy(2)  // 编译错误
    

    反过来,指针类型(*Point)变量,它是可以调用Point类型的方法

    type Point struct{}
    
    func (p *Point) PtrFunc() {}
    func (p Point) Func()     {}
    
    func main() {
    	p := Point{}
    	ptr := &Point{}
    	ptr.PtrFunc()
    	ptr.Func()
    
    	Point{}.Func()
    	Point{}.PtrFunc() // 编译错误:cannot call pointer method on Point literal
    
    	p.Func()
    	p.PtrFunc() // 编译器做了隐式转换
    }
    

    疑惑:如果所有类型T方法的接收者是T自己(而非*T),那么复制它的实例是安全的;调用方法的时候必须进行一次复制。但是任何方法的接收者是指针的情况下,应该避免复制T的实例,因为这么做可能会破坏原本的数据。

    nil是一个合法的接收

    方法的接收者可以是nil

    // *IntList的类型nil代表空列表
    type IntList struct {
    	Value int
    	Next  *IntList
    }
    
    func (list *IntList) Sum() int {
    	if list == nil {
    		return 0
    	}
    	return list.Value + list.Next.Sum()
    }
    
    func main() {
    	a1 := IntList{1, nil}
    	a2 := IntList{2, &a1}
    	a3 := IntList{3, &a2}
    
    	fmt.Println(a3.Sum())  // 6
    
    }
    

    当定义一个类型允许为nil作为接收者,应该在文档中显式地表明

    通过结构体内嵌组成类型

    在一个结构体A中嵌套另一个结构体B,则结构体A可以调用结构体B的方法

    import (
    	"fmt"
    	"image/color"
    	"math"
    )
    
    type Point struct{ X, Y float64 }
    
    func (p Point) Distance(q Point) float64 {
    	return math.Hypot(p.X-q.X, p.Y-q.Y)
    }
    
    func (p *Point) ScaleBy(factor float64) {
    	p.X *= factor
    	p.Y *= factor
    }
    
    type ColoredPoint struct {
    	Point
    	Color color.RGBA
    }
    
    func main() {
    	var cp ColoredPoint
    	cp.X = 1
    	fmt.Println(cp.Point.X)  // 1
    
    	p := ColoredPoint{Point{1, 1}, color.RGBA{255, 0, 0, 255}}
    	q := ColoredPoint{Point{5, 4}, color.RGBA{0, 0, 255, 255}}
    
    	//fmt.Println(p.Distance(q)) // 编译错误:cannot use q (type ColoredPoint) as type Point in argument to p.Point.Distance
    	fmt.Println(p.Distance(q.Point))  // 5
    	p.ScaleBy(2)
    	q.ScaleBy(2)
    	fmt.Println(p.Distance(q.Point))  // 10
    }
    

    ColoredPoint类型内嵌了Point类型,它可以调用PointDistanceScaleBy方法。也可以直接访问Point的成员变量。

    实际上,内嵌字段会告诉编译生成额外的包装方法来调用 Point声明的方法:

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

    匿名字段可以是指向命名类型的指针,字段和方法间接地来自于所指向的对象。这可以让我们共享通用的结构以及使对象之间的关系更加动态、多样化。
    我们将ColoredPoint的匿名字段改成指针类型,在对比一下和上面非指针类型的区别:

    import (
    	"fmt"
    	"image/color"
    	"math"
    )
    
    type Point struct{ X, Y float64 }
    
    func (p Point) Distance(q Point) float64 {
    	return math.Hypot(p.X-q.X, p.Y-q.Y)
    }
    
    func (p *Point) ScaleBy(factor float64) {
    	p.X *= factor
    	p.Y *= factor
    }
    
    type ColoredPoint struct {
    	*Point
    	Color color.RGBA
    }
    
    func main() {
    	var cp ColoredPoint
    	cp.Point = &Point{}  // 匿名指针类型的默认值是nil,必须对其进行初始化
    	cp.Point.X = 1  // 如果没有上面的那一行,执行报错:panic: runtime error: invalid memory address or nil pointer dereference
    	fmt.Println(cp.Point.X) // 1
    
    	p := ColoredPoint{&Point{1, 1}, color.RGBA{255, 0, 0, 255}}  // 初始化是Point传地址
    	q := ColoredPoint{&Point{5, 4}, color.RGBA{0, 0, 255, 255}}
    
    	//fmt.Println(p.Distance(q)) // 编译错误:cannot use q (type ColoredPoint) as type Point in argument to p.Point.Distance
    	fmt.Println(p.Distance(*q.Point)) // 5  实参传递时,要转化为值
    	p.ScaleBy(2)
    	q.ScaleBy(2)
    	fmt.Println(p.Distance(*q.Point)) // 10
    }
    

    结构体类型也可以由多个匿名字段

    type ColoredPoint struct {
        Point
        color.RGBA
    }
    
    p := ColoredPoint{Point{1, 1}, color.RGBA{255, 0, 0, 255}}
    

    当调用p.ScaleBy方法时,它会先查找ColoredPoint有没有声明这个方法,如果没有,再从其内嵌对象Pointcolor.RGBA上查找,再从Pointcolor.RGBA的内嵌对象上查找。当同一个查找级别中有同名方式时,编译器报错;

    type A struct {}
    func (a A) Func() {}
    type B struct {}
    func (b B) Func() {}
    type C struct {
        A
        B
    }
    
    func main() {
        c := C{}
        c.Func()  // 编译错误:ambiguous selector c.Func
    }
    

    方法只能在命名的类型(比如Point)和指向他们指针(*Point)中声明,但内嵌帮助我们能够在未命名的结构体类型中声明方法。

    方法变量与表达式

    我们可以将方法赋予一个方法变量,方法变量是一个函数,本质上会绑定到接收者上,可以理解为方法的引用,方法变量只要传递实参就可以调用成功。

    a := Point{1, 2}
    b := Point{4, 6}
    distanceFromA := a.Distance  // 方法变量
    fmt.Println(distanceFromA(b))  // 5
    origin := Point{0, 0}
    fmt.Println(distanceFromA(origin)) // 2.23606797749979
    
    scaleA := a.ScaleBy  // 方法变量
    scaleA(2)
    fmt.Println(a)  // {2, 4}
    

    方法表达式与方法变量相似,区别是方法变量是由将类型声明的变量的方法赋予的,而方法表达式是有类型的方法赋予的,有点绕,看一下例子:

    a := Point{1, 2}
    b := Point{4, 6}
    distanceFromA := a.Distance  // 方法变量 由a的方法赋予
    distance := Point.Distance // 方法表达式 由Point类型的方法赋予
    

    方法的接收者会替换成函数的第一个参数

    fmt.Println(distanceFromA(b))  // 5 方法变量
    fmt.Println(distance(a, b))  // 5 方法表达式 
    fmt.Printf("%T
    ", distance) // func(Point, Point) float64
    
    // scale := Point.ScaleBy // 编译报错:nvalid method expression Point.ScaleBy (needs pointer receiver: (*Point).ScaleBy
    scale := (*Point).ScaleBy
    scale(&a, 2)
    fmt.Println(a)
    fmt.Printf("%T
    ", scale)  // func(*Point, float64)
    

    封装

    控制变量和方法不能通过对象访问(私有),即为封装。Go语言中通过控制命名的大小写来实现,首字母大写的标识符可以被导出,小写的就不可以。因此,可以通过结构体来是实现封装,向调用者隐藏重要的数据和实现细节,防止非法更改。

    type IntSet struct {
        words []uint64
    }
    
    type IntSet2 []uint64
    

    对比两个类型,IntSet将实际存储数据的slice封装成了一个不可访问字段,IntSet2也将数据存储在slice,但它是可以被访问的,我们可以同*s在其他包中访问、更改。

    思考:结构体里的字段一定都要封装起来,不让使用者看到吗?

    封装的优点:

    • Go语言封装的单元是包而不是类型,包内的函数和方法对结构体的字段是可见的
    • 实现细节可以对包的使用方屏蔽,方便设计者灵活改变
    • 防止使用者非法更改结构体内的变量

    封装的缺点:

    • 需要设计者编写很多的方法来实现对字段的读取和更新,因为调用者无法自助。

    封装并不总会需要的,要结合实际的适用场景区别对待。

  • 相关阅读:
    mysql 存储过程 异常处理机制
    Maven 私服打包
    Flink(2):Flink的Source源
    Flink(1):Flink的基础案例
    最后一课
    我的获奖记录及 Important Dates in OI
    目录
    入坑 OI 三周年之际的一些感想
    洛谷 P3781
    Atcoder Typical DP Contest S
  • 原文地址:https://www.cnblogs.com/Zioyi/p/14774881.html
Copyright © 2020-2023  润新知