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

第六章:方法

6.1 方法声明

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

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

package main

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

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

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

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

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
}

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 *= factor
	p.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 int
	Tail *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 main

import "image/color"

type Point struct {
	X, Y float64
}

type ColoredPoint struct {
	Point
	Color 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 {
    *Point
    Color 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 共享同一个 Point
	p.ScaleBy(2)
	fmt.Println(*p.Point, *q.Point)
}

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

type ColoredPoint struct {
	Point
	color.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 []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 {
		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 兼容的情况下去除对其的导出,所以在一开始的选择一定要经过深思熟虑并且要考虑到包内部的一些不变量的保证,未来可能的变化,以及调用方的代码质量是否会因为包的一点修改而变差。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

泰山派Linux环境下自动烧录脚本(EMMC 2+16G)

脚本名字: download.sh 输入./download -h获取帮助信息 ,其中各个IMG/TXT烧录的地址和路径都在前几行修改即可 #!/bin/bash# # DownLoad.sh 多镜像烧录脚本 # 版本:1.1 # 作者:zhangqi # 功能:通过参数选择烧录指定镜…

使用开源项目:pdf2docx,让PDF转换为Word

目录 1.安装python 2.安装 pdf2docx 3.使用 pdf2docx 转换 PDF 到 Word pdf2docx:GitCode - 全球开发者的开源社区,开源代码托管平台 环境:windows电脑 1.安装python Download Python | Python.org 最好下载3.8以上的版本 安装时记得选择上&#…

一、TensorFlow的建模流程

1. 数据准备与预处理: 加载数据:使用内置数据集或自定义数据。 预处理:归一化、调整维度、数据增强。 划分数据集:训练集、验证集、测试集。 转换为Dataset对象:利用tf.data优化数据流水线。 import tensorflow a…

设计模式 - 行为模式_Template Method Pattern模板方法模式在数据处理中的应用

文章目录 概述1. 核心思想2. 结构3. 示例代码4. 优点5. 缺点6. 适用场景7. 案例:模板方法模式在数据处理中的应用案例背景UML搭建抽象基类 - 数据处理的 “总指挥”子类定制 - 适配不同供应商供应商 A 的数据处理器供应商 B 的数据处理器 在业务代码中整合运用 8. 总…

计算图 Compute Graph 和自动求导 Autograd | PyTorch 深度学习实战

前一篇文章,Tensor 基本操作5 device 管理,使用 GPU 设备 | PyTorch 深度学习实战 本系列文章 GitHub Repo: https://github.com/hailiang-wang/pytorch-get-started PyTorch 计算图和 Autograd 微积分之于机器学习Computational Graphs 计算图Autograd…

C++11详解(一) -- 列表初始化,右值引用和移动语义

文章目录 1.列表初始化1.1 C98传统的{}1.2 C11中的{}1.3 C11中的std::initializer_list 2.右值引用和移动语义2.1左值和右值2.2左值引用和右值引用2.3 引用延长生命周期2.4左值和右值的参数匹配问题2.5右值引用和移动语义的使用场景2.5.1左值引用主要使用场景2.5.2移动构造和移…

Spring Boot常用注解深度解析:从入门到精通

今天,这篇文章带你将深入理解Spring Boot中30常用注解,通过代码示例和关系图,帮助你彻底掌握Spring核心注解的使用场景和内在联系。 一、启动类与核心注解 1.1 SpringBootApplication 组合注解: SpringBootApplication Confi…

生成式AI安全最佳实践 - 抵御OWASP Top 10攻击 (下)

今天小李哥将开启全新的技术分享系列,为大家介绍生成式AI的安全解决方案设计方法和最佳实践。近年来生成式 AI 安全市场正迅速发展。据IDC预测,到2025年全球 AI 安全解决方案市场规模将突破200亿美元,年复合增长率超过30%,而Gartn…

git:恢复纯版本库

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的,可以在任何平台上使用。 源码指引:github源…

蓝桥杯python基础算法(2-1)——排序

目录 一、排序 二、例题 P3225——宝藏排序Ⅰ 三、各种排序比较 四、例题 P3226——宝藏排序Ⅱ 一、排序 (一)冒泡排序 基本思想:比较相邻的元素,如果顺序错误就把它们交换过来。 (二)选择排序 基本思想…

python学opencv|读取图像(五十四)使用cv2.blur()函数实现图像像素均值处理

【1】引言 前序学习进程中,对图像的操作均基于各个像素点上的BGR值不同而展开。 对于彩色图像,每个像素点上的BGR值为三个整数,因为是三通道图像;对于灰度图像,各个像素上的BGR值是一个整数,因为这是单通…

Slint的学习

Slint是什么 Slint是一个跨平台的UI工具包,支持windows,linux,android,ios,web,可以用它来构建申明式UI,后端代码支持rust,c,python,nodejs等语言。 开源地址:https://github.com/slint-ui/slint 镜像地址:https://kkgithub.com/…

惰性函数【Ⅱ】《事件绑定的自我修养:从青铜到王者的进化之路》

【Ⅱ】《事件绑定的自我修养:从青铜到王者的进化之路》 1. 代码功能大白话(给室友讲明白版) // 青铜写法:每次都要问浏览器"你行不行?" function addEvent青铜版(element, type, handler) {if (window.add…

Unity飞行代码 超仿真 保姆级教程

本文使用Rigidbody控制飞机,基本不会穿模。 效果 飞行效果 这是一条优雅的广告 如果你也在开发飞机大战等类型的飞行游戏,欢迎在主页搜索博文并参考。 搜索词:Unity游戏(Assault空对地打击)开发。 脚本编写 首先是完整代码。 using System.Co…

基于微信小程序的私家车位共享系统设计与实现(LW+源码+讲解)

专注于大学生项目实战开发,讲解,毕业答疑辅导,欢迎高校老师/同行前辈交流合作✌。 技术范围:SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:…

C++编程语言:抽象机制:模板(Bjarne Stroustrup)

目录 23.1 引言和概观(Introduction and Overview) 23.2 一个简单的字符串模板(A Simple String Template) 23.2.1 模板的定义(Defining a Template) 23.2.2 模板实例化(Template Instantiation) 23.3 类型检查(Type Checking) 23.3.1 类型等价(Type Equivalence) …

多线程的常用方法

getName和setName方法 注意点 setName方法最好放在线程启动之前 最好在线程启动之前修改名字,因为线程启动之后,如果执行过快的话,那么在调用 setName() 之前线程可能就已经结束了 MyThread t1 new MyThread("haha"); t1.setNa…

C++继承的基本意义

文章目录 一、继承的本质和原理二、重载、隐藏和覆盖三、基类与派生类的转换 一、继承的本质和原理 继承的本质:a. 代码的复用 b. 类和类之间的关系: 组合:a part of… 一部分的关系 继承:a kind of… 一种的关系 总结&#xff…

简单易懂的倒排索引详解

文章目录 简单易懂的倒排索引详解一、引言 简单易懂的倒排索引详解二、倒排索引的基本结构三、倒排索引的构建过程四、使用示例1、Mapper函数2、Reducer函数 五、总结 简单易懂的倒排索引详解 一、引言 倒排索引是一种广泛应用于搜索引擎和大数据处理中的数据结构,…

FinRobot:一个使用大型语言模型的金融应用开源AI代理平台

“FinRobot: An Open-Source AI Agent Platform for Financial Applications using Large Language Models” 论文地址:https://arxiv.org/pdf/2405.14767 Github地址:https://github.com/AI4Finance-Foundation/FinRobot 摘要 在金融领域与AI社区间&a…