接口&多态

前言

按理来说,OOP编程的三大特性,前面讲完了两个,应该下一个就是多态,但是Golang中 的多态就是通过接口来实现的,所以我们先讲一下接口。

接口(interface)

接口的设计在Golang中是大量存在的,Golang面向对象的核心就是接口编程,Golang与其他的语言相比也是很有特色。

快速入门

我们这里不讲细节,就感受一下,接口是怎么用的

Go 不需要 implements 关键字。只要你的类型有接口中定义的所有方法,就算实现了该接口。

 package main
 ​
 import "fmt"
 ​
 type Speaker interface {
     Speak() string
 }
 ​
 type Dog struct {
     Name string
 }
 ​
 func (d Dog) Speak() string {
     return "Woof! My name is " + d.Name
 }
 ​
 type Cat struct {
     Name string
 }
 ​
 func (c Cat) Speak() string {
     return "Meow! I'm " + c.Name
 }
 ​
 func main() {
     var s Speaker
 ​
     s = Dog{Name: "Buddy"}
     fmt.Println(s.Speak())
 ​
     s = Cat{Name: "Luna"}
     fmt.Println(s.Speak())
 }
image-20251114205041573

注意:DogCat 并没有“声明”自己实现了 Speaker,但因为它们都有 Speak() string 方法,所以自动满足接口,没理解我们来形象的理解一下。

代码比喻说明
type Speaker interface { Speak() string }规则:“必须会说话”定义能力标准
func (d Dog) Speak() ...狗狗学会了自我介绍实现接口的方法
func (c Cat) Speak() ...猫咪也会自我介绍另一个实现
var s Speaker一个通用话筒只接受“会说话的”
s = Dog{...} / s = Cat{...}换不同动物上台多态:同一个话筒,不同表现

基本介绍

interface类型可以定义一组方法,但是不需要实现,Interface不能包含任何变量,到某个自定义类型要使用的时候,根据具体情况把这些方法写出来,语法就是

 type 接口名 interface {
     方法() 返回值列表
     方法() 返回值列表
 }

小结:

1.里面的所有放啊都是没有方法体的,即所有的发给发都没有实现的方法,接口体现了程序设计的多态,和高内聚低耦合的思想

2.Golang 的接口,不需要显式的实现,只需要一个变量,含有接口类型中的所有方法,那么这个变量就是先这个接口,在Golang中没有implements 关键字

注意事项和细节说明

1.接口本身是不能创建实例的,但是可以指向一个实现接口的自定义类型的变量。

 type AInterface interface {
  Say()
 }
 type Stu struct {
  Name string
 }
 ​
 func (stu Stu) Say() {
  fmt.Println("Stu say")
 }
 ​
 func main() {
  var stu Stu
  var a AInterface = stu
  a.Say()
 }
image-20251117152247902

但是如果这个自定义类型没有实现这个接口的所有方法,就不能只想这个自定义类型的变量。

2.一个自定义类型实现了某个接口的所有方法,我们说这个自定义类型实现了该接口,一个自定义类型只有实现了某个接口,才能将该自定义类型的变量赋给该接口类型

3.只要是自定义数据类型就可以实现接口,不一定是结构体

4.一个自定义类型可以实现多个接口

 type AInterface interface {
  Say()
 }
 type BInterface interface {
  Hello()
 }
 type Stu struct {
  Name string
 }
 ​
 func (stu Stu) Say() {
  fmt.Println("Stu say")
 }
 func (stu Stu) Hello() {
  fmt.Println("Stu Hello")
 }
 ​
 func main() {
  var stu Stu
  var a AInterface = stu
  var b BInterface = stu
  a.Say()
  b.Hello()
 }

5.接口不能有任何变量

6.一个接口可以继承多个别的接口,这个时候如果要实现这个接口也必须把继承的接口的方法全部实现

 package main
 ​
 import "fmt"
 ​
 type AInterface interface {
  Say()
 }
 type BInterface interface {
  Hello()
 }
 type CInterface interface {
  AInterface
  BInterface
  World()
 }
 type Stu struct {
  Name string
 }
 ​
 func (stu Stu) Say() {
  fmt.Println("Stu say")
 }
 func (stu Stu) Hello() {
  fmt.Println("Stu Hello")
 }
 ​
 func (stu Stu) World() {
  fmt.Println("Stu World")
 }
 func main() {
  var stu Stu
  var c CInterface = stu
  c.Say()
  c.Hello()
  c.World()
 }
image-20251117160959759

7.Interface类型默认是引用类型(指针),如果没有初始化就是使用就会输出nil

8.空接口没有任何方法,所以所有类型都实现了空接口,我们可以把任何一个变量赋给空接口。

 type T interface {
 }

小练习

需求:对Hero结构体切片的排序

思路:对数组/切片排序,我们当然可以用冒泡排序,也可以用系统提供的一些方法sort package – sort – Go Packages,对结构体排序我们也可以用冒泡,也可以用系统提供的函数sort package – sort – Go Packages

image-20251118190627981

我们可以看到,如果我们要用这个接口,我们就要实现这三个方法。

我们来写一些看看

 package main
 ​
 import (
     "fmt"
     "math/rand"
     "sort"
 )
 ​
 type Hero struct {
     Name string
     Age  int
 }
 ​
 type Heroslice []Hero
 ​
 func (hs Heroslice) Len() int {
     return len(hs)
 }
 func (hs Heroslice) Less(i, j int) bool {
     return hs[i].Age < hs[j].Age
 }
 func (hs Heroslice) Swap(i, j int) {
     temp := hs[i]
     hs[i] = hs[j]
     hs[j] = temp
 }
 func main() {
     var intSli = []int{5, 10, 8, 520, 666, 68, 0}
     sort.Ints(intSli)
     fmt.Println(intSli)
 ​
     /*测试是否可以对结构体切片进行排序*/
     var heroes Heroslice
     for i := 0; i < 10; i++ {
         hero := Hero{
             Name: fmt.Sprintf("英雄%d", rand.Intn(1000)),
             Age:  rand.Intn(100),
         }
         heroes = append(heroes, hero)
     }
     for _, v := range heroes {
         fmt.Println(v)
     }
     sort.Sort(heroes)
     fmt.Println("---------------------------------------------")
     for _, v := range heroes {
         fmt.Println(v)
     }
 }
 ​

接口和继承

 package main
 ​
 import "fmt"
 ​
 type Monkey struct {
     Name string
 }
 ​
 func (this *Monkey) climbing() {
     fmt.Println(this.Name, "生来会爬树")
 }
 ​
 type BabyMonkey struct {
     Monkey
 }
 ​
 func main() {
     /*创建一个实例*/
     monkey := BabyMonkey{
         Monkey{
             Name: "孙悟空",
         },
     }
     
     monkey.climbing()
 }
 ​

这里BabyMonkey继承了猴子结构体的可以爬树的方法,但是BabyMonkey是孙悟空,他还可以腾云驾雾,可以像鸟一样飞,我们可以用接口来实现

 package main
 ​
 import "fmt"
 ​
 type Monkey struct {
     Name string
 }
 type BabyMonkey struct {
     Monkey
 }
 type BirdAble interface {
     Flying()
 }
 ​
 func (this *Monkey) climbing() {
     fmt.Println(this.Name, "生来会爬树")
 }
 func (this *BabyMonkey) Flying() {
     fmt.Println(this.Name, "学会了飞")
 }
 func letItFly(b BirdAble) {
     b.Flying()
 }
 func main() {
     /*创建一个实例*/
     monkey := BabyMonkey{
         Monkey{
             Name: "孙悟空",
         },
     }
 ​
     monkey.climbing()
     letItFly(&monkey)
 }
 ​

接口可以作为结构体的功能的一种拓展,而且不会破坏结构体原本的继承关系。

这里因为比较简单,所以说可能并不能太能理解到接口方便的点。

继承的价值在于解决代码的复用性和可维护性,接口的价值在于设计好各种规范让其自定义类型去实现这些方法,接口比继承更加灵活,继承是满足is –a 的关系,接口是需要满足like –a的关系,接口在一定程度上实现代码解耦。

多态

基本介绍

变量/实例具有多种形态,是面向对象的第三大特征,在Go语言中多态是通过接口来实现的,可以按照统一的接口调用不同的实现,这个时候接口变量就呈现不同的形态

快速入门

 package main
 ​
 import "fmt"
 ​
 type Speaker interface {
  Speak() string
 }
 ​
 type Dog struct {
  Name string
 }
 ​
 func (d Dog) Speak() string {
  return "Woof! My name is " + d.Name
 }
 ​
 type Cat struct {
  Name string
 }
 ​
 func (c Cat) Speak() string {
  return "Meow! I'm " + c.Name
 }
 func main() {
  var s Speaker
 ​
  s = Dog{Name: "Buddy"}
  fmt.Println(s.Speak())
 ​
  s = Cat{Name: "Luna"}
  fmt.Println(s.Speak())
 }
 ​

在上面已经实现过一遍的案例中,Speaker的接口既可以接受Dog的变量又可以接受Cat的变量,就体现了Speaker多态的特性,这种体现被称为多态参数,还有一种体现方式叫做多态数组。

我们来看一个案例,我们给一个Speakers数组里存放Dogs和Cats结构体

 package main
 ​
 import "fmt"
 ​
 type Speaker interface {
  Speak() string
 }
 ​
 type Dog struct {
  Name string
 }
 ​
 func (d Dog) Speak() string {
  return "Woof! My name is " + d.Name
 }
 ​
 type Cat struct {
  Name string
 }
 ​
 func (c Cat) Speak() string {
  return "Meow! I'm " + c.Name
 }
 func main() {
  var speakers [3]Speaker
  speakers[0] = Dog{"111"}
  speakers[1] = Cat{"222"}
  speakers[2] = Dog{"333"}
  fmt.Println(speakers)
 }
 ​
image-20251120195055386

?????这对吗,这不对吧,怎么能够放两个不同类型的结构体进去,我勒个豆

这就是多态的魅力。

类型断言

这个时候,Cat除了调用Speaker接口声明的方法外,还有一个特有的方法叫Backflip,请遍历数组,如果是Cat变量,那就除了实现接口声明的变量外,还要实现Backflip。

这里我们就需要一种判断,判断这里的Speaker是哪个类型,这里我就就需要用到类型断言

我们先来看一段错误代码

 type Point struct {
     x int
     y int
 }
 ​
 func main() {
     var a interface{}
     var point Point = Point{1, 2}
     a = point         
     var b Point
     b = a              /*❌ 编译报错*/
     fmt.Println(b)
 }
  • a 的静态类型是 interface{},编译器只知道:里面“可能装着任何东西”。
  • b 的类型是 Point。
  • 直接赋值要求:右边的静态类型要能赋值给左边(编译期就得确定)。
  • 编译器并不知道 a 里面一定是 Point,所以必须你“声明一下类型”——也就是类型断言。

我们这里用直接断言

 b := a.(Point)   // 这里运行时会检查:a 里是不是 Point

我们就能正确运行,但是这样不够安全,如果类型不对,会报panic,我们通常会加一个判断

 b, ok := a.(Point)
 if !ok {
     fmt.Println("a 里装的不是 Point")
 }else{
     fmt.Println(b)
 }

我们再回过头来实现最开始的需求

Cat除了调用Speaker接口声明的方法外,还有一个特有的方法叫Backflip,请遍历数组,如果是Cat变量,那就除了实现接口声明的变量外,还要实现Backflip。

 package main
 ​
 import "fmt"
 ​
 type Speaker interface {
  Speak() string
 }
 ​
 type Dog struct {
  Name string
 }
 ​
 func (d Dog) Speak() string {
  return "Woof! My name is " + d.Name
 }
 ​
 type Cat struct {
  Name string
 }
 ​
 func (c Cat) Speak() string {
  return "Meow! I'm " + c.Name
 }
 func (c Cat) Backflip()string{
  return "Backfliping"
 }
 type Animals struct{
 }
 func (Animals) Doing(speaker Speaker) string {
     result := speaker.Speak()          
     if cat, ok := speaker.(Cat); ok {  
         result += " " + cat.Backflip()
    }
     return result
 }
 func main() {
  var speakers [3]Speaker
  speakers[0] = Dog{"111"}
  speakers[1] = Cat{"222"}
  speakers[2] = Dog{"333"}
  fmt.Println(speakers)
  var animals Animals
  for _,v := range speakers{
  var str string
  str =  animals.Doing(v)
  fmt.Println(str)
  }
 }
 ​

我们再来一个小案例,写入一个函数通过循环判断传入参数的类型

 func TypeJudge(items ...interface{}) {
     for i, x := range items {
         switch x.(type) { // 这里 type 是一个关键字,固定写法
         case bool:
             fmt.Printf("param #%d is a bool 值是%v\n", i, x)
         case float64:
             fmt.Printf("param #%d is a float64 值是%v\n", i, x)
         case int, int64:
             fmt.Printf("param #%d is an int 值是%v \n", i, x)
         case nil:
             fmt.Printf("param #%d is nil 值是%v \n", i, x)
         case string:
             fmt.Printf("param #%d is a string 值是%v\n", i, x)
         default:
             fmt.Printf("param #%d's type is unknown , 值是%v\n", i, x)
        }
    }
 }

严格说:这段代码用的是 类型 switch,不是那种 x.(T) 形式的“显式类型断言”。

原理上来看:类型 switch 是基于类型断言实现的语法糖,它在内部帮你对 x 做了一连串的类型断言,只是你不用自己一条条写 x.(int), x.(string) 了

暂无评论

发送评论 编辑评论


				
上一篇
下一篇