2023-11-26 分类: 网站建设
面向对象的编程风格在开发者中非常流行,尤其以C++和Java为代表的编程语言风靡一时!
有趣的是,这两种语言几乎不出所料都是从C语言衍生而来,但是它们不同于C的面向过程编程。这种面向对象的编程风格给开发者带来了极大的方便,解放了劳动,松耦合,高内聚也成为了设计标准,让我们可以更愉快的复制粘贴,成为代码的搬运工。很多第三方工具都是开箱即用的,语义清晰,职责明确,都是面向对象的。编程的好处!
Go 语言也是从 C 语言派生而来的。不知道大家是否也好奇Go语言是否支持面向对象的编程风格?
准确地说,Go 支持面向对象编程,而不是面向对象的语言!
不,它和薛定谔的猫一样不确定吗?
其实这个答案是官方的答案,不是我自己凭空捏造出来的。详情请参考Is Go an-?
为什么这么说?
Go 支持封装,但不支持继承和多态,所以严格按照面向对象规范,Go 语言不是面向对象的编程语言。
然而,Go 提供的接口是一种非常易于处理且更通用的方式。虽然它在表达上与其他主流编程语言略有不同,甚至无法实现多态,但 Go 的接口不仅适用于结构。 body,它也可以应用于任何数据类型,这无疑是非常灵活的!
比较有争议的是继承。由于没有关键字支持继承特性,所以没有继承的痕迹。虽然有一些方法可以将类型嵌入到其他类型中来实现子类化,但这并不是真正的继承。
因此,Go 既支持面向对象的编程风格,又不完全是面向对象的编程语言。
如果换个角度看问题,正是因为没有继承,Go比面向对象的编程语言更轻量级。您可能希望考虑继承特性,子类和父类之间的关系,单继承或多继承。访问控制权限等问题!
按照面向对象的编程规范,实现封装特性的部分应该是类和对象,但是这个概念和实现语言的关键词是分不开的,但是Go没有关键词而是C语言 关键字,所以调用类或者对象不是很合适,所以下面的解释过程还是采用这种结构!
如何定义结构
关键字声明结构,属性之间的回车和换行。
例如下面例子中定义了动态数组结构,下面例子中将使用动态数组结构作为演示对象。
type MyDynamicArray struct {
ptr *[]int
len int
cap int
}
在Go语言中定义对象的多个属性时,用直接换行代替分号来分隔?为什么它与其他主流编程语言不同?
对于习惯了分号结尾的开发者来说,他们可能有一段时间不习惯 Go 的这种语法,所以他们决定探索 Go 编程规范!
如果手动添加分号,编辑器会提示分号重复,所以我猜可能是Go编译器自动添加了分号,用分号作为语句语句的分隔符。手动添加分号后,Go忽略了或者添加了分号,所以报了上面的错误。
这样做有什么好处?
不是自己加分号,编译器无条件加分号的结果,更何况其他主流编程语言都是手动加分号的!
当有多个属性时,可以直接换行,不用加分号作为分隔符。对于从来没有接触过编程语言的小白来说,可能会省事,但是对于有编程经验的开发者来说,要记住不能加分号,真的很吵!
如果多个属性写在一行,则没有换行符。我看你怎么区分它们。这个时候应该用逗号还是分号隔开?
首先,空格不能分隔多个属性,所以试试分号或逗号。
根据提示提示需要分号或换行符,换行符是标准形式,试试分号能不能分开?
此时编辑器不会报错或警告,所以一行上的多个属性应该用分号隔开,这意味着Go编译器识别多个属性还是和其他主流编程语言一样。用数字隔开,但开发者不能用!
和上面的规则类似,记忆很简单,验证也比较容易。难点在于理解为什么?
为什么 Go 是这样设计的?或者如何理解这种设计思想所代表的语义?
Go 作为一种新的编程语言,不仅体现在具体的语法差异上,更重要的是编程思想的特殊性。
就像面向对象中的接口概念一样,设计者只需要定义抽象的行为,并不关心行为的具体实现。
如果我们也用这种思维去理解不同的编程语言,那么就可以通过现象看本质,否则真的很容易陷入语法细节,进而可能会忽略背后的核心思想。
其实对于结构的多属性分隔符,其实不管用什么作为分隔符,逗号或者句号都可以,只要编译器能识别出这是一个不同的属性。
因为大多数主流编程语言一般都是用分号作为分隔符,开发者需要手动写分隔符让编译器识别,但是Go语言不这么认为,算了,直接换行,我也能识别出来out(虽然底层的 Go 编译器在编译时仍然使用分号来表示换行)!
加不加分号,对于开发者来说,只是一个分隔多个属性的标志。如果不加就可以实现,为什么还要加?
这三个基本问题是什么、为什么和如何。如果简单易学、易懂,学什么、怎么学就够了,但这样学、学就难免会出现自治的局面。也就是说,各种编程语言之间没有关系,每种语言都是独立的?!
世界上有千万种语言,编程语言也有很多。学一门新语言不使用旧语言,学一门新语言和春小白有什么区别?
学习就是学习,可惜对旧语言没有帮助,也没有加深对旧语言的理解。这只是对一种全新语言的纯粹学习。
语言是由进化创造的。它不是空中楼阁。它建立在现有系统的基础上,逐步发展和演进。任何新语言或多或少都会找到旧语言的影子。
那何不尝试一下,弄清楚新语言设计的初衷和设计过程中面临的问题,然后再看语言是如何解决问题的。求解的过程称为实现细节。我觉得这种方式应该是更好的学习方式!
虽然你不能在语言设计环境中,也不一定了解语言设计面临的挑战,但先问并试着问为什么,你能不能不这样设计等等,应该是一个好的开始。
所以接下来的文章将采用语义分析的角度,尝试理解Go语言背后的原始设计,并通过大量的辅助测试来验证猜想。不再是简单的知识罗列过程,当然必要的知识归纳还是很重要的,这个自然不会放弃。
既然已经定义了动态数组,也就是设计者的工作暂时告一段落了。作为用户,我们如何使用我们的动态数组?
根据面向对象的术语,从类创建对象的过程称为实例化。但是,我们已经知道 Go 并不是一个完整的面向对象语言,所以为了避免使用面向对象的技术术语尽可能多地引用 Go 的实现细节,我们可以暂时将其理解为结构类型和结构变量。随着以后学习的深入,我们可能会对这部分有更深入的了解。
func TestMyDynamicArray(t *testing.T){
var arr MyDynamicArray
// { 0 0}
t.Log(arr)
}
以上写法没有特别强调。它完全是使用之前文章中介绍过的语法规则来实现的。 var arr 表示声明类型的变量 arr。此时直接打印变量的值,结果为{0 0}。
最后两个值
都是0,自然容易理解,因为我们在Go语言中解释变量的时候已经介绍过了。 Go的变量类型默认初始化有一个对应的0值,而int类型的len cap属性自然是0,而ptr *[]int是数组的指针,所以是nil。
等等,有些不对劲。这里有一个设计错误。明明叫做动态数组,里面的结果是切片的。怎么回事?
先修复这个错误。可以看出,粗心大意的效果太糟糕了,语义发生了变化。我先纠正一下!
我们知道要使用数组,必须指定数组的初始化长度。第一感觉是用cap所代表的容量来初始化*[cap]int数组,但是不行。编辑器提示必须使用整数。
虽然 cap 是一个 int 类型的变量,但内部数组 [cap]int 不识别这个方法。可能是因为这两个变量是一起声明的。 cap 和 (cap)int 都是变量,不能赋值。
那么如果指定了初始化长度,应该指定多少,如果是0,语义上是正确但与实际使用不符,因为这样的话,内部数组就不能按照方法插入了!
所以数组的初始化长度不能为零,解决了无法操作数组的问题,但是语义不正确。因此,在这种情况下,需要维护len和cap这两个变量的值,以确保语义和逻辑正确。 ,其中len代表数组的实际数量,cap代表内部数组的实际分配长度。由于这两个变量非常重要,不应被调用者随意修改,最多只能查看变量的值,所以必须提供一种机制来保护变量的值。
接下来我们尝试用函数封装的思路来完成这个需求,代码实现如下:
type MyDynamicArray struct {
ptr *[10]int
len int
cap int
}
func TestMyDynamicArray(t *testing.T){
var myDynamicArray MyDynamicArray
t.Log(myDynamicArray)
myDynamicArray.len = 0
myDynamicArray.cap = 10
var arr [10]int
myDynamicArray.ptr = &arr
t.Log(myDynamicArray)
t.Log(*myDynamicArray.ptr)
}
var声明结构体变量并设置结构体的基本属性,然后操作内部数组实现对数组的访问修改。
然而,我们犯了一个典型的错误。调用者不应该关注实现细节。这不是打包要做的!
具体的实现细节由设计者完成面向对象编程语言,并将相关数据封装成一个整体,对外提供相应的接口,让调用者可以安全方便地调用。
第一步是封装内部数组相关的两个变量,只对外提供访问接口,不提供设置接口,防止调用者随意修改。
显然这部分应该由函数来实现,所以有如下转换过程。
可惜编辑器直接报错:它必须是类型名或指向类型名的指针。
函数不能放置在结构中。这与 C 系列非常相似,但是像 Java 这样的衍生系列会觉得不可思议。无论如何,这意味着结构只能定义结构而不能定义行为!
那我们把函数移到结构外,但是我们定义的函数名字叫len,而且系统也有len函数,这时候能正常运行吗?让我们拭目以待,眼见为实。
除了函数本身报错,函数内部的len也报错,因为此时函数和结构体还没有建立任何连接。如何访问 len 属性?不报错才怪!
解决这个问题很简单。直接将结构体的指针传递给len函数是不够的,这样在函数内部可以访问结构体的属性。
从设计的角度来看,它确实解决了函数定义的问题,但是用户调用函数的方式看起来与面向对象的编写方式有些不同。
func TestMyDynamicArray(t *testing.T) {
var myDynamicArray MyDynamicArray
t.Log(myDynamicArray)
myDynamicArray.len = 0
myDynamicArray.cap = 10
var arr [10]int
myDynamicArray.ptr = &arr
t.Log(myDynamicArray)
t.Log(*myDynamicArray.ptr)
(*myDynamicArray.ptr)[0] = 1
t.Log(*myDynamicArray.ptr)
t.Log(len(&myDynamicArray))
}
面向对象的方法一般都是通过点操作符来实现的。访问属性或方法,以及我们实现的属性访问。但是方法是函数调用的典型形式吗?这看起来不像是一种方法!
为了让普通函数看起来像面向对象的方法,Go做了如下改动,将当前结构体的变量声明移到函数名的前面,从而实现类似于this或self in 面向语言的效果。
func len(myArr *MyDynamicArray) int {
return myArr.len
}
这时候方法名和参数返回值又报错了。根据提示,函数名和字段名不能相同?
这真的是一件很神奇的事情。有没有可能 Go 无法区分函数和字段?这是未知的。
然后我们要修改函数名,改成面向对象中流行的方法命名规则,如下:
func (myArr *MyDynamicArray) GetLen() int {
return myArr.len
}
让我们简单地谈谈 Go 的可访问性规则。大写字母开头表示公共权限,小写字母开头表示私有权限。 Go 只有这两种类型的权限,这两种权限都是特定于包的。先这样理解就好了。
根据实验中得到的方法规则,继续改进其他方法,补充其他方法。
现在我们已经解决了私有变量的可访问性问题。初始化逻辑没有处理。一般来说,初始化逻辑可以在构造函数中执行。 Go 是否支持构造函数以及如何触发构造函数?功能?
尝试按照其他主流编程语言中构造函数的编写方式来编写Go的构造函数。没想到Go编译器直接报错,提示重新定义了类型,影响了其余部分!
如果修改方法名,理论上可以解决报错问题,但这不是构造函数的样子。 Go 可能不支持构造函数吗?
此时构造函数的面向对象形式转化为自定义函数实现的构造函数。更准确的说,这是一个类似于工厂模式实现的构造函数方法。
func NewMyDynamicArray() *MyDynamicArray {
var myDynamicArray MyDynamicArray
return &myDynamicArray
}
Go 语言真的不支持构造函数吗?
至于是否支持构造函数,或者应该如何支持,真相不明。随着学习的深入,相信以后会有明确的答案。以下是我个人观点的简要表达。
首先我们知道Go的结构体只能定义数据,结构体的方法必须定义在结构体之外。为了符合面向对象的使用习惯,即通过实例对象的点操作符来访问方法。 Go的方法只能是函数的变体,即普通函数指向结构体变量的声明部分,移到函数名前面来实现方法。这种把函数变成方法的模式也符合Go一贯的命名规则:按照人的思维习惯命名,先有输入再输出等逻辑。
结构方法从语法和语义两个维度支持面向对象规范,那么构造函数应该怎么做才能实现面向对象?
顾名思义,构造函数应该是一个函数,而不是一个方法。该方法由指向自身的参数组成。这一点不应包含在构造函数中。否则,应该有对象的实例,并且会构造纱线?
既然构造函数是普通函数,那么按照面向对象的命名约定,方法名应该是结构体名,但是如果你真的操作了,编辑器会直接报错,所以这不符合到面向对象的命名约定!
这样构造函数的名字可能不是结构类型的名字,而是其他特殊的名字。最好能通过名字知道名字,并且有在实例化对象时自动调用的能力。
当然,这个名字取决于 Go 的设计者如何命名。在这里靠猜测很难猜到,否则我就是设计师!
另外,还有一种可能,就是Go没有构造函数。如果要实现构造函数的逻辑,只能另寻他路了。
有什么可靠的依据吗?
我认为这是可能的。构造函数虽然提供了自动初始化的能力,但如果真的在构造函数中加入复杂的初始化逻辑,无疑会增加日后排查的难度,带来一定的用户。阅读障碍,所以在某种程度上,构造函数很可能被滥用!
这是否意味着不需要构造函数?
不能说同样的话。除了基本的变量初始化和简单的逻辑之外,构造函数在实际编程中还有一定的用途。为了避免滥用,直接禁用。有点像喝毒解渴的感觉吧?
因此,我个人的观点是,构造函数的初始化逻辑应该保留,或者可以用其他方式实现,或者干脆放弃构造函数,让编译器自动实现构造函数,就像编译器可以自动添加一样就像多个字段之间的分号。
如果开发者真的需要构造函数,结构体初始化的逻辑总是可以通过工厂模式或者单例模式自定义,所以放弃也可以!
最后,以上纯属个人猜想。不知道Go中有没有构造函数。如果你知道,请清楚地告诉我答案。我个人倾向于没有构造函数,最多只提供类似的构造函数初始化。逻辑!
现在,我们已经封装了结构体的数据,定义了结构体的方法,实现了结构体的工厂函数。那么让我们继续完善动态数组,实现数组的基本操作。
func NewMyDynamicArray() *MyDynamicArray {
var myDynamicArray MyDynamicArray
myDynamicArray.len = 0
myDynamicArray.cap = 10
var arr [10]int
myDynamicArray.ptr = &arr
return &myDynamicArray
}
func TestMyDynamicArray(t *testing.T) {
myDynamicArray := NewMyDynamicArray()
t.Log(myDynamicArray)
}
首先将测试用例中的逻辑提取到工厂函数中。不带参数的工厂函数初始化的默认内部数组长度为10,然后再考虑调用者的规范和动态数组函数的实现,暂时实现最基本的功能。 .
初始化的内部数组都是零值,所以需要先提供外界可以添加的接口,实现如下:
func (myArr *MyDynamicArray) Add(index, value int) {
if myArr.len == myArr.cap {
return
}
if index < 0 || index > myArr.len {
return
}
for i := myArr.len - 1; i >= index; i-- {
(*myArr.ptr)[i+1] = (*myArr.ptr)[i]
}
(*myArr.ptr)[index] = value
myArr.len++
}
由于默认的初始化工厂函数暂时是一个定长数组,所以新元素实际上是一个定长数组,但这并不妨碍动态数组部分的后续实现。
为了方便操作,提供了插入头部和插入尾部两个接口,可以实现更高级的基于动态数组的数据结构。
func (myArr *MyDynamicArray) AddLast(value int) {
myArr.Add(myArr.len, value)
}
func (myArr *MyDynamicArray) AddFirst(value int) {
myArr.Add(0, value)
}
为了测试动态数组的算法是否正确,提供了打印方法查看数组的结构。
可以看出打印方式显示的数据结构和真实的结构数据是一样的,接下来我们更有信心继续封装动态数组!
func (myArr *MyDynamicArray) Set(index, value int) {
if index < 0 || index >= myArr.len {
return
}
(*myArr.ptr)[index] = value
}
func (myArr *MyDynamicArray) Get(index int) int {
if index < 0 || index >= myArr.len {
return -1
}
return (*myArr.ptr)[index]
}
这两个接口比较简单,更新数组指定索引的元素,根据索引查询数组的值。
接下来,我们开始测试动态数组的所有接口!
动态数组暂时告一段落。不知道大家有没有好奇我们为什么用动态数组作为例子来解释面向对象?
其实主要是验证上一篇的猜想,即切片和数组是什么关系?
我认为切片的底层是一个数组,但是语法层面提供了支持,让你看不到数组的阴影。既然仙女已经学会了面向对象,那么就用面向对象的方式来实现切片的功能,虽然不能模拟语法层面的实现,但是功能特性是可以模仿的!
以下是对本文知识点的总结,即封装的实现。
如何封装结构
之所以叫结构体,是因为Go的关键字不是,而且也是面向对象编程风格中唯一支持的特性。不支持继承和多态,我会开一篇文章详细说明。
结构是封装数据的一种手段。结构体只能定义数据,不能定义方法。这些数据有时称为字段,有时称为属性或简称为变量。至于叫什么,也没什么特别的。重要的是,如何命名与环境的语义有关。
type MyDynamicArray struct {
ptr *[10]int
len int
cap int
}
这个结构中有三个变量。变量由换行符而不是分号和换行符分隔。一开始感觉有点奇怪,不过编辑器一般都很聪明。如果你习惯性地加分号,会提示你删除,所以不用在意语法细节。
结构不支持写函数,只支持数据结构,也就是说数据和行为是分离的,两者的关系比较弱。
func (myArr *MyDynamicArray) IsEmpty() bool {
return myArr.len == 0
}
这种方式的功能与普通功能略有不同。包含结构变量的参数被推进到函数名的前面。语义也很清楚。它是指结构的功能。为了区别于普通函数,这种函数被称为方法。
其实就简单的实现函数而言,方法和函数没有区别,无非就是调用者的使用方式!
func IsEmpty(myArr *MyDynamicArray) bool {
return myArr.len == 0
}
之所以采用这种设计方式,一方面是体现了函数的重要性,毕竟在Go语言中它们是一等公民!
另一方面是为了实现面向对象的语法习惯,不管是属性还是方法面向对象编程语言,都用点号调用。操作员。
在官方文档中,这个结构参数被称为接收者,因为数据和行为是弱相关的。发送数据的人是谁?
不言而喻,发送方应该是调用方传递过来的结构体实例对象,结构体变量将数据结构体发送给接收方方法,从而将数据和行为联系在一起。
func TestMyDynamicArray(t *testing.T) {
myDynamicArray := NewMyDynamicArray()
fmt.println(myDynamicArray.IsEmpty())
}
好的,以上就是第一次面向对象体验的所有部分。这只是很小的一部分,我花了三天时间。我想说的是,转变思维不容易,写好文章也不容易。 !
在下一篇文章中,我会继续介绍面向对象的包装特性,讲解更多干货。如果您觉得本文对您有帮助,请转发您的评论,感受您的阅读!
文章标题:,,继承的特性子类Go-?Go
网页链接:/news48/296798.html
成都网站建设公司_创新互联,为您提供标签优化、搜索引擎优化、企业建站、做网站、电子商务、自适应网站
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联
猜你还喜欢下面的内容