要真正理解“面向对象”的含义,我们需要回顾一下这个概念的起源。第一个面向对象的语言 simula 出现在 1960 年代。它介绍了对象、类、继承和子类、虚拟方法、协程等等。也许最重要的是,它引入了数据和逻辑完全独立的思维范式转变。
虽然您可能不熟悉 Simula,但您无疑熟悉将 Simula 称为灵感的语言,包括 Java、C++、C# 和 Smalltalk,它们反过来又是 Objective C、Python、Ruby、Javascript、Scala 的灵感来源、PHP、Perl……当今使用的几乎所有流行语言的名副其实的列表。这种思维转变已经占据了主导地位,以至于今天大多数程序员从未以任何其他方式编写代码。
由于不存在标准定义,为了我们讨论的目的,我们将提供一个定义。
面向对象的系统不是将程序构建为代码和数据,而是使用“对象”的概念将两者集成在一起。对象是具有状态(数据)和行为(代码)的抽象数据类型。
也许作为初始实现具有继承性和多态性的结果,几乎所有派生类都采用了这一特性,面向对象编程的定义通常也包括这些特性作为需求。
我们将看看 Go 是如何处理对象、多态和继承的,并让您做出自己的结论。
Go 中的对象
Go 没有一个叫做“对象”的东西,但“对象”只是一个表示意义的词。 重要的是意义,而不是术语本身。
虽然 Go 没有称为“对象”的类型,但它确实有一个类型,它与集成了代码和行为的数据结构的相同定义相匹配。 在 Go 中,这称为 结构体。
“struct”是一种包含命名字段和方法的类型。
让我们用一个例子来说明这一点:
type rect struct { width int height int } func (r *rect) area() int { return r.width * r.height } func main() { r := rect{width: 10, height: 5} fmt.Println("area: ", r.area()) }
点击运行示例,可以在线运行上面代码查看结果。
我们可以在这里谈论很多。最好逐行浏览代码并解释发生了什么。
第一个块定义了一种称为“rect”的新类型。这是一个结构体类型。该结构体有两个字段,都是 int 类型。
下一个块是定义一个绑定到这个结构体的方法。这是通过定义一个函数并将其附加(绑定)到一个矩形来实现的。从技术上讲,在我们的示例中,它实际上附加到一个指向 rect 的指针。虽然该方法绑定到该类型,但 Go 要求我们使用该类型的值来进行调用,即使该值是该类型的零值(在结构体的情况下,零值是 nil)。
最后一个块是我们的 main 函数。第一行创建一个 rect 类型的值。我们可以使用其他语法来做到这一点,但这是最惯用的方式。第二行将在我们的 rect 'r' 上调用 area 函数并打印结果。
我们还没有做什么?在大多数面向对象的语言中,我们将使用“class”关键字来定义我们的对象。使用继承时,最好为这些类定义接口。在这样做时,我们将定义一个继承层次树(在单继承的情况下)。
另外值得注意的是,在 Go 中,任何命名类型都可以有方法,而不仅仅是结构体。例如,我可以定义一个类型为 int 的新类型“Counter”并在其上定义方法。
继承与多态
有几种不同的方法来定义对象之间的关系。 虽然它们彼此有很大不同,但作为代码重用的机制,它们都有一个共同的目的。
- 继承
- 多重继承
- 子类型(多态)
- 对象组合
单继承和多继承
继承是指一个对象基于另一个对象,使用相同的实现。存在两种不同的继承实现。它们之间的根本区别在于一个对象是可以从单个对象继承还是从多个对象继承。这是一个看似很小的区别,但具有很大的影响。单继承的层次结构是一棵树,而多继承的层次结构是一个格子。单继承语言包括 PHP、C#、Java 和 Ruby。多继承语言包括 Perl、Python 和 C++。
子类型(多态)
在某些语言中,子类型和继承是交织在一起的,以至于如果您的特定观点来自一种它们紧密耦合的语言,那么这对于上一节来说似乎是多余的。子类型建立 is-a 关系,而继承只重用实现。子类型定义了两个(或多个)对象之间的语义关系。继承只定义了句法关系。
对象组合
对象组合是通过包含其他对象来定义一个对象。对象不是从它们继承,而是包含它们。与子类型的 is-a 关系不同,对象组合定义了 has-a 关系。
Go 中的继承
Go 是有意设计的,完全没有任何继承。这并不意味着对象(结构体)之间没有关系,而是 Go 作者选择使用替代机制来暗示关系。对于许多第一次接触 Go 的人来说,这个决定似乎会削弱 GO。实际上,它是 Go 最好的属性之一,它解决了围绕继承的十年前的问题和争论。
Go 中的多态和组合
Go 严格遵循组合优于继承的原则,而不是继承。 Go 通过结构体和 接口 之间的子类型 (is-a) 和对象组合 (has-a) 关系来实现这一点。
Go 中的对象组合
Go 用来实现对象组合原理的机制称为嵌入类型。 Go 允许你在一个结构体中嵌入另一个结构体,给它们一个 has-a 关系。
一个很好的例子是 Person 和 Address 之间的关系。
type Person struct { Name string Address Address } type Address struct { Number string Street string City string State string Zip string } func (p *Person) Talk() { fmt.Println("Hi, my name is", p.Name) } func (p *Person) Location() { fmt.Println("I’m at", p.Address.Number, p.Address.Street, p.Address.City, p.Address.State, p.Address.Zip) } func main() { p := Person{ Name: "Steve", Address: Address{ Number: "13", Street: "Main", City: "Gotham", State: "NY", Zip: "01313", }, } p.Talk() p.Location() }
点击运行示例,可以在线运行上面代码查看结果。
上面代码运行结果
从这个例子中要意识到的重要事情是 Address 仍然是一个独特的实体,同时存在于 Person 中。 在 main 函数中,我们演示了您可以将 p.Address 字段设置为地址,或者通过点符号访问它们来简单地设置这些字段。
Go 中的伪子类型
伪 is-a 关系以类似且直观的方式工作。 通过对我们上面的例子进行扩展。 让我们使用以下语句。 一个人可以说话。 公民是人,因此公民可以说话。
此代码依赖并添加到上面示例中的代码。
type Citizen struct { Country string Person } func (c *Citizen) Nationality() { fmt.Println(c.Name, "is a citizen of", c.Country) } func main() { c := Citizen{} c.Name = "Steve" c.Country = "America" c.Talk() c.Nationality() }
输出结果如下
我们在 go 中使用所谓的匿名字段来完成这个伪 is-a 关系。 在我们的示例中,Person 是 Citizen 的匿名字段。 只给出类型,不给出字段名称。 它假定了 Person 的所有属性和方法,并且可以自由使用它们或对它们进行扩展。
Go 中的真正子类型化
正如我们上面写的,子类型是 is-a 关系。 在 Go 中,每种类型都是不同的,没有什么可以充当另一种类型,但两者都可以遵循相同的接口。 接口可以用作函数(和方法)的输入和输出,从而在类型之间建立 is-a 关系。
Go 中对接口的依从性不是通过像“using”这样的关键字来定义的,而是通过在类型上声明的实际方法来定义的。 在 Efficient Go 中,它将这种关系称为“如果某事可以做到这一点,那么它可以在这里使用。”这非常重要,因为它使人们能够创建一个接口,该接口定义在外部包中的类型可以遵守。
继续上面的示例,我们添加一个新函数 SpeakTo 并修改主函数来尝试对一个公民和一个人说话。
func SpeakTo(p *Person) { p.Talk() } func main() { p := Person{Name: "Dave"} c := Citizen{Person: Person{Name: "Steve"}, Country: "America"} SpeakTo(&p) SpeakTo(&c) }
上面示例的输出结果如下
# command-line-arguments ./main.go:47:13: cannot use &c (type *Citizen) as type *Person in argument to SpeakTo
正如预期的那样,这失败了。 在我们的代码中,Citizen 不是 Person,尽管它们共享许多相同的属性,但它们被视为不同的类型。
但是,如果我们添加一个名为 Human 的接口并将其用作 SpeakTo 函数的输入,它将按预期工作。
type Human interface { Talk() } func SpeakTo(h Human) { h.Talk() } func main() { p := Person{Name: "Dave"} c := Citizen{Person: Person{Name: "Steve"}, Country: "America"} SpeakTo(&p) SpeakTo(&c) }
运行结果如下
Hi, my name is Dave Hello, my name is Steve and I'm from America
我们可以点击上面的运行示例查看完整代码及其运行结果
关于 Go 中的子类型,有两个关键点:
- 我们可以使用匿名字段来遵守接口。 我们也可以实现很多接口。 通过使用匿名字段和接口,我们非常接近真正的子类型。
- Go 确实提供了适当的子类型功能,但仅限于使用类型。 接口可用于确保各种不同的类型都可以作为函数的输入被接受,甚至可以作为函数的返回值,但实际上它们保留了不同的类型。 这清楚地显示在主函数中,我们不能直接在 Citizen 上设置 Name,因为 Name 实际上不是 Citizen 的属性,它是 Person 的属性,因此在 Citizen 的初始化期间还没有出现。
Go,没有对象或继承的面向对象编程
正如我们在这里展示的那样,尽管存在一些术语差异,面向对象的基本概念在 Go 中仍然存在并且使用也情况也很好。 术语差异是必不可少的,因为所使用的机制实际上与大多数面向对象的语言不同。
Go 使用结构体作为数据和逻辑的结合。 通过组合,可以在 Structs 之间建立 has-a 关系,以最大限度地减少代码重复,同时避免继承的脆弱混乱。 Go 使用接口在类型之间建立 is-a 关系,而无需不必要的和反作用的声明。
欢迎使用新的“无对象”面向对象编程模型。
请发表评论