【Go语言圣经】第六节:方法

第六章:方法

6.1 方法声明

在函数声明时,在其名字之前放上一个变量,这就是声明了变量对应类型的一个方法,相当于为这种类型定义了一个独占的方法。

下例为 Point 类型声明了计算两个点之间距离的方法:

package mainimport "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)
}

上述代码中的 p,叫做方法的接收器(receiver)。在 Golang 当中,不会像其他语言那样使用 this 或 self 作为接收器,可以任意选择作为接收器的名字

下例尝试使用方法:

package mainimport ("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, 5}distance := p.Distance(q)fmt.Println(distance)
}

需要注意的是,此处的p.Distance是方法的调用,我们当然可以声明一个Distance同名函数,接受pq作为参数,计算二者之间的距离。函数和方法是不冲突的,可以同时调用。

但是需要注意的话,如果我们声明了一个名为X的方法,编译器会报错,因为产生了歧义,因为X已经是Point的成员变量了。

下例定义了一个 Path 类型,Path 代表一个线段的集合,并且同样为 Path 定义一个名为 Distance 的方法:

type Path []Pointfunc (path Path) Distance() float64 {sum := 0.0for i := range path {if i > 0 {sum += path[i - 1].Distance((path[i]))}}return sum
}

Path 是一个命名的 slice 类型,但是我们仍然可以为其定义方法。在 Golang 当中,我们可以给同一个包内的任意命名类型定义方法,只要这个命名类型的底层类型不是指针或 interface。

下例调用新方法,计算三角形周长:

func main() {perim := Path{{1, 1},{5, 1},{5, 4},{1, 1},}fmt.Println(perim.Distance())
}

6.2 基于指针对象的方法

当调用一个函数时,会对其每一个参数值进行拷贝,如果一个函数需要更新一个变量,或者函数的其中一个参数是在太大我们希望能够避免进行这种默认的拷贝,该情况下我们就需要用到指针了:

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

上述方法的名字是(*Point).ScaleBy,括号是必须的,没有括号的话这个表达式可能会被理解为*(Point.ScaleBy),即类型方法的指针。

在现实的程序当中,一般会约定,如果 Point 这个类有一个指针作为接收器的方法,那么所有 Point 的方法都必须有一个指针接收器,即使是那些并不需要这个指针接收器的函数

只有类型(比如 Point)和指向它们的指针(*Point),才可能出现在接收器声明里的两种接收器。此外,为了避免歧义,在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器当中的,比如:

type P *int
func (P) f() { /* ... ... ... */ }	// Compile Error

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

func main() {r := &Point{1, 2}r.ScaleBy(2)	fmt.Println(*r)	// {2, 4}
}

或者:

p := Point{1, 2}
(&p).ScaleBy(2)

不过上述两种方法稍显笨拙,Golang 本身在这种地方会帮助我们。如果接收器 p 是一个 Point 类型的变量,并且其方法需要一个 Point 指针作为接收器,可以用下面这种简短的写法:

p.ScaleBy(2)

编译器会隐式地帮助我们用&p去调用 ScaleBy 方法。这种简化写法只适用于“变量”,包括 struct 里的字段,比如p.X,以及 array 和 slice 内的元素。不能通过一个无法取到地址的接收器来调用指针方法,比如临时变量的内存地址无法获取

Point{1, 2}.ScaleBy(2)	// Compile Error

我们可以用一个*Point这样的接收器来调用 Point 的方法,因为我们可以通过地址找到这个变量,只要用解引用符号*获取变量即可。同样地,如果是一个指针想要调用其所指对象类型的非指针接收器方法时,Golang 的编译器会为我们隐式地插入解引用符:

pptr.Distance(q)
// 等价于
(*pptr).Distance(q)

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

就像一些函数允许 nil 指针作为参数一样,方法理论上也可以使用 nil 指针作为接收器,尤其当 nil 对于对象来说是合法的零值时,比如 map 或 slice。在下面的简单 int 链表的例子里,nil 代表的是空链表:

type IntList struct {Value intTail *IntList
}
func (list *IntList) Sum() int {if list == nil {return 0}return list.Value + list.Tail.Sum()
}	// 使用递归的方法计算链表值的综合

不必担心此时 list 是否为 nil,就算 list 为 nil,它也可以调用 IntList 的方法。

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

下例定义了一个 ColoredPoint 类型:

package mainimport "image/color"type Point struct {X, Y float64
}type ColoredPoint struct {PointColor color.RGBA
}

我们完全可以将 ColoredPoint 定义为一个有三个字段的 struct,但是我们却将 Point 这个类型嵌入到了 ColoredPoint 来提供 X 和 Y 这两个字段。

结构当中内嵌其它结构可以使我们在定义 ColoredPoint 时得到一种句法上的简写形式,并使其包含 Point 类型所具有的一切字段,然后再定义一些自己的。我们可以直接认为通过嵌入的字段就是 ColoredPoint 自身的字段。

对于 Point 中的方法我们也有类似的用法,可以将 ColoredPoint 类型当作接收器来调用 Point 里的方法,即使 ColoredPoint 里没有声明这些方法:

func main() {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)q.ScaleBy(2)fmt.Println(p.Distance(q.Point))
}

Point 类的方法也被引入到了 ColoredPoint 当中。一个 ColoredPoint 并不是一个 Point,但它 “has a Point”,并且它有从 Point 类里引入的 Distance 和 ScaleBy 方法。

在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前类型当中。添加这一层间接关系让我们可以共享通用的结构并动态地改变对象之间的关系:

type ColoredPoint struct {*PointColor color.RGBA
}func main() {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))q.Point = p.Point // p 和 q 共享同一个 Pointp.ScaleBy(2)fmt.Println(*p.Point, *q.Point)
}

一个 struct 类型也可以有多个匿名字段,比如:

type ColoredPoint struct {Pointcolor.RGBA
}

6.4 方法值和方法表达式

执行一个方法常见的形式是p.Distance(),实际上这一步可以被拆分为两步来执行。p.Distance叫做“选择器”,选择器返回一个值,这个值是将方法Point.Distance绑定到特定接受其变量的函数。

这个函数可以不通过指定其接收器即可被调用,原因在于p.Distance这条语句已经将Distance方法与p绑定了。只需要传入函数的参数即可:

P := Point{1, 2}
q := Point{4, 6}distanceFromP := p.Distance
fmt.Println(distanceFromP(q))
var origin Point
fmt.Println(distanceFromP(origin))

在一个包的API需要一个函数值,且调用方法希望操作的是某一个绑定了对象的方法的话,方法“值”会非常有用。例如,下例中的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)

与调用一个普通的函数相比,调用一个方法时,必须要用选择器语法来指定方法的接收器。当 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)”

下例是一个更复杂的例子:

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 []Pointfunc (path Path) TranslateBy(offset Point, add bool) {var op func(p, q Point) Pointif add {op = Point.Add} else {op = Point.Sub}for i := range path {path[i] = op(path[i], offset)}
}

6.6 封装

“封装”指的是一个对象的变量或方法对调用方是不可见的,是 OOP 最关键的一个概念。

Golang 只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种限制包内成员的方式同样适用于 struct 或一个类型的方法。因而如果我们想要封装一个对象,必须将其定义为一个 struct。

一个例子如下:

type IntSet struct {words []uint64
}

当然我们也可以直接将 IntSet 定义为 slice 类型,这样我们就需要把代码中所有方法里的s.word*s替换掉:

type IntSet []uint64

上述这种基于名字的手段使得在 Golang 中最小的封装单元是 package,而不是像其他语言一样的类型。一个 struct 类型的字段对同一个包的所有代码都有可见性,无论你的代码是写在一个函数还是一个方法里。

封装最重要的优点是阻止了外部调用方对对象内部的值进行修改:

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。

注意:Golang 的编码风格不禁止直接导出字段。当然,一旦进行了导出,就没有办法在保证 API 兼容的情况下去除对其的导出,所以在一开始的选择一定要经过深思熟虑并且要考虑到包内部的一些不变量的保证,未来可能的变化,以及调用方的代码质量是否会因为包的一点修改而变差。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/894282.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

亚博microros小车-原生ubuntu支持系列:18 Cartographer建图

Cartographer简介 Cartographer是Google开源的一个ROS系统支持的2D和3D SLAM(simultaneous localization and mapping)库。基于图优化(多线程后端优化、cere构建的problem优化)的方法建图算法。可以结合来自多个传感器&#xff0…

安卓(android)实现注册界面【Android移动开发基础案例教程(第2版)黑马程序员】

一、实验目的(如果代码有错漏,可查看源码) 1.掌握LinearLayout、RelativeLayout、FrameLayout等布局的综合使用。 2.掌握ImageView、TextView、EditText、CheckBox、Button、RadioGroup、RadioButton、ListView、RecyclerView等控件在项目中的…

爬虫基础(四)线程 和 进程 及相关知识点

目录 一、线程和进程 (1)进程 (2)线程 (3)区别 二、串行、并发、并行 (1)串行 (2)并行 (3)并发 三、爬虫中的线程和进程 &am…

自签证书的dockerfile中from命令无法拉取镜像而docker的pull命令能拉取镜像

问题现象: docker pull images拉取镜像正常 dockerfile中的from命令拉取镜像就会报出证书错误。报错信息如下: [bjxtbwj-kvm-test-jenkins-6-243 ceshi_dockerfile]$ docker build . [] Building 0.4s (3/3) FINISHED …

计算机网络 IP 网络层 2 (重置版)

IP的简介: IP 地址是互联网协议地址(Internet Protocol Address)的简称,是分配给连接到互联网的设备的唯一标识符,用于在网络中定位和通信。 IP编制的历史阶段: 1,分类的IP地址: …

面对企业文件交换难题,镭速跨网文件交换系统是如何解决的?

在当今这个数字化快速发展的时代,企业越来越依赖于数据交换来维持其业务运作。无论是内部网络之间的沟通还是与外部合作伙伴的数据共享,高效且安全的跨网文件交换都显得尤为重要。然而,在实际操作中,许多企业面临着各种各样的挑战…

Many Whelps! Handle It! (10 player) Many Whelps! Handle It! (25 player)

http://db.nfuwow.com/80/?achievement4403 http://db.nfuwow.com/80/?achievement4406 最少扣你50DKP! 第二阶段 当奥妮克希亚升空后,在10秒内引出50只奥妮克希亚雏龙,随后击败奥妮克希亚。 World of Warcraft [CLASSIC][80猎人][Grandel][最少扣你5…

自制虚拟机(C/C++)(一、分析语法和easyx运用,完整虚拟机实现)

网上对虚拟机的解释很多,其实本质就一句话 虚拟机就是机器语言解释器 我们今天要实现汇编语言解释器,下一次再加上ndisasm反汇编器就是真正虚拟机了 注:这里的虚拟机指的是VMware一类的,而不是JVM,python一样的高级语言解释器 …

36. printf

1. printf 格式化函数说的是 printf、 sprintf 和 scanf 这样的函数,分为格式化输入和格式化输出两类函数。学习 C 语言的时候常常通过 printf 函数在屏幕上显示字符串,通过 scanf 函数从键盘获取输入。这样就有了输入和输出了,实现了最基本…

实验八 JSP访问数据库

实验八 JSP访问数据库 目的: 1、熟悉JDBC的数据库访问模式。 2、掌握使用My SQL数据库的使用 实验要求: 1、通过JDBC访问mysql数据,实现增删改查功能的实现 2、要求提交实验报告,将代码和实验结果页面截图放入报告中 实验过程&a…

python学opencv|读取图像(四十六)使用cv2.bitwise_or()函数实现图像按位或运算

【0】基础定义 按位与运算:全1取1,其余取0。按位或运算:全0取0,其余取1。 【1】引言 前序学习进程中,已经对图像按位与计算进行了详细探究,相关文章链接如下: python学opencv|读取图像&…

使用vhd虚拟磁盘安装两个win10系统

使用vhd虚拟磁盘安装两个win10系统 前言vhd虚拟磁盘技术简介准备工具开始动手实践1.winX选择磁盘管理2.选择“操作”--“创建VHD”3.自定义一个位置,输入虚拟磁盘大小4.右键初始化磁盘5.选择GPT分区表格式6.右键新建简单卷7.给卷起个名字,用于区分8.打开…

基于云计算、大数据与YOLO设计的火灾/火焰目标检测

摘要:本研究针对火灾早期预警检测需求,采用在Kaggle平台获取数据、采用云计算部署的方式,以YOLOv11构建模型,使用云计算服务器训练模型。经训练,box loss从约3.5降至1.0,cls loss从约4.0降至1.0&#xff0c…

计算机毕业设计Python+CNN卷积神经网络考研院校推荐系统 考研分数线预测 考研推荐系统 考研爬虫 考研大数据 Hadoop 大数据毕设 机器学习

温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…

小程序-基础加强-自定义组件

前言 这次讲自定义组件 1. 准备今天要用到的项目 2. 初步创建并使用自定义组件 这样就成功在home中引入了test组件 在json中引用了这个组件才能用这个组件 现在我们来实现全局引用组件 在app.json这样使用就可以了 3. 自定义组件的样式 发现页面里面的文本和组件里面的文…

docker安装emqx

emqx安装 拉取emqx镜像 docker pull emqx/emqx:v4.1.0 运行docker容器 docker run -tid --name emqx -p 1883:1883 -p 8083:8083 -p 8081:8081 -p 8883:8883 -p 8084:8084 -p 18083:18083 emqx/emqx:v4.1.0 放行端口 1、如果要是自己的虚拟机,并且关闭了防火墙&a…

【4Day创客实践入门教程】Day4 迈向高手之路——进一步学习!

Day4 迈向高手之路——进一步学习! 目录 Day4 迈向高手之路——进一步学习!更多的开发板外壳制作 Day0 创想启程——课程与项目预览Day1 工具箱构建——开发环境的构建Day2 探秘微控制器——单片机与MicroPython初步Day3 实战演练——桌面迷你番茄钟Day4…

深度学习之“缺失数据处理”

缺失值检测 缺失数据就是我们没有的数据。如果数据集是由向量表示的特征组成,那么缺失值可能表现为某些样本的一个或多个特征因为某些原因而没有测量的值。通常情况下,缺失值由特殊的编码方式。如果正常值都是正数,那么缺失值可能被标记为-1…

日志收集Day007

1.配置ES集群TLS认证: (1)elk101节点生成证书文件 cd /usr/share/elasticsearch ./bin/elasticsearch-certutil cert -out config/elastic-certificates.p12 -pass "" --days 3650 (2)elk101节点为证书文件修改属主和属组 chown elasticsearch:elasticsearch con…

arm-linux-gnueabihf安装

Linaro Releases windows下打开wsl2中的ubuntu,资源管理器中输入: \\wsl$gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf.tar.xz 复制到/home/ark01/tool 在 Ubuntu 中创建目录: /usr/local/arm,命令如下: …