方法
重点:面向对象编程(OOP:Object Oriented Programming)
不同于C++和Java的Class
类写法,
1.方法声明
1 2 3 4 5 6 7 8 9 10 11 12 13
| import "math"
type Point struct { X, Y float64 }
func Distance(p, q Point) float64 { return math.Hypot(q.X-p.X, q.Y-p.Y) }
func (p Point) Distance(q Point) float64 { return math.Hypot(q.X-p.X, q.Y-p.Y) }
|
此处11行的代码,有一个Point
类型的变量为p
,此处的p
名为方法的接收器receiver
,早期也被称为”向一个对象发送消息”。
此处我们来看一下C++的写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| #include <iostream> #include <math.h> using namespace std;
class Point { protected: float X, Y;
public: Point(float x, float y); float Distance(Point q); };
Point::Point(float x, float y) { X = x; Y = y; }
float Point::Distance(Point q) {
return hypot(X - q.X, Y - q.Y); } int main() { Point p(1, 1); Point q(2, 2); cout << p.Distance(q) << endl; }
|
视线回到go
我们来测试一下上面的代码
1 2 3 4
| p := Point{1, 2} q := Point{4, 6} fmt.Println(Distance(p, q)) fmt.Println(p.Distance(q))
|
第三行调用的是函数,第四行调用的是方法,它俩没有产生任何冲突,仅仅是名字相同,一个是简单的函数运算,一个是Point类下的指定方法。
p.Distance(q)
被称为选择器,只有对应的类型才能够使用。
1 2 3 4 5 6 7 8 9 10 11 12
| 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
}
|
此处Point
是一个slice
类型,虽然不是结构体,但是依然可以定义方法。
其实任何类型都可以定义方法,只要不是指针和interface{}
此处的Distance()
是特定于Path
的方法
如果是对于同一类型,内部方法必须使用唯一方法名,但是如果是不同类型,方法名可以相同
2.基于指针对象的方法
调用函数时,会拷贝参数值,但是如果函数需要修改一个变量或者变量太大,这个时候就需要使用指针,通过地址直接修改变量。
如下:
1 2 3 4
| func (p *Point) ScaleBy(factor float64) { p.X *= factor p.Y *= factor }
|
现实之中,如果一个Point
的类含有一个指针作为接收器的方法,理论上其余的方法也都应该以指针作为接收器方法。
特别注意,如果一个类型名本身就是一个指针,是不能够出现在接收器之中的。
1 2
| type P *int func (P) f() { }
|
想要使用Point
的指针接收器,只需要提供Point
类型的指针即可
如下:
1 2 3
| r := &Point{1, 2} r.ScaleBy(2) fmt.Println(*r)
|
1 2 3 4
| p := Point{1, 2} pptr := &p pptr.ScaleBy(2) fmt.Println(p)
|
1 2 3
| p := Point{1, 2} (&p).ScaleBy(2) fmt.Println(p)
|
不过后面两种方法略显笨拙,以下为简短写法
编译器会隐式地帮我们用&p 去调用 ScaleBy 这个方法。这种简写方法只适用于“变量”,包括struct 里的字段比如 p.X,以及 array 和 slice 内的元素比如 perim[0]。我们不能通过一个无法取到地址的接收器来调用指针方法,比如临时变量的内存地址就无法获取得到:
但是我们可以用一个Point 这样的接收器来调用 Point 的方法,因为我们可以通过地址来找到这个变量,只要用解引用符号来取到该变量即可。编译器在这里也会给我们隐式地插入*这个操作符,所以下面这两种写法等价的:
1 2
| pptr.Distance(q) (*pptr).Distance(q)
|
3.通过嵌入结构体来扩展类型
1 2 3 4 5 6
| import "image/color" type Point struct{ X, Y float64 } type ColoredPoint struct { Point Color color.RGBA }
|
本来可以将颜色点定义为含有X
、Y
、Color
三个字段的结构体,但是我们可以把Point
嵌套放入ColoredPoint
之中。
在声明的时候,我们不需要再定义Point
,而是可以直接使用ColoredPoint
定义X
、Y
,
对于 Point 中的方法我们也有类似的用法,我们可以把 ColoredPoint 类型当作接收器来调用Point 里的方法,即使 ColoredPoint 里没有声明这些方法:
1 2 3 4 5 6 7 8 9 10 11
| 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))
p.ScaleBy(2)
|
展示可以看出Point
类的方法都被引入到了ColoredPoint
之中
此处涉及到面向对象的知识,Point
被称之为基类,而ColoredPoint
被称之为子类。
刚刚的调用等价于
1 2 3 4 5 6
| func (p ColoredPoint) Distance(q Point) float64 { return p.Point.Distance(q) } func (p *ColoredPoint) ScaleBy(factor float64) { p.Point.ScaleBy(factor) }
|
当 Point.Distance 被第一个包装方法调用时,它的接收器值是 p.Point,而不是 p,当然了,在 Point 类的方法里,你是访问不到 ColoredPoint 的任何字段的。
在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中。添加这一层间接关系让我们可以共享通用的结构并动态地改变对象之间的关系。下面这个 ColoredPoint 的声明内嵌了一个*Point 的指针。
1 2 3 4 5 6 7
| type ColoredPoint struct {
*Point
Color color.RGBA
}
|