golang学习笔记20-面向对象(二):方法与结构体【重要】
本节内容是面向对象的核心与基础,很重要。
注意:由于导包语句已经在19讲(笔记19:面向对象的引入)展示过了,所以这里就不展示了。
一、方法的定义与细节
方法是与特定类型(通常是结构体)关联的函数。方法可以操作该类型的实例,并且可以使用其字段。定义方法时,需要指定一个接收者(receiver),这使得方法能够访问该接收者的属性。语法如下:
func (receiver TypeName) MethodName(parameters) returnList {
// 方法体
}
receiver是接收者的名称,类型是你想要关联的自定义类型(通常是结构体)。
MethodName是方法的名称。
parameters是方法接受的参数。
returnList是返回值类型列表。
首先,在utils中定义一个结构体和一个方法:
type Circle struct {
Radius float64
}
// 定义一个方法,计算圆的面积
// 1. 方法中参数名字随便起
// 2. 方法 Area 和结构体 Circle绑定,必须在同包中定义!
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
然后在main中调用:
func main() {
// 创建一个 Circle 实例
circle := utils.Circle{Radius: 5}
// 调用方法
area := circle.Area()
// 输出结果
fmt.Printf("Area: %.2f\n", area)
}
一些细节
1.如果其他类型的变量调用area一定会报错。
2.结构体对象传入方法area中,属于值传递,和参数传递一致。
3.receiver的类型是你想要关联的自定义类型。
4.如果某个自定义类型实现了返回string,名字为String()的方法(如果是同包内调用,s可以小写),那么fmt包的Printf或Println函数打印该类型时,会自动调用String()。
对于前两点,首先,在utils中定义另一个方法来演示值传递:
// 定义一个方法,演示值传递
func (c Circle) SetRadius(newRadius float64) {
c.Radius = newRadius
fmt.Printf("Inside Radius: %.2f\n", c.Radius)
}
然后在main文件中调用:
type test struct {
a int
}
func main() {
// 创建一个 Circle 实例
circle := utils.Circle{Radius: 5}
// 调用方法
area := circle.Area()
fmt.Printf("Area: %.2f\n", area)
// 1. 如果其他类型变量调用 area 一定会报错
// 下面的代码将导致编译错误
//t := test{}
// t.area() // 这行会报错,因为 radius 不是 Circle 类型
// 2. 结构体对象传入方法 Area 中,属于值传递
circle2 := utils.Circle{Radius: 10}
fmt.Printf("Old Radius: %.2f\n", circle2.Radius)
// 调用 SetRadius 方法尝试修改半径
circle2.SetRadius(15)
fmt.Printf("New Radius: %.2f\n", circle2.Radius) // 仍然是 10
}
OK,如果我就想在SetRadius内改变radius呢?那肯定是用指针了,但方法比数组指针简单:将SetRadius中的Circle前加上*即可,因为编译器会自动处理结构体指针,所以在main中的代码无需改动!
对于第三点,receiver的类型是你想要关联的自定义类型,这也就是说,基本类型不能作为方法中的接收类型!读者可自行尝试,这里不做赘述。
对于第四点,首先,在utils内定义String():
func (p Person) String() string {
str := fmt.Sprintf("name=%s,age=%v,sex=%s", p.Name, p.Age, p.Sex)
return str
}
然后在main中调用:
func main() {
p := utils.Person{Name: "李华", Age: 30, Sex: "女"}
fmt.Printf("%v\n", p) // 使用格式化字符串
fmt.Println(p) // 直接打印
}
这个String方法呢,也是定义结构体时常用的,以便输出结构体信息。这里我提一个问题:如果定义了Person的别名,打印其实例还会自动调用String()吗?验证很简单,读者可自行尝试,这里不做赘述。
二、方法与函数的区别
方法 | 函数 | |
---|---|---|
和其他类型的关系 | 绑定到特定类型 | 独立于任何类型 |
语法 | func (receiver ReceiverType) MethodName(parameters) returnType | func FunctionName(parameters) returnType |
调用方式 | 通过类型的实例调用 instance.MethodName() | 直接调用 FunctionName() |
作用域 | 可以访问绑定类型的字段 | 只能访问传入的参数 |
指定类型是否需要和传入类型一致 | 不需要(可以随意传入值或指针) | 需要 |
关于最后一点,首先在utils中定义两个接收者不同的方法:
// 方法:接收者为值类型
func (p Person) PValue() {
fmt.Println("Hello, my name is", p.Name)
}
// 方法:接收者为指针类型
func (p *Person) PPointer() {
fmt.Println("Hello, my name is", p.Name)
}
然后在main文件中调用:
// 函数:必须传入 Person 类型
func PrintPerson(p utils.Person) {
fmt.Println("Person's name:", p.Name)
}
func main() {
// 创建值类型和指针类型的实例
p1 := utils.Person{Name: "李华"}
p2 := &utils.Person{Name: "张三"}
p1.PPointer() // 调用接收者为指针的方法,传入值类型
PrintPerson(p1) // 调用函数,传入值类型
p2.PValue() // 调用接收者为值类型的方法,传入指针
PrintPerson(*p2) // 调用函数,传入解引用的指针
}
程序输出如下:
Hello, my name is 李华
Person's name: 李华
Hello, my name is 张三
Person's name: 张三