返回 登录
1

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

原文:A C++ developer looks at Go (the programming language), Part 2: Modularity and Object Orientation
作者:Murray
翻译:Peter

这是关于评论GO语言的第二部分,以下为整个系列的链接:

第一部分里面就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)中,你现在必须手动编码大量的转发方法来实现此目的,您仍然需要继承某种东西,以告知支持封装接口的类型系统。

评论