从Java到Golang-对interface的理解

-目录-

If it walks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

如果一个东西走路、游泳、叫声都像鸭子,那么它大概就是一只鸭子。

对interface的理解

interface的实现

在JAVA里,interface定义了一个类的行为标准,里面可以包含不可变换的实体,例如没有函数体的函数或者不可改变的值比如final变量。一旦一个类implement这个interface,那么就要将这个interface的全部方法都实现。这样在工程上的好处是,解决了多继承问题(JAVA不能实现多继承,本质上是因为多个class之间的可变实体如同名变量,同名方法不同方法体会冲突),更高程度上抽象了一个类,只要知道这个类实现了什么接口,就可以调用接口的方法。在这个过程中不需要知道类中的具体实现,把函数注释写在接口的文件即可,保护了类。

在golang里,struct(类)定义了一系列的属性,并没有定义方法。如果这个struct想要有方法,就需要以func (cat Cat) func(input Type) output Type{ body }的形式给出,最开头的(cat Cat)即代表是这个CatStruct(类)的方法,它被称为receiver。这时一个struct如果实现了这个接口里定义的所有方法,可以称为实现了这个接口,就如一个东西做了很多鸭子做的事情那么它就是一只鸭子。注意这里面没有显式的说明某某struct实现某interface,只要全实现了,才能说明实现了这个interface。比如:

package main

import (
	"fmt"
)

type Animal interface {
	eat()
}

type Cat struct {
	name string
}

// 这里Cat实现了Animal接口的全部方法,即Cat实现了Animal接口
func (cat Cat) eat() {
	fmt.Println(cat.name + "eat")
}

func main() {
	// 常规调用
    var cat Cat
	cat = Cat{name: "wu"}
	cat.eat()
	fmt.Println(cat.name)//还可以直接调用属性
    
    // 赋值给接口变量 再调用方法
    // 可以调用方法 不能调用属性 animal.name是错的
    var animal Animal
	animal = Cat{name: "mao"}
	animal.eat()
	
}

而在JAVA里,类似的实现是这样的:

public class TestC {
    public static void main(String[] args) {
        // 调用类方法
        new A().method();
    }
}

interface In {
    public void method();
}

// 使用implements显式声明实现接口In
class A implements In {
    @Override
    public void method() {
        System.out.println("it's A");
    }
}

如果把struct类比成类的话,他们的重载可以比作下图。实际上Go也可以实现像java那一对多。

重载

利用interface做到多态

​ 函数名前加上receiver即称为方法。不同receiver接收不同类型,甚至可以是基本类型如int(需要type定义,不能直接使用)。利用这个办法可以实现重载(一个方法对不同输入做出不同反应)。

package main

type Int int

type MyStruct struct {
	name string
}

type MyStruct2 struct {
	age string
}

func (a Int) process() int {
	return int(a)
}

func (s MyStruct) process() string {
	return s.name
}

func (s MyStruct2) process() string {
	return s.age
}

func main() {
	var a Int = 1
	b := MyStruct{"ok"}
	c := MyStruct2{"11"}
	a.process()
	b.process()
	c.process()
}

​ 可以看到,这样做并没有什么意义。因为最终还是要一个一个调用,没法像Java那样使用一个符号处理全部。能不能像Java那样在处理时,只使用一个符号呢。是可以实现的,利用空接口类型断言。详见下一节。

interface作为函数参数

​ interface可以作为函数参数,代表某一类含有相同方法的struct(这些struct的对应的方法的交集为interface里的全部的方法)。传入的参数(interface类型)都有相同方法,即使每次传入参数名字不同,但是仍然可以书写一样的方法名进行调用;如果传入interface列表,也可以遍历列表调用。这个实现过程在JAVA里几乎是完全一致的。

package main

import "fmt"

type Animal interface {
	speak()
}

type Cat struct {
	voice string
}
 
type Dog struct {
	voice string
}

// 实现接口Animal
func (cat Cat) speak() {
	fmt.Println("cat can speak " + cat.voice)
}

// 实现接口Animal
func (dog Dog) speak() {
	fmt.Println("dog can speak " + dog.voice)
}

// 传入接口Animal类型参数
func animalSpeak(animal Animal){
	animal.speak()
}

// 体现了使用接口做参数的优越性,传入接口数组,批量调用方法
func animalAllSpeak(animals [2]Animal) {
	for _, item := range animals {
		item.speak()
	}
}

func main() {
	cat := Cat{voice: "miao"}
	dog := Dog{voice: "wang"}
	animalSpeak(cat)
	animalSpeak(dog)
    // 接口数组
    list := [2]Animal{cat, dog}
	animalAllSpeak(list)
}

在JAVA里类似的实现是这样的:

public class TestC {
    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        // a是类A的实例,因为类A实现了In接口,所以可以作为In接口类型参数传入函数
        new TestC().call(a);
        new TestC().call(b);
        // 通过数组传入,批量调用
        In[] list = {a, b};
        new TestC().callAll(list);
    }

    // 以In接口类型为函数参数
    public void call(In imIn) {
        imIn.method();
    }
    
    // 以In[]接口数组类型作为函数参数
    public void callAll(In[] imIns) {
        // foreach循环 for(元素类型 元素变量 : 遍历对象){使用元素变量}
        for (In imIn : imIns) {
            imIn.method();
        }
    }
}

interface In {
    public void method();
}

// 类实现接口
class A implements In {
    @Override
    public void method() {
        System.out.println("it's A");
    }
}

// 类实现接口
class B implements In {
    @Override
    public void method() {
        System.out.println("it's B");
    }
}

​ 当interface做参数的时候,若有struct传入(该struct实现了该interface),则传入的量保存了该struct的内部信息和struct种类,但是只能直接访问方法,不能直接访问内部变量。若要访问其struct的变量,要先进行类型断言i.(T)让编译器知道作为Interface传入的struct到底属于哪个struct,因为接口只是规定了实现方法没有规定里面能包含什么变量

类型断言的语法格式如下:

value, ok := x.(T)

其中,x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)。

该断言表达式会返回 x 的值(也就是 value)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型:

例子 :

package main

import "fmt"

type I interface {
	Me()
}

type S struct {
	a string
}

// Me 实现了接口,才能让struct以该接口类型传入函数
func (s S) Me() {

}

func main() {
	var i I
	i = S{"string inside"}
	describe(i)
}

func describe(i I) {
	//i是实现了接口的类型,但是作为接口传入就是接口,需要断言才能调用内部的值
	// 可以直接访问方法
	i.Me()
	// b:=i.a 错误,不能直接访问,要加上类型断言,相当于强制转换
	t, _ := i.(S)
	b:=t.a
	fmt.Println(b)
}

// 输出
// string inside

​ 只要实现了interface的结构都可以作为此interface类型传入函数参数,那么空接口interface{}呢?要知道所有struct都包含空方法,所有struct都实现了空接口。因此,空interface可以作为通用类型代表任何struct。利用这个通用变量加上类型断言可以实现多态Reload。如下

package main

import "fmt"

type I interface {
	Show()
}

type Dog struct {
	name string
}

type Cat struct {
	eat string
}

func (dog Dog) Show() {
	fmt.Println(dog.name)
}

func (cat Cat) Show() {
	fmt.Println(cat.eat)
}

func main() {
	dog := Dog{"kiki"}
	cat := Cat{"fish"}
	ShowAll(dog)
	ShowAll(cat)
}

func ShowAll(i I) {
	// 直接访问方法不需要断言
	i.Show()
	t, ok := i.(Dog)
	if ok {
		fmt.Println("dog name " + t.name)
	}
	// 此处不能再用t保存实际值,要用新的变量r
	r, ok := i.(Cat)
	if ok {
		fmt.Println("cat eat " + r.eat)
	}
}
// 输出
// kiki
// dog name kiki
// fish
// cat eat fish

​ 在JAVA里是:定义不同输入类型的同名方法(多个)–>实现重载。在Go里是:定义一个接收空接口interface{}类型的方法(可以只有一个)–>在方法内判断传入参数类型做出不同反应–>实现重载(这里的参数类型需要自己封装成一个struct,否则无法传入)。这里可以看出Go和Java的不同,Java允许你创建多个同名方法然后运行时帮你判断参数类型,然后自动调用你创建的匹配该参数类型方法;而Go更加底层一点,需要你自己实现判断参数类型的过程然后做出对应处理。

​ 其实他们本质都是一样的,这个语言特性被称作反射(Reflection)。反射指的是,程序在运行过程中能够获得自身信息,例如函数main(param)运行时,他会知道自己叫"main",如果内部有定义变量,他可以知道变量名字,还可以在传入参数param时知道它的数据类型。注意这里的param可以在编写程序时不指定类型,只需要在调用这个函数时,利用语言标准库提供的一些方法就可以获得变量类型。反射的例子:

​ 反射这个特性赋予了语言“动态”的特性,静态语言编译运行,一切变量信息都在编译时定下,程序不会在运行时再获取自身信息。而反射允许语言在运行时动态获取信息。

© 2019 - 2023 · YuYoung's Blog