今天是golang专题的第11篇文章,我们一起来聊聊golang当中多态的这个话题。
如果大家系统的学过C++、Java等语言以及面向对象的话,相信应该对多态不会陌生。
多态是面向对象范畴当中经常使用并且非常好用的一个功能,如果你之前没有学过的话也没有关系,我们用一个简单的例子来说明一下。多态主要是用在强类型语言当中,像是Python这样的弱类型语言,变量的类型可以随意变化,也没有任何限制,其实区别不是很大。
多态的含义
对于Java或者是C++而言,我们在使用变量的时候,变量的类型是明确的。但是如果我们希望它可以宽松一点,比如说我们用父类指针或引用去调用方法,但是在执行的时候,能够根据子类的类型去执行子类当中的方法。也就是说实现我们用相同的调用方式调出不同结果或者是功能的情况,这种情况就叫做多态。
举个非常经典的例子,比如说猫、狗和人都是哺乳动物。这三个类都有一个say方法,大家都知道猫、狗以及人类的say是不一样的,猫可能是喵喵叫,狗是汪汪叫,人类则是说话。
class Mammal {
public void say() {
System.out.println("do nothing")
}
}
class Cat extends Mammal{
public void say() {
System.out.println("meow");
}
}
class Dog extends Mammal{
public void say() {
System.out.println("woof");
}
}
class Human extends Mammal{
public void say() {
System.out.println("speak");
}
}
这段代码大家应该都不难看懂,这三个类都是Mammal的子类,假设这个时候我们有一系列实例,它们都是Mammal的子类的实例,但是这三种类型都有,我们希望用一个循环来一起全都调用了。虽然我们接收变量的时候是用的Mammal的父类类型去接收的,但是我们调用的时候却会获得各个子类的运行结果。
比如这样:
class Main {
public static void main(String[] args) {
List<Mammal> mammals = new ArrayList<>();
mammals.add(new Human());
mammals.add(new Dog());
mammals.add(new Cat());
for (Mammal mammal : mammals) {
mammal.say();
}
}
}
不知道大家有没有get到精髓,我们创建了一个父类的List,将它各个子类的实例放入了其中。然后通过了一个循环用父类对象来接收,并且调用了say方法。我们希望虽然我们用的是父类的引用来调用的方法,但是它可以自动根据子类的类型调用对应不同子类当中的方法。
也就是说我们得到的结果应该是:
speak
woof
meow
这种功能就是多态,说白了我们可以在父类当中定义方法,在子类当中创建不同的实现。但是在调用的时候依然还是用父类的引用去调用,编译器会自动替我们做好内部的映射和转化。
抽象类与接口
这样实现当然是可行的,但其实有一个小小的问题,就是Mammal类当中的say方法多余了。因为我们使用的只会是它的子类,并不会用到Mammal这个父类。所以我们没必要实现父类Mammal中的say方法,做一个标记,表示有这么一个方法,子类实现的时候需要实现它就可以了。
这就是抽象类和抽象方法的来源,我们可以把Mammal做成一个抽象类,声明say是一个抽象方法。抽象类是不能直接创建实例的,只能创建子类的实例,并且抽象方法也不用实现,只需要标记好参数和返回就行了。具体的实现都在子类当中进行。说白了抽象方法就是一个标记,告诉编译器凡是继承了这个类的子类必须要实现抽象方法,父类当中的方法不能调用。那抽象类就是含有抽象方法的类。
我们写出Mammal变成抽象类之后的代码:
abstract class Mammal {
abstract void say();
}
很简单,因为我们只需要定义方法的参数就可以了,不需要实现方法的功能,方法的功能在子类当中实现。由于我们标记了say这个方法是一个抽象方法,凡是继承了Mammal的子类都必须要实现这个方法,否则一定会报错。
抽象类其实是一个擦边球,我们可以在抽象类中定义抽象的方法也就是只声明不实现,也可以在抽象类中实现具体的方法。在抽象类当中非抽象的方法子类的实例是可以直接调用的,和子类调用父类的普通方法一样。但假如我们不需要父类实现方法,我们提出提取出来的父类中的所有方法都是抽象的呢?针对这一种情况,Java当中还有一个概念叫做接口,也就是interface,本质上来说interface就是抽象类,只不过是只有抽象方法的抽象类。
所以刚才的Mammal也可以写成:
interface Mammal {
void say();
}
把Mammal变成了interface之后,子类的实现没什么太大的差别,只不过将extends关键字换成了implements。另外,子类只能继承一个抽象类,但是可以实现多个接口。早先的Java版本当中,interface只能够定义方法和常量,在Java8以后的版本当中,我们也可以在接口当中实现一些默认方法和静态方法。
接口的好处是很明显的,我们可以用接口的实例来调用所有实现了这个接口的类。也就是说接口和它的实现是一种要宽泛许多的继承关系,大大增加了灵活性。
以上虽然全是Java的内容,但是讲的其实是面向对象的内容,如果没有学过Java的小伙伴可能看起来稍稍有一点点吃力,但总体来说问题不大,没必要细扣当中的语法细节,get到核心精髓就可以了。
讲这么一大段的目的是为了厘清面向对象当中的一些概念,以及接口的使用方法和理念,后面才是本文的重头戏,也就是Go语言当中接口的使用以及理念。
Golang中的接口
Golang当中也有接口,但是它的理念和使用方法和Java稍稍有所不同,它们的使用场景以及实现的目的是类似的,本质上都是为了抽象。通过接口提取出了一些方法,所有继承了这个接口的类都必然带有这些方法,那么我们通过接口获取这些类的实例就可以使用了,大大增加了灵活性。
但是Java当中的接口有一个很大的问题就是侵入性,说白了就是会颠倒供需关系。举个简单的例子,假设你写了一个爬虫从各个网页上爬取内容。爬虫爬到的内容的类别是很多的,有图片、有文本还有视频。假设你想要抽象出一个接口来,在这个接口当中定义你规定的一些提取数据的方法。这样不论获取到的数据的格式是什么,你都可以用这个接口来调用。这本身也是接口的使用场景,但问题是处理图片、文本以及视频的组件可能是开源或者是第三方的,并不是你开发的。你定义接口并没有什么卵用,别人的代码可不会继承这个接口。
当然这也是可以解决的, 比如你可以在这些第三方工具库外面自己封装一层,实现你定义的接口。这样当然是OK的,但是显然比较麻烦。
Golang当中的接口解决了这个问题,也就是说它完全拿掉了原本弱化的继承关系,只要接口中定义的方法能对应的上,那么就可以认为这个类实现了这个接口。
我们先来创建一个interface,当然也是通过type关键字:
type Mammal interface {
Say()
}
我们定义了一个Mammal的接口,当中声明了一个Say函数。也就是说只要是拥有这个函数的结构体就可以用这个接口来接收,我们和刚才一样,定义Cat、Dog和Human三个结构体,分别实现各自的Say方法:
type Dog struct{}
type Cat struct{}
type Human struct{}
func (d Dog) Say() {
fmt.Println("woof")
}
func (c Cat) Say() {
fmt.Println("meow")
}
func (h Human) Say() {
fmt.Println("speak")
}
之后,我们尝试使用这个接口来接收各种结构体的对象,然后调用它们的Say方法:
func main() {
var m Mammal
m = Dog{}
m.Say()
m = Cat{}
m.Say()
m = Human{}
m.Say()
}
出来的结果当然和我们预想的一样:
总结
今天我们一起聊了面向对象中多态以及接口的概念,借此进一步了解了为什么golang中的接口设计非常出色,因为它解耦了接口和实现类之间的联系,使得进一步增加了我们编码的灵活度,解决了供需关系颠倒的问题。但是世上没有绝对的好坏,golang中的接口在方便了我们编码的同时也带来了一些问题,比如说由于没了接口和实现类的强绑定,其实也一定程度上增加了开发和维护的成本。
总体来说这是一个仁者见仁的改动,有些写惯了Java的同学可能会觉得没有必要,这是过度解绑,有些人之前深受其害的同学可能觉得这个进步非常关键。但不论你怎么看,这都不影响我们学习它,毕竟学习本身是不带立场的。今天的内容当中包含一些Java和面向对象的概念,只是用来引出后面golang的内容,如果存在部分不理解的地方,希望大家抓大放小,理解核心关键就好了,不需要细扣每一个细节。
有疑问加站长微信联系(非本文作者)