zoukankan      html  css  js  c++  java
  • Golang的Interface是个什么鬼

    问题概述

    Golang的interface,和别的语言是不同的。它不需要显式的implements,只要某个struct实现了interface里的所有函数,编译器会自动认为它实现了这个interface。第一次看到这种设计的时候,我的第一反应是:What the fuck?这种奇葩的设计方式,和主流OO语言显式implement或继承的区别在哪儿呢?

    直到看了SICP以后,我的观点发生了变化:Golang的这种方式和Java、C++之流并无本质区别,都是实现多态的具体方式。而所谓多态,就是“一个接口,多种实现”。

    SICP里详细解释了为什么同一个接口,需要根据不同的数据类型,有不同的实现;以及如何做到这一点。在这里没有OO的概念,先把OO放到一边,从原理上看一下这是怎么做到的。

    先把大概原理放在这里,然后再举例子。为了实现多态,需要维护一张全局的查找表,它的功能是根据类型名和方法名,返回对应的函数入口。当我增加了一种类型,需要把新类型的名字、相应的方法名和实际函数入口添加到表里。这基本上就是所谓的动态绑定了,类似于C++里的vtable。对于SICP中使用的lisp语言来说,这些工作需要手动完成。而对于java,则通过implements完成了这项工作。而golang则用了更加激进的方式,连implements都省了,编译器自动发现自动绑定。

    一个复数包的例子

    SICP里以复数为例,我用clojure、java和golang分别实现了一下,代码放在https://github.com/nanoix9/golang-interface。这里的目的是实现一个复数包,它支持直角坐标(rectangular)和极坐标(polar)两种实现方式,但是两者以相同的形式提供对外的接口,包括获取实部、虚部、模、辐角四个操作,文中简单起见,仅以获取实部为例。代码中有完整的内容。

    Clojure版

    对于直角坐标,用一个两个元素的列表表示它,分别是实部和虚部。

    (defn make-rect [r i] (list r i))

    对于极坐标,也是含有两个元素的列表,分别表示模和辐角

    (defn make-polar [abs arg] (list abs arg))

    现在要加一个“取实部”的函数get-real。问题来了,我希望这个函数能同时处理两种坐标,而且对于使用者来说,无论使用哪种坐标表示,get-real函数的行为是一致的。最简单的想法是,增加一个tag字段用于区分两种类型,然后get-real根据类型信息执行不同的操作。

    为此,定义attach-tagget-tagget-content函数用于关联标签、提取标签和提取内容:

    (defn attach-tag [tag data] (list tag data))
    (defn get-tag [data-with-tag] (first data-with-tag))
    (defn get-content [data-with-tag] (second data-with-tag))

    在构造复数的函数中加入tag

    (defn make-rect [r i] (attach-tag 'rect (list r i)))
    (defn make-polar [abs arg] (attach-tag 'polar (list abs arg)))

    get-real函数首先获取tag,根据直角坐标或极坐标执行不同的操作

    (defn get-real [c]
      (let [tag (get-tag c)
            num (get-content c)]
        (cond (= tag 'rect) (first num)
              (= tag 'polar) (* (first num) (Math/cos (second num)))
              :else (println "Unknown complex type:" tag))))

    但是这样有个问题,如果要加第三种类型怎么办?必须修改get-real函数。也就是说,要增加一种实现,必须改动函数主入口。有没有方法避免呢?答案就是采用前面的查找表(当然这不是唯一方法,SICP中还介绍了消息传递方法,这里就不介绍了)。这个查找表提供get-opput-op两个方法

     (defn get-op [tag op-name] ...
     (defn put-op [tag op-name func] ...)

    这里只给出原型,get-op根据类型名和方法名,获取对应的函数入口。而put-op向表中增加类型名、方法名和函数入口。这张表的内容直观上可以这么理解

    tagop-name'get-real'get-image...
    'rect get-real-rect get-image-rect ...
    'polar get-real-polar get-image-polar ...

    于是get-real函数可以这样实现:首先每种类型各自将自己的函数入口添加到查找表

    (defn install-rect []
      (letfn [(get-real [c] (first c))]
        put-op 'rect 'get-real get-real))
    
    (defn install-polar []
      (letfn [(get-real [c] (* (first c) (Math/cos (second c))))]
        put-op 'polar 'get-real get-real))
    
    (install-rect)
    (install-polar)

    注意这里用了局部函数letfn,所以两种类型都用get-real作为函数名并不冲突。

    定义apply-generic函数,用来从查找表中获取函数入口,并把tag去掉,将内容和剩余参数送给获取到的函数

    (defn apply-generic [op-name tagged-data & args]
      (let [tag (get-tag tagged-data)
            content (get-content tagged-data)
            func (get-op tag op-name)]
        (if (null? func)
            (println "No entry for data type" tag "and method" op-name))
            (apply func (cons content args))))

    get-real函数可以实现了

    (defn get-real [c]
        (apply-generic 'get-real c))

    Java版

    Java实现复数包就不需要这么麻烦了,编译器完成了大部分工作。当然Java是静态语言,还有类型检查。

    public interface Complex {
        public double getReal();
        ...
    }
    
    public class ComplexRect implements Complex {
    
        private double real;
        private double image;
    
        public double getReal() {
            return real;
        }
    
        ...
    }
    
    public class ComplexPolar implements Complex {
    
        private double abs;
        private double arg;
    
        public double getReal() {
            return abs * Math.cos(arg);
        }
    
        ...
    }

    Golang版

    Golang和Java的差别就是省去了implements

    type Complex interface {
        GetReal() float64
        ...
    }
    
    type ComplexRect struct {
        real, image float64
    }
    
    func (c ComplexRect) GetReal() float64 {
        return c.real
    }
    
    ...
    
    type ComplexPolar struct {
        abs, arg float64
    }
    
    func (c ComplexPolar) GetReal() float64 {
        return c.abs * math.Cos(c.arg)
    }
    
    ...

    乍一看看不出ComplexRectComplex之间有什么关系,它是隐含的,编译器自动发现。这样的做法更灵活,比如增加一个新的接口类型,编译器会自动发现那些struct实现了该接口,而无需修改struct的代码。如果是java,就必须修改源代码,显式的implements

    总结

    通过这个问题,我意识到,OO只不过是一种方法,其实本没有什么对象。至于为什么要OO,最根本的,是要实现“一个接口,多种实现”,这就要求接口是稳定的,而实现有可能是多变的。如果接口也是经常变的,那就没必要把接口抽象出来了。至于代码结构是否反映了世界的继承/组合等关系,这并不重要,也不是根本的。重要的是,将稳定的接口和不稳定的实现分离,使得改动某个模块的时候,不至于影响到其他部分。这是软件本质上的复杂性提出的要求,对于大型软件来说,模块的分解和隔离尤为重要。

    为了达到这个目的,C++实现了vtable,Java提供了interface,Golang则自动发现这种关系。可以用OO,也可以不用OO。无论语言提供了哪种方式,背后的思想是统一的。甚至我们可以在语言特性满足不了需求的时候,自己实现相关的机制,例如spring,通过xml完成依赖注入,这使得可以在不改动源代码的情况下,用一种实现替换另一种实现。

  • 相关阅读:
    PNG文件格式具体解释
    opencv2对读书笔记——使用均值漂移算法查找物体
    Jackson的Json转换
    Java实现 蓝桥杯VIP 算法训练 装箱问题
    Java实现 蓝桥杯VIP 算法训练 装箱问题
    Java实现 蓝桥杯VIP 算法训练 单词接龙
    Java实现 蓝桥杯VIP 算法训练 单词接龙
    Java实现 蓝桥杯VIP 算法训练 方格取数
    Java实现 蓝桥杯VIP 算法训练 方格取数
    Java实现 蓝桥杯VIP 算法训练 单词接龙
  • 原文地址:https://www.cnblogs.com/aquastone/p/thinking-on-golang-interface.html
Copyright © 2011-2022 走看看