找回密码
 立即注册
  • QQ空间
  • 回复
  • 收藏

C++程序员是如何评价GO语言的(模块化和面向对象)

admin 2019-9-9 12:10 91人围观 C++相关



作者丨Murray

翻译丨Peter


这是关于评论GO语言的第二部分,第一部分:C++程序员是如何评价GO的,第三部分会在不日后在CSDN公众号(ID:csdnnews)发布。


在第一部分里面就GO语言的简单功能(特征)做了论述,如常用语法,基本类型等。本文将主要提及GO所支持的package(包)和面向对象。在这之前呢,还是建议读者阅读一下此书,照旧,欢迎各方高人点评和纠错。

总的来说,我发现GO语言面向对象的语法有点乱,一致性差、不明显,所以对于大多数使用场合,个人更倾向于C++明显的继承层次结构。

在这个部分的文章里面故意不提及系统构建,分发或者配置等内容。

Packages(包)

Go代码是以软件包的形式组织的,Java也有包的概念,二者很像,跟C++命名空间也有点类似。 在源文件的开头声明包的名称:


package foo


当需要用到某个包时,用import方式导入:


package bar  //定义了包名

 import (    //告诉Go编译器这个程序需要使用 foo、moo 包(的函数,或其他元素)

 "foo"

  "moo"

)

func somefunc() {

    foo.Yadda()

  var a moo.Thing

      ...

}


包名称应与文件的目录名称匹配。 这是import语句找到对应包的关键。一个目录可允许有多个文件,这些文件都是同一个包的一部分。

package main不受以上规则约束。由于其唯一性,所以对应的目录不需要命名为main。

结构体

在Go语言中可以像C一样声明一个结构体:


type Thing struct {

  // Member fields.

  // Notice the lack of the var keyword.

  a int

  B int // See below about symbol visibility

}

var foo Thing

foo.B = 3

var bar Thing = Thing{3}

var goo *Thing = new(Thing)

goo.B = 5


我习惯使用var关键字演示变量的实际类型,也可能会选择较短的表达式 := 。

请注意,我们可以将其创建为一个值或一个指针(使用内置的new()函数),与C或C ++不同,Go中的结构体所占的实际内存并不能确定是在堆还是栈上。 具体由编译器决定,一般是根据内存是否需要延续功能调用来分配。

以前,我们已经看到内置的make()函数用于实例化slices(切片)和maps(集合)。 make()仅适用于那些内置类型。 对于自定义类型,可以使用new()函数。 我发现这个区别有点混乱,但是我一般不喜欢使用语言本身就可以实现的类型区别。 我喜欢C++标准库如何在C++中实现方式,当往库里面添加内容时,语言本身几乎没有什么特别的支持。

Go类型通常具有“构造函数”(而不是方法),应该调用该函数来正确实例化该类型,但是我认为没有办法强制执行正确的初始化,就像C++或Java中的默认构造函数。 例如:


type Thing struct {

  a int

  name string

  ...

}

func NewThing() *Thing {

  // 100 is a suitable default value for a in this type:

  f := Thing{100, nil}

  return &f

}

// Notice that different "constructors" must have different names,

// because go doesn't have function or method overloading.

func NewThingWithName(name string) *Thing {

  f := Thing{100, name}

  return &f

}


Embedding Structs(嵌套结构体)

可以匿名地将一个结构体“嵌入”到其他结构体中,如下所示:


type Person struct {

   Name string

}

type Employee struct {

  Person

  Position string

}

var a Employee

a.Name = "bob"

a.Position = "builder"  


这感觉有点像C ++和Java中的继承,例如,可以这样:


var e = new(Employee)

// Compilation error.

var p *Person = e

// This works instead.

// So if we thought of this as a cast (we probably shouldn't),

// this would mean that we have to explicitly cast to the base class.

var p *Person = e.Person

// This works.

e.methodOnPerson()

// And this works.

// Name is a field in the contained Person struct.

e.Name = 2

// These work too, but the extra qualification is unnecessary.

e.Person.methodOnPerson()


Methods(方法)

Go语言中的结构体可以有Methods(与结构相关联的函数),这点和C/Java语言的classes(类)很像 , 但在语法方面略有不同。 Method在结构体外被声明,并且通过在函数名之前指定“receiver”来进行调用。 例如,它声明(并实现)Thing结构体的DoSomething方法:


func (t Thing) DoSomething() {

 ...

}


还有需要注意的一点,由于GO没有内置如“self”或“this”的实体名,故必须为receiver指定一个名称。这感觉有点相互矛盾。

可以使用指针替代,而且如果要更改关于struct实例的任何内容,指针是不二选择:


func (t *Thing) ChangeSomething() {

 t.a = 4

}


如果需要保持代码的一致性,最好给method receivers指定为指针类型。

跟C++/Java不同,这允许检查实例是否为nil(Go为null或nullptr),使其可以在null实例上调用方法。 这让我想起Objective-C如何在nil实例上调用方法而没有崩溃,甚至返回一个nil/zero值。 我发现Objective-C没有这一机制,更让我沮丧的是,Go允许这样做,但没有一定的一致性。

与C++或Java不同,GO甚至可以将methods与非struct(非类)类型相关联。 例如:


type Meters int

type Feet int

func (Meters) convertToFeet() (Feet) {

  ...

}

Meters m = 10

f := p.convertToFeet()


没有赋值或比较运算符重载

在C++里面,可将=、 !=、 <、>、等运算符重载,所以可以使用这些常规的运算符,使代码看起来更整洁:


MyType a = getSomething();

MyType b = getSomethingElse();

if (a == b) {

  ...

}


在Go语言中不能这么使用, 只有部分内建类型是可比较的,如数字类型,字符串,指针或通道,或由这些类型组成的结构体或数组。当处理接口时这会是一个麻烦,我们稍后会看到。

符号可见性: 大写或小写字母

以大写字母开头的符号 (类型、函数、变量) 可从包外部获得。结构方法和以大写字母开头的成员变量可从结构外部获得。否则,它们就是私有的包或结构。例如:


type Thing int // This type will be available outside of the package.

var Thingleton Thing// This variable will be available outside of the package.

type thing int // Not available outside of the package.

var thing1 thing // Not available outside of the package.

var thing2 Thing // Not available outside of the package.

// Available outside of the package.

func DoThing() {

 ...

}

// Not available outside of the package.

func doThing() {

 ...

}

type Stuff struct {

  Thing1 Thing // Available outside of the package.

     thing2 Thing // "private" to the struct.

}

// Available outside of the struct.

func (s Stuff) Foo() {

 ...

}

// Not available outside of the struct.

func (s Stuff) bar() {

  ...

}

// Not available outside of the package.

type localstuff struct {

...

}


感觉这有点奇怪。相对而言, c++和Java中明确用public和private关键字声明显得更加友善。

Interfaces(接口)

有方法的接口

如果两个Go类型满足一个接口,那它们都具有该接口的方法, 这与Java接口类似。 Go接口也有点像C ++中的一个完全抽象类(只有纯虚方法),跟C ++ Concept(概念)也很像(自C ++ 17)。

例如:


type Shape interface {

  // The interface's methods.

  // Note the lack of the func keyword.

  SetPosition(x int, y int)

  GetPosition() (x int, y int)

  DrawOnSurface(s Surface)

}

type Rectangle struct {

  ...

}

// Methods to satisfy the Shape interface.

func (r *Rectangle) SetPosition(x int, y int) {

  ...

}

func (r *Rectangle) GetPosition() (x int, y int) {

  ...

}

func (r *Rectangle) DrawOnSurface(s Surface) {

   ...

}

// Other methods:

func (r *Rectangle) setCornerType(c CornerType) {

   ...

}

func (r *Rectangle) cornerType() (CornerType) {

   ...

}

type Circle struct {

  ...

}

// Methods to satisfy the Shape interface.

func (c *Circle) SetPosition(x int, y int) {

  ...

}

func (c *Circle) GetPosition() (x int, y int) {

  ...

}

func (c *Circle) DrawOnSurface(s Surface) {

  ...

}

// Other methods:

...


然后,就可以使用接口类型而不是特定的 “实际” 类型:


var someCircle *Circle = new(Circle)

var s Shape = someCircle

s.DrawOnSurface(someSurface)


请注意,这里使用的是Shape, 而不是使用Shape(指向Shape的指针), 即使是从 Circle(指向circle)转换。 “接口值”似乎是隐式指针,这似乎是不必要的混淆。 如果指向接口的指针只是具有与这些“接口值”相同的行为,即使语言禁止使用指针的接口类型, 它也会更加一致。

隐式满足接口类型

但是,没有明确声明类型应实现接口。

通过这种方式, 接口就像C++的概念, 虽然C++概念是纯编译时功能, 用于通用 (模板) 代码。您的类可以符合 c++ 概念,而无需具体声明。因此, 与 go 接口一样, 如果必须, 您可以使用现有类型而不更改它。

编译器仍需检查类型是否兼容, 但可能是检查类型的方法链表, 而不是检查类层次结构或已实现接口的链表。例如:


var a *Circle = new(Circle)

var b Shape = a // OK. The compiler can check that Circle has Shape's methods.


像 c++ 的dynamic_cast一样,GO 也可以在运行时检查。例如,可以检查一个接口值是否引用一个同时满足另一个接口的实例:


// Sometimes the Shape (our interface type) is also a Drawable

// (another interface type), sometimes not.

var a Shape = Something.GetShape()

// Notice that we want to cast to a Drawable, not a *Drawable,

// because Drawable is an interface.

var b = a.(Drawable) // Panic (crash) if this fails.

var b, ok = a.(Drawable) // No panic.

if ok {

  b.DrawOnSurface(someSurface)

}


或者,可以检查接口值是否特指某种具体类型。例如:


// Get Shape() returns an interface value.

// Shape is our interface.

var a Shape = Something.GetShape()

// Notice that we want to cast to a *Thing, not a Thing,

// because Thing is a concrete type, not an interface.

var b = a.(*Thing) // Panic (crash) if this fails.

var b, ok = a.(*Thing) // No panic.

if ok {

  b.DoSomething()

}


Runtime调用

接口方法也类似于 c++ 虚方法 (或java 方法), 接口变量也类似于多态基类的实例。为了通过接口变量实际调用接口的方法, 程序需要在运行时检查其实际类型, 并调用该类型的特定方法。也许,与 c++ 一样,编译器有时可以优化掉这种间接寻址。

这显然不如直接调用 c++ 模板中的模板化类型在编译时标识的方法那样有效。但它显然是简单得多。

比较接口

接口值有时可以比较, 但这似乎是一个危险的业务。接口值为:


  • 类型不同,则不相等。

  • 类型相同,只有一个为nil,不相等。

  • 类型相同,可比较,并且它们的值一样,则相等。


但是,如果类型是相同的,但这些类型是不可比较的, 将导致Go在运行时抛出异常 “panic”。(译者注:panic 是用来表示非常严重的不可恢复的错误的。在Go语言中这是一个内置函数,接收一个interface{}类型的值(也就是任何值了)作为参数。panic的作用就像我们平常接触C++的异常)

希望实现关键字

在C ++中,如果你愿意,可以显式声明一个类应符合该概念,或者你可以从一个基类中显示的派生出来,而在Java中,必须使用“implements”关键字。由于GO语言没有此机制,因此需要习惯。我想要这些声明来记录我的架构,根据他们的一般目的明确地显示我的“具体”类的预期,而不是仅仅用一些其他代码来表达它们。没有这个感觉很脆弱。

该书建议将这个笨拙的代码放在某处,以检查一个类型是否真正实现了一个接口。注意_(下划线)的使用意味着我们不需要为结果保留一个命名变量。


var _ MyInterface =(* MyType)(nil)


如果类型不满足接口,转换是不可能的,编译器应该报错。作为最初级测试,我认为这是明智之举。特别是如果您的包提供的类型,不是真正使用的包本身。对于我来说, 这是一个很糟糕的替代品,它使用特定的语言构造对类型本身进行明显的编译时检查。

接口嵌入

在接口中嵌入接口

GO不具有继承层次结构的概念, 但您可以在一个接口中 “嵌入”另 一个接口, 以指示满足一个接口的类也满足另一个接口。例如:


type Positionable interface {

  SetPosition(x int, y int)

  GetPosition() (x int, y int)

}

type Drawable interface {

  drawOnSurface(s Surface) }

}

type Shape interface {

  Positionable

  Drawable

}


为了满足Shape接口,任何类型也必须满足Drawable和Positionable接口。 因此,任何满足Shape接口的类型都可以与Drawable或Positionable接口关联的方法使用。 这有点像一个java接口扩展另一个接口。

在结构体中嵌入一个满足接口的结构体

我们早些时候就看到了如何将一个结构嵌入另一个匿名结构体中。如果包含的struct实现了一个接口, 则包含的struct也可以实现该接口, 而不需要手动实现的转发方法。例如:


type Drawable interface {

drawOnSurface(s Surface)

}

type Painter struct {

  ...

}

// Make Painter satisfy the Drawable interface.

func (p *Painter) drawOnSurface(s Surface) {

  ...

}

type Circle struct {

 // Make Circle satisfy the Drawable interface via Painter.

 Painter

 ...

}

func main() {

  ...

  var c *Circle = new(Circle)

  // This is OK.

  // Circle satisfies Drawable, via Painter

  c.drawOnSurface(someSurface)

  // This is also OK.

  // Circle can be used as an interface value of type Drawable, via Painter.

  var d Drawable = c

  d.drawOnSurface(someSurface)

}


再一次感觉有点像继承

我实际上非常喜欢匿名地包含结构体的(接口)结构影响父结构的接口,即使是Go的异样接口系统,尽管我希望语法对于发生的事情更加明显。在C++中有类似的东西可能很好。封装而不是继承(和Decorator模式)是一种非常高效的技术,C ++通常会尝试以多种方式进行操作,而不会对最好的方式有所了解,尽管这本身就会成为复杂性的来源。但是在C++(和Java)中,你现在必须手动编码大量的转发方法来实现此目的,您仍然需要继承某种东西,以告知支持封装接口的类型系统。

----------------------------------------------------------------------------------------------------------------------
我们尊重原创,也注重分享,文章来源于微信公众号:CSDN,建议关注公众号查看原文。如若侵权请联系qter@qter.org。
----------------------------------------------------------------------------------------------------------------------

鲜花

握手

雷人

路过

鸡蛋

yafeilinux和他的朋友们微信公众号二维码

微信公众号

专注于Qt嵌入式Linux开发等。扫一扫立即关注。

Qt开源社区官方QQ群二维码

QQ交流群

欢迎加入QQ群大家庭,一起讨论学习!

我有话说......