Hello World
Spiga

为什么我不喜欢Go语言式的接口(即Structural Typing)

2013-04-10 18:37 by 老赵, 13196 visits

所谓Go语言式的接口,就是不用显示声明类型T实现了接口I,只要类型T的公开方法完全满足接口I的要求,就可以把类型T的对象用在需要接口I的地方。这种做法的学名叫做Structural Typing,有人也把它看作是一种静态的Duck Typing。除了Go的接口以外,类似的东西也有比如Scala里的Traits等等。有人觉得这个特性很好,但我个人并不喜欢这种做法,所以在这里谈谈它的缺点。当然这跟动态语言静态语言的讨论类似,不能简单粗暴的下一个“好”或“不好”的结论。

那么就从头谈起:什么是接口。其实通俗地讲,接口就是一个协议,规定了一组成员,例如.NET里的ICollection接口:

interface ICollection {
    int Count { get; }
    object SyncRoot { get; }
    bool IsSynchronized { get; }
    void CopyTo(Array array, int index);
}

这就是一个协议的全部了吗?事实并非如此,其实接口还规定了每个行为的“特征”。打个比方,这个接口的Count除了需要返回集合内元素的数目以外,还隐含了它需要在O(1)时间内返回这个要求。这样一个使用了ICollection接口的方法才能放心地使用Count属性来获取集合大小,才能在知道这些特征的情况下选用正确的算法来编写程序,而不用担心带来性能问题,这才能实现所谓的“面向接口编程”。当然这种“特征”并不单指“性能”上的,例如Count还包含了“不修改集合内容”这种看似十分自然的隐藏要求,这都是ICollection协议的一部分。

由此我们还可以解释另外一些问题,例如为什么.NET里的List<T>不叫做ArrayList<T>(当然这些都只是我的推测)。我的想法是,由于List<T>IList<T>接口是配套出现的,而像IList<T>的某些方法,例如索引器要求能够快速获取元素,这样使用IList<T>接口的方法才能放心地使用下标进行访问,而满足这种特征的数据结构就基本与数组难以割舍了,于是名字里的Array就显得有些多余。

假如List<T>改名为ArrayList<T>,那么似乎就暗示着IList<T>可以有其他实现,难道是LinkedList<T>吗?事实上,LinkedList<T>根本与IList<T>没有任何关系,因为它的特征和List<T>相差太多,它有的尽是些AddFirstInsertBefore方法等等。当然,LinkedList<T>List<T>都是ICollection<T>,所以我们可以放心地使用其中一小部分成员,它们的行为特征是明确的。

这方面的反面案例之一便是Java了。在Java类库中,ArrayListLinkedList都实现了List接口,它们都有get方法,传入一个下标,返回那个位置的元素,但是这两种实现中前者耗时O(1)后者耗时O(N),两者大相近庭。那么好,我现在要实现一个方法,它要求从第一个元素开始,返回每隔P个位置的元素,我们还能面向List接口编程么?假如我们依赖下标访问,则外部一不小心传入LinkedList的时候,算法的时间复杂度就从期望的O(N/P)变成了O(N2/P)。假如我们选择遍历整个列表,则即便是ArrayList我们也只能得到O(N)的效率。话说回来,Java类库的List接口就是个笑话,连Stack类都实现了List,真不知道当年的设计者是怎么想的。

简单地说,假如接口不能保证行为特征,则“面向接口编程”没有意义。

而Go语言式的接口也有类似的问题,因为Structural Typing都只是从表面(成员名,参数数量和类型等等)去理解一个接口,并不关注接口的规则和含义,也没法检查。忘了是Coursera里哪个课程中提到这么一个例子:

interface IPainter {
    void Draw();
}

interface ICowBoy {
    void Draw();
}

在英语中Draw同时具有“画画”和“拔枪”的含义,因此对于画家(Painter)和牛仔(Cow Boy)都可以有Draw这个行为,但是两者的含义截然不同。假如我们实现了一个“小明”类型,他明明只是一个画家,但是我们却让他去跟其他牛仔决斗,这样就等于让他去送死嘛。另一方面,“小王”也可以既是一个“画家”也是个“牛仔”,他两种Draw都会,在C#里面我们就可以把他实现为:

class XiaoWang : IPainter, ICowBoy {
    void IPainter.Draw() {
        // 画画
    }

    void ICowBoy.Draw() {
        // 掏枪
    }
}

因此我也一直不理解Java的取舍标准。你说这样一门强调面向对象强调接口强调设计的语言,还要求强制异常,怎么就不支持接口的显示实现呢?

这就是我更倾向于Java和C#中显式标注异常的原因。因为程序是人写的,完全不会因为一个类只是因为存在某些成员,就会被当做某些接口去使用,一切都是经过“设计”而不是自然发生的。就好像我们在泰国不会因为一个人看上去是美女就把它当做女人,这年头的化妆和PS技术太可怕了。

我这里再小人之心一把:我估计有人看到这里会说我只是酸葡萄心理,因为C#中没有这特性所以说它不好。还真不是这样,早在当年我还没听说Structural Typing这学名的时候就考虑过这个问题。我写了一个辅助方法,它可以将任意类型转化为某种接口,例如:

XiaoMing xm = new XiaoMing();
ICowBoy cb = StructuralTyping.From(xm).To<ICowBoy>();

于是,我们就很快乐地将只懂画画的小明送去决斗了。其内部实现原理很简单,只是使用Emit在运行时动态生成一个封装类而已。此外,我还在编译后使用Mono.Cecil分析程序集,检查FromTo的泛型参数是否匹配,这样也等于提供了编译期的静态检查。此外,我还支持了协变逆变,还可以让不需要返回值的接口方法兼容带有返回值的方法(现在甚至还可以为其查找扩展方法),这可比简单通过名称和参数类型判断要强大多了。

有了多种选择,我才放心地说我喜欢哪个。JavaScript中只能用回调编写代码,于是很多人说它是JavaScript的优点,说回调多么多么美妙我会深不以为然——只是没法反抗开始享受罢了嘛……

这篇文章好像吐槽有点多?不过这小文章还挺爽的。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

74 条回复

  1. 韦恩卑鄙
    116.226.213.*
    链接

    韦恩卑鄙 2013-04-10 19:13:49

    于是,我们就很快乐地将只懂画画的小明送去决斗了。 不上gist嘛坏人

  2. 老赵
    admin
    链接

    老赵 2013-04-10 20:19:10

    @韦恩卑鄙

    什么gist?

  3. 链接

    焜 邱 2013-04-10 20:21:39

    .Net 什么都做对了。唯一做错的是没给人 mono 不死的信心。

  4. 老赵
    admin
    链接

    老赵 2013-04-10 20:33:47

    @焜 邱

    微软都提出Community Promise了,你还要怎么给信心?又没法控制人的想法。

  5. Wuvist
    175.156.235.*
    链接

    Wuvist 2013-04-10 22:33:43

    例如Count还包含了例如“不修改集合内容”这种看似十分自然的隐藏要求,这都是ICollection协议的一部分。

    我能否在C#里面写一个类实现ICollection接口,然后在这里类的Count方法里去修改集合内容呢?每Count一次加一,越Count越多~这个类一定要叫Money~

  6. 老赵
    admin
    链接

    老赵 2013-04-10 22:36:36

    @Wuvist

    这个艰巨的任务就交给你了……

  7. Wuvist
    175.156.235.*
    链接

    Wuvist 2013-04-10 23:34:29

    @老赵

    我的意思是.Net的接口设计也不能保证你所说的“接口保证行为特征”,如果你因此不喜欢Go语言式的接口,那么你也应该不喜欢.Net的语言式的接口。

    但是,看上去,你挺喜欢.Net的接口的;所以,你并没有解释“为什么不喜欢Go语言式的接口”。

  8. 老赵
    admin
    链接

    老赵 2013-04-11 00:07:51

    @Wuvist

    再去读几遍文章。

    我的意思是,在.NET里面接口跟类型的关系是由程序员显式指定的,不会出现由编译器根据表面相似而自动联系起来继而产生的问题。

    你非要写一个破坏协议的实现,那也没人可以拦着你。

  9. Wuvist
    175.156.235.*
    链接

    Wuvist 2013-04-11 00:32:18

    @老赵

    Go里面类适用于哪些接口,不也是程序员显示指定的吗?

    区别在于是实现者显示指定,还是调用者显示指定。

    我也可以说作为调用者,你非要乱使用一个你不了解的类,那也没人可以拦着你。

    但抬杠毫无意义。

    值得讨论的,是接口调用者还是接口实现者责任的问题;这才是“Go语言式接口”与常见语言接口的本质不同。

  10. cd
    121.9.213.*
    链接

    cd 2013-04-11 08:42:23

    这段时间看你喷了各种go的边边角角, 我只能说你思想僵硬固化了。

    你认为有任何完美的语言, 能满足所有人各种奇怪的需求么。

    比如你可以要求“你非要写一个破坏协议的实现,那也没人可以拦着你。”,

    别人也可以要求“作为调用者,你非要乱使用一个你不了解的类,那也没人可以拦着你。”

    你有任何语言可以同时满足上者么?

    .Net如果很完美那你还关注其他语言为啥呢?

    如果你没法做到一调众口, 那保持简单是最好的。 而go对我来说,特性足够的简单, 又足够够用。

    喷者恒喷之, 请您继续喷的津津乐道。

  11. 链接

    Haart 2013-04-11 09:02:27

    @Wuvist @cd

    感觉你们还是没有搞清楚老赵写的文章的意思。

    首先,对于任何一个类来说,实现者永远比调用者更清楚这个类的细节。

    对于静态类型语言。实现者声明该类实现了接口,这个做法是合理的,因为实现者很清楚该类可以满足该接口的条件。 对于动态类型语言。调用者将某类作为某接口一样使用,这个可能合理也可能不合理,因为调用者可能并不清楚这个类的细节,仅仅从名字上猜测该类可以像某接口那样用,语言本身无法提供类似 身份标签 这样的强制限制。

    当然,以上并不是说鸭子类型这种东西一无是处,我认为鸭子类型是舍弃严谨性来获取灵活性,这种折中策略在描述易变的业务逻辑的时候还是很有用的。

  12. 四不象
    116.228.132.*
    链接

    四不象 2013-04-11 09:39:07

    感觉者更本不是问题,通过方法名称来区分就可以了 比如 ArrayGet ListGet PainterDraw CowboyDraw

  13. 四不象
    116.228.132.*
    链接

    四不象 2013-04-11 09:50:07

    Go的接口和类型关系也可以通过特殊命名规则的方式来联系,反而显得更加灵活。这不是Go语言接口的缺点,而是你用.Net的思路去使用Go而导致的问题

  14. 老赵
    admin
    链接

    老赵 2013-04-11 10:07:51

    @Wuvist

    你还是没懂,我说的就是显式标注接口可以让设计者有能力通过良好的设计避免使用者误用,只要使用者不犯错,那么设计者的目的就达到了。在Java里把一个LinkedList传递给List 那是符合设计的使用方式(用List就是为了抽象),不是误用,谈不上“乱使用一个你不了解的类”,一个类本身就是个隐藏细节的东西。

    你还听不懂我也不多说了。

  15. earthengine
    1.0.5.*
    链接

    earthengine 2013-04-11 10:52:31

    Go 是否支持代码标注?如果支持的话,能否在鸭子类型上加上标注兼容的限制?如果可以,你文章里提到的大多数问题都可以解决。

    我说的代码标注是Java的Annotation和.NET的Attribute这类东西。

  16. 老赵
    admin
    链接

    老赵 2013-04-11 10:57:33

    @earthengine

    假如不是语言级别支持的检查,那这个标注也就是类似于注释这样的东西。

  17. 老赵
    admin
    链接

    老赵 2013-04-11 11:08:24

    @四不象

    因为怕重名把一个Get方法改名为XxxGet,每个方法都加前缀的做法叫作“蓝精灵命名法(Smurf Naming Convention)”,你难道写代码时真会把Draw取名为PainterDraw吗?

  18. 珧麒麟
    124.42.13.*
    链接

    珧麒麟 2013-04-11 11:11:24

    韦恩的意思是让你上StructuralTyping的代码。

    话说把文中的代码加上了颜色,看上去挺舒服的。

  19. 老赵
    admin
    链接

    老赵 2013-04-11 11:37:07

    @珧麒麟

    当年还没GitHub甚至还没什么抽离出项目的习惯,要放到现在肯定先开一个新项目,然后引用。

  20. zhangyf
    114.212.87.*
    链接

    zhangyf 2013-04-11 12:40:16

    话说感觉还是需要引入Spec#这类机制,直接把功能规约写入到代码之中啊。 不过非功能属性似乎就不好办了啊

  21. earthengine
    101.160.162.*
    链接

    earthengine 2013-04-11 17:58:12

    Java的Annotation带有可扩展的语法支持。Attribute我不太熟悉。如果有类似Java Annotation一样的语法元素,就可以强制编译器实施附加的类型限制。

    经典的例子是@Override,近期还有@Nullable等。第三方的Annotation甚至可以实现访问器构造器等的自动生成。

  22. 老赵
    admin
    链接

    老赵 2013-04-11 20:58:44

    @earthengine

    懂你的意思了,不过这个倒不单单是annotation,那个是Java 5开始的,你要的是Java 6开始的编译器处理能力,这个相当于给编译器加一个预处理扩展了。

  23. 科技球
    216.239.45.*
    链接

    科技球 2013-04-12 02:48:34

    go的interface机制主要的作用是减少不必要的dependency。譬如说,某个公司开发了一个library,这个library有class A, B, C等等。如果library的客户意识到了class A, B, C有某些common usage pattern,在go里面这个客户就可以定义一个interface,然后在他自己的code里面对着这个interface进行操作了。library的公司不需要depends on客户定义的interface,他甚至不需要知道这个interface的存在。

    .NET风格机制的支持者会觉得,如果一个library的开发者既然意识到了这个common pattern的存在,他自己就应该让library里面定义某个interface然后让class A, B, C显式地继承它。所以这个问题归根结底就是一个philosophy取舍的问题:到底需不需要要求类库的实现者identify所有可能的usage pattern,然后调用者有新的用法或者需求都depends on类库的实现者?用这样额外的dependency和coupling来避免所谓的“使用错误”,是否值得?如果类库作者停止开发了呢?

    另一个问题是,老赵似乎有一种观点,能够表达在code里面的约定一定比写在注释里或者文档里面的约定要好。但是真的是这样吗?首先,目前没有一种程序语言能够表达出所有的约定,即使这篇文章举的.NET interface的例子,也不能表达出O(1)或者Count属性Immutable等方面的要求。interface同样不能描述thread-safety的需求。象那种面向spec编程什么的我觉得都是在这个方向有点走火入魔了。以牺牲生产力的代价去保证编译时抓住所有的使用错误这件事情到底值得吗?我觉得这里边有一个界,有些东西适合用code去表达,有些东西适合用注释和文档去表达,最终的目的都是为了最大化地提高生产力和code的扩展性及维护性。

    当然我不是说编译时间检查一点价值都没有,它固然非常有用。只是说,在真实的用例中,一个程序员在不了解自己使用类库的情况下把画画的小明送去决斗的可能性有多少?值得用那么多额外的标注,dependency,coupling以及牺牲flexibility去避免它吗?且不说还有unit test, regression test, integration test去抓住这个错误。

    如果用.NET的思维去理解go的interface,固然会觉得它不好。但是其实go鼓励的是一种全新的软件开发的模式。为什么一定要有类的实现者,和调用者的区分?甚至为什么一定要有类这个概念的存在?这都是值得思考的。go里面其实没有类这个概念,只有数据类型,处理数据类型的函数和方法,以及建立在这些基础之上无比灵活的使用案例。如果一个类型不符合某个接口,调用者甚至可以扩展它。go的interface一个美妙的地方是,很多最最基本的数据类型,比如int,array,time等,都可以作为接口传递到函数里面。go不是OOP,只是把OOP思想中好的部分抓取过来了。

    另外推荐一个视频: A Tour of the Go Programming Language with Russ Cox go的创始人做的,短短30分钟但是其实讲出了很多go的精髓。

  24. 老赵
    admin
    链接

    老赵 2013-04-12 10:04:50

    @科技球

    你还是没看懂我的文章,又在说.NET的interface没有表达出O(1)之类的,我说它表达出来了么?我的观点就是建立在“没人能表达出来”这点上的。

    其他一些东西的确有道理,但是这些道理是动态静态语言,自由严格语言中常见的。就像我文章里说的,是我不喜欢,但是我认为都是可谈的,也可以理解有些人支持它们。

  25. 科技球
    216.239.45.*
    链接

    科技球 2013-04-12 10:23:42

    @老赵

    我并不觉得这个动静态语言之争,go是静态语言,但是却避免了传统静态语言中的dependency和coupling。同时它又可以通过编译时类型检查避免变量名typo,参数顺序传错等等这些程序员常见的错误,等于说结合了动静态语言的优点。

    我知道你没有说.NET的interface表达出O(1)的意思。但我的point是,.NET的interface没有表达出来不能说明.NET的interface不好,同理,go的interface没有表达出来的东西也不能说明它不好。有些东西不是适合通过interface去表达的。

  26. 老赵
    admin
    链接

    老赵 2013-04-12 10:36:04

    @科技球

    Go是静态语言没错,但其实这跟动静语言之争差不多,就是我后面说的“自由严格”的争论。Go只是在一根轴上比动态语言更严格,比某些静态语言更自由,我只会说这也是一种tradeoff,你说它结合了动静语言的优点,但至少我不敢如此肯定。

    你完全可以说.NET的interface这方面做的不好,我可没说.NET就做得完美了,我也希望它的interface可以检查地更多。我不同意你说的“这些东西”是不适合通过interface去表达的,文章里说的很清楚,这些都是我认为是interface的一部分,不能因为技术上做不到检验,就认为这个东西不是技术上设计上应该存在的东西。

    当然这还是动静态语言之争——自由严格语言之争。

  27. 科技球
    76.21.115.*
    链接

    科技球 2013-04-12 11:34:09

    这样可能就进入到了“意识形态”之争,其实我并不是想一定要争个谁对谁错。下面就表达一下我对“自由严格”之争的基本观点:

    第一,我不认为用interface去检查一切能够检查的东西,换句话说,用interface去表达一切library实现者对使用者的约定就是最好的design。我们需要分清楚什么东西是适合用程序语言表达的,什么东西是适合用自然语言表达的,甚至什么东西是适合用数学语言表达的(O(1), O(n)什么的不正是数学语言吗?)。程序语言不适合表达所有的东西,不然我们干嘛需要看编程书,算法书呢?

    第二,什么东西适合用程序语言去表达?我觉得程序语言归根结底是人与机器交流的平台,而不是人与人交流的平台。我们之所以提倡静态语言,是为了给计算机表达出足够的信息让计算机能够根据这些信息进行优化,同时利用计算机在编译的时候做一些类似于拼写检查的东西避免人类常犯的错误。但程序不适合用来做人与人交流的平台,哪怕是程序员与程序员交流的平台,与其设计一种语言去表达什么O(1),O(n),thread-safety之类的信息,何不更简单地在注释或者文档里写清楚?同理,技术上的检验也不是越多越好,拼写错误这些是人类常犯的错误,但是high-level的概念上的错误,比如送画画的小明去决斗却是程序员本身就不应该犯的错误。用interface这种程序语言去表达编程的思想,最终的结果就是编程的思想禁锢在程序语言设计者制定的牢笼里。

  28. 难易
    58.215.136.*
    链接

    难易 2013-04-12 12:35:23

    一句话,java和net默认程序员是啥都不知道的笨蛋,c和go假定程序员知道自己在做什么。

  29. Wuvist
    49.128.61.*
    链接

    Wuvist 2013-04-12 12:36:04

    @科技球

    我觉得我们的意思是一样的,而结果就是 @老赵 认为我们没看懂他的文章。

  30. 老赵
    admin
    链接

    老赵 2013-04-12 12:52:55

    @科技球

    我同意已经说到意识形态上面去了,我也知道很难避免,所以我说是我不喜欢,我文章一开始就说了,不是能简单作出好或不好的结论,怎么又讨论到这方面来了?同样我也说了,我不同意,但我可以理解别人的看法。

    所以,我觉得我已经做的相当到位了么。你说了好多,依然是动态静态语言争论里里常见的说法,我说这是老生常谈不要介意。

    @Wuvist

    你又来凑热闹了,说起来你们还是没理解我的意思,例如说.NET也没限制之类的,但有人理解了就成。

  31. 链接

    Hax 2013-04-12 13:20:03

    我看到Go的interface的时候也感觉有哪里存在点问题,但是说不上来。老赵解惑了啊。

    不过我觉得 @科技球 讲得也有道理。尽管C#或者更严格的语言提供了更强的约束,但是绝大多数设计者其实也不能真正善加利用。比如老赵吐槽的Java的List接口问题。与其给出不完善的约束,不如不要。Go的哲学是否就是这样?不知道我这样理解是否妥当?

  32. 老赵
    admin
    链接

    老赵 2013-04-12 13:43:19

    @Hax

    的确是有道理的,类似的讨论在动态静态语言里面已经出现过很多次了,由于Structural Typing从这方面来说正好处于两者之间,所以相似话题又出现一次。

    还有假如像Java这样有接口问题,那还不如Go这样,只是我对接口的要求比一般人的理解要高……

  33. 链接

    Haart 2013-04-12 15:30:34

    我的理解是这样的:

    老赵从实现者角度,要求接口提供强的约束(注意, 不是表达). 比如 int Accept(IList sockets) 要求参数必须是一个实现了IList接口的对象。 而不是随便从哪找来的一个带了Count属性的东西。 这样老赵在实现这个方法的时候,可以放心大胆地使用Count属性,使用下标存取元素,而不必担心搞出个油漆匠算法。

    科技球从使用者角度,要求调用的时候灵活,不必要的依赖越少越好。 既然 Accept 方法需要一个列表,那我就构造一个列表塞进去,没必要非得去实现一个IList接口,增加额外的依赖性。 如果 Accept方法 要求这个列表的元素存取和长度运算必须是O(1),那么在文档里写明就可以了。

    这两种看法都掺和了个人喜好,说到底就是个理念的区别,习惯的区别, 没必要上纲上线,定个是非高低出来。

  34. 老赵
    admin
    链接

    老赵 2013-04-12 18:05:22

    @Haart

    感觉也不好说我是实现者角度,我的要求是为实现者和使用者双方考虑的,面向接口编程。对于接口使用者来说,他要对别人传入的对象表示放心,有信心地使用。而接口的实现者,则要仔细地提供实现,然后再“小心地”标注类与接口之间的关系,这才算作提供了一个接口的实现。估计你是从这个角度说是对实现者的要求吧……

  35. 科技球
    216.239.45.*
    链接

    科技球 2013-04-13 00:55:38

    @老赵

    意识形态之争才是最有意思的讨论了:)其实我都没有想争论的意思。既然你在文章里说谈的是"go这种接口设计的缺点",那我就在comment里谈谈它的优点了,同时说明一下所谓的缺点也不见得有想象中的那么大。所以客观上也你提高了你博客的阅读体验了,倒没有想砸场子。有些话的确是老生常谈了,但是这个就有点“信者恒信”了。

    扯远一点,我觉得现在主流程序员对于OOP有点像“宗教信仰”了。于是对非OOP的一些“异端邪说”本能地排斥或者说不喜欢。但可能理性地看待OOP的思想,就会发现有些东西也不那么make sense。比如说,明明是现在主流的OOP语言都没有一个好的多重继承的模型(C++的多重继承问题多过好处),于是OOP的提倡者为了说明OOP是描述世界的模型就把世界“想像”成了一个单继承关系的树状结构,认为“万物皆Object”,但殊不知程序本身不是用来描述世界的,它只是用来描述计算。这本身就是程序思想束缚程序员思想的例子。直到go的embedded composition的出现才让人们意识到:原来我们一直使用的继承模型所带来的好处就是这些啊。

    有一些更深层次的讨论也许不太适合在这里说,毕竟这里是@老赵的地盘。况且我也说不出什么太有见地的话,很多东西其实go的开发团队在各种talk中已经说得很清楚了。我就决定结束这个讨论了。祝大家都身体健康,工作顺利,多多推动像go这种有思想的“非主流”语言的发展:)

  36. tokimeki
    61.57.134.*
    链接

    tokimeki 2013-04-13 07:27:20

    我只想表達兩點:

    1. Interface 的隱喻特徵的一致性需要設計者的配合。如果設計者(故意)不配合,編譯一樣會過,程式一樣會跑。在這一點上面,無論是顯式或隱式 Interface 都一樣。

    2. 同名的問題我覺得其實問題不大,只是編譯階段不會報錯。這部分在寫應用程式時,可以用單元測試的手法抓出可能會發生的問題。

  37. zhangyf
    114.212.87.*
    链接

    zhangyf 2013-04-13 20:23:17

    Java的ArrayList签名是这样的:

    public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    

    随机访问的特性用RandomAccess标记接口标识。

  38. 幸存者
    169.145.89.*
    链接

    幸存者 2013-04-15 10:38:52

    Structural typing 只是不用显示指定实现的接口,又不表示不需要实现接口的语义。

    在我看来 structural typing 的好处远大于它带来的问题,因为它方便了开发人员。

  39. 老赵
    admin
    链接

    老赵 2013-04-15 14:41:02

    @幸存者

    我说的就是我不希望隐式地实现语义,方便是自然的,去除接口搞Duck Typing更方便,至于Structural Typing带来的好处跟问题哪个大就没那么容易说清了。

  40. 链接

    damao 2013-04-20 10:35:03

    貌似老赵举的例子说服力不大,对于更一般的get和set,我们都知道去加上后缀getXXX, setXXX什么的,把Cowboy.Draw改成DrawGun不就了结了么,我觉得把自然语言的多义性引入程序设计然后纠结于语法,还不如把命名弄精确点,宁可多敲点键盘,程序理解起来也顺畅

  41. 老赵
    admin
    链接

    老赵 2013-04-20 21:20:56

    @damao

    呵呵,这东西难道真的都是你能控制的么?没有实现过第三方的接口吗?

  42. 链接

    damao 2013-04-21 19:04:52

    是的,现实中有很多东西我们无法控制,但是在接口定义上,总是会受到所用语言的约束,比如你举例的两接口如果是用C#定义的,那么某个第三方服务可以要求提供一个同时实现了Painter和Cowboy的XiaoWang,我调用时给出的肯定是用.NET系语言实现或包装的;如果两接口是Go定义的,那么第三方就不能像C#那样去定义XiaoWang,我用Go去调用它的服务自然就不会碰到什么问题,也就是说在同一语言系统内,就算是第三方或既成事实的定义,也不能突破这个约束,真要跨语言调用,就会采用WebService之类的中立接口技术。

  43. 老赵
    admin
    链接

    老赵 2013-04-21 21:44:04

    @damao

    听不懂你在说什么,都扯到哪儿跟哪儿了,好像完全没关系的样子,估计你也没听懂我在说什么。

  44. 链接

    damao 2013-04-22 09:52:32

    sorry,思路没整理就顺着写下来了,而且感觉有点走题,仅仅解释下上面那一大段话吧: 我的意思是,既然我用Go语言声明不出XiaoWang : IPainter, ICowBoy{...}这样的接口,那么就不会有第三方能声明出同样的接口,当我自己要实现XiaoWang的时候,代码就在控制下了,比如我的XiaoWang可能会有AsPainter和AsCowboy成员方法;剩下的就是对方要求ICowboy,结果我给了IPainter的情况了,这里StructuralTyping就有问题,但是这通常意味两个不同的领域,一般不会搞混的吧

  45. 老赵
    admin
    链接

    老赵 2013-04-22 14:50:04

    @damao

    还是听不懂……算了就这样吧,已经有很多人能听懂我的意思了……

  46. earthengine
    124.181.109.*
    链接

    earthengine 2013-04-22 18:16:27

    我倒是听懂了。他的意思是,你既然选用了Go语言,就得遵守Go的规矩。既然Go没有提供非鸭子的类型,那么当你真的需要实现你文章里所要的东西(防止要Cowboy给Painter)时,就要用不同的方法。大猫的方法是让他的类提供AsPainter等方法,让调用者明确说明需要的是什么东西。这样一来,如果还是要Cowboy给了Painter,那就是实现有问题了。

    想想看,他说的其实有道理。每种语言都有优缺点,而为了扬长避短都会有各种惯例。比如C没有名字空间,为了防止名字冲突使用了前缀命名方式。C++有自动释放机制,因此有RAII的惯例。Go还是一门年轻的语言,惯例正在形成。很可能他说的解决方式会形成一种惯例。

  47. earthengine
    124.179.212.*
    链接

    earthengine 2013-04-24 22:09:23

    刚刚看到Java 的 RandomAccess 接口,发现不同语言还真的有不同处理方式。关于List按下标访问的性能问题在Java中是通过额外标记一个接口解决的。也就是说,如果你得到一个List,要确保用下标访问不会影响性能,你应该检查它是否同时实现了空接口RandomAccess。

  48. 老赵
    admin
    链接

    老赵 2013-04-24 22:14:52

    @earthengine

    这我不认为是语言特性或文化,RandomAccess这东西感觉就是个补丁,说明Java同学也意识到这个问题了,活活。

  49. earthengine
    1.0.5.*
    链接

    earthengine 2013-04-29 14:59:46

    你这篇文章,其实涉及到多继承中类或接口的方法重名的问题。方法重名是多继承中最难处理的问题之一。就我所知每种语言的处理都不怎么完美。C++最早面对这个问题,提出了重名的语法,但被否决,因为可以用Wrapper类的方法绕过这个问题。最终解决方案是没有解决。Java的处理方法类似:不予解决。C#试图处理这个问题,引入了接口显式实现机制,但还是不完美:显式实现的方法和其他类方法相比显得是二等公民:它们不能被普通方法直接访问,而必须通过转型。为解决这个问题可以让显式方法成为调用普通方法的Wrapper,但这样就跟加Wrapper类的方法差不了多少了。

    当然,把不完美留给类库,让调用者看到完美的接口,毕竟好些。

    Eiffel采用了统一的解决方案:允许把方法实现明确重命名。但是,Eiffel的语言本身就我看来太另类,而且认受性不够。

    大猫建议的方案,其实是一个跨平台的方案:它易于理解,并可基本原样适用于以上所有平台。

  50. earthengine
    58.165.56.*
    链接

    earthengine 2013-04-29 18:05:11

    你可以说Java的RandomAccess是个补丁,但这个接口早在2002年.NET 1.0出来的时候就出现在Java 1.4里面了。如果说是Java意识到这个缺陷加以补救的话,那也是先行者的错误。没有它错误的尝试,就没有.NET后来的改进。况且,把两种接口分开好还是合并好在我看来根本没有绝对的答案。

  51. 老赵
    admin
    链接

    老赵 2013-04-30 11:55:30

    @earthengine

    “蓝精灵命名法”当然是个跨平台的易于理解的方案,只不过没法不呵呵呵……至于先行者的的错误,这个没办法,只能说Java一开始设计地真差。至于没有Java的错误是否就没有.NET的改进,这我还真觉得不一定,谁说低级错误人人都会犯一次?我就不信没有Java这个先行者,.NET就会让Stack实现List接口。这是个笑话,不是“先行者”犯的,是“低级错误”。

    不过,这种接口方面的问题在我看来答案是没什么好讨论的,假如不做到我提出的对接口的要求,真不知道很多人的“面向接口编程”是怎么搞的,要么你不搞“面向接口编程”倒也罢。

    当然我懒得继续争论这种东西。

  52. earthengine
    1.0.5.*
    链接

    earthengine 2013-05-09 10:26:00

    Stack实现List接口当然是个笑话。Java 还有数不清的类似错误,改正起来很难。如果要修复Stack的问题有几个选择:如果把它标记为过时接口,浪费了一个大好的名字。如果另加一个标记接口指出这个Stack的List接口会返回未实现异常,危险性太高。好在Annotation发展起来了,将来有机会把从Stack到List的转型,或者任何不想要的转型,利用一个附加的Annotation标记为过时。

    假泛型也有机会利用Annotation翻身成为真泛型,不就是加些语法糖的事么:通过访问Annotation完全可以获得方法或者类的真正泛型参数。不过这些都有待将来的发展了。

    可是随机访问和顺序访问是否完全分开那是另一个问题。强迫用户选择,那可以被认为是“过早优化”,因为大多数情况下两者的差别不足以影响程序质量。

    虽然多数情况下,顺序访问可以解决问题,代码也很漂亮,但也有一些很常见的情形适合下标访问。比方说你打算在枚举的时候跳过头三个元素,你会写 for (int i=3;i<list.length();++i){...}, 如果硬要改成顺序访问,就难看很多,你得有一个变量记住跳过了多少个元素。如果传过来的对象两种方法都可以用,那用户自然会选择最容易的。只有性能分析指出这里出了问题,进行优化才是合适的。

  53. 老赵
    admin
    链接

    老赵 2013-05-09 14:10:31

    @earthengine

    你知道是笑话就OK咯,所以不用说没它别人一定犯这错误。当然肯定它不是不想改,而是不能改,要保证兼容性。

    假泛型真泛型还真不是语法糖问题,是要改运行时的。

    最后你说顺序访问随机访问这种叫做“过早优化”,“不足以影响程序质量”之类的,我只能继续呵呵呵了,当然我理解很多人是怎么写程序的。

  54. Korall
    58.62.190.*
    链接

    Korall 2013-05-12 16:24:02

    Go 语言 不了解;但是像这样:“因为Structural Typing都只是从表面(成员名,参数数量和类型等等)去理解一个接口,并不关注接口的规则和含义,也没法检查。” 的说法我认为有欠妥。按取的例子说的 IPainter 和 ICowBoy ,其实所谓的接口的隐含“特征”————接口的规则和含义 是被接口的名字显式地定义或者限定了;但是 Structural Typing 一样有有检查这些隐式“特征”的能力:只要Structural Typing 正确:这里的接口其实不仅仅是 Draw 这个行为,同时也还有 Painter 和 CowBoy 的区别————— 在 Structural Typing 因为没有接口的名称所以这些隐含的区别只能显式的指定了,即在接受 Draw 这个行为的同时要显示指明是 Painter 而不是 CowBoy ,就是说所有的所需的特征全部要显式地约定了才不会让使用者误会;一旦只是约定了只需要 Draw 这个行为,那么的确所需的就是只这么一个接口,别无其他,不管这个 Draw 行为到底是以 Painter 为背景还是CowBoy 都没有关系,Structural Typing 并不是不关注接口的规则和含义,而是对接口的关注和检查在这里都是从“表面“上完成了,但这个“表面”如果再加上一个接口名字其实还一样是“表面”,并没有更强。所以在这里说 Structural Typing不关注接口的规则和含义 也没法检查 我认为理解不正确。

    但实际上因为接口的名字或者说类型——其实根本区别就是对类型的定义方式,Structural Typing 从"表面"上定义了类型,Nominal Typing 是从名字上定义了类型(不可能有名字一样但类型却不一样的吧) ——是显式地声明和定义的,但是 Structural Typing 却绕开了这个过程 从而使接口的特征、含义更加晦涩,让人把握起来更加难,我觉得这才是这里需要指向的原因。

  55. 链接

    Haart 2013-05-13 14:09:11

    @earthengine

    真假泛型的区别可没有你想象的这么简单.

    Java的泛型是假的,是因为字节码里根本就没有类型信息, 一旦编译之后就无法获得泛型模板的类型参数了. 要变成真泛型,就要修改字节码的格式, 这就意味着新的class无法在旧的jvm上跑,就好比.NET2.0的东西不能再.NET1.1上跑。

  56. Scan
    119.6.200.*
    链接

    Scan 2013-05-15 15:50:42

    go在判断是否实现接口的时候,用到了,包名、函数的签名,其中,函数签名包括函数名和参数、返回值的类型。如果仅仅是函数名,那名称相同而语意不同的碰撞很容易发生,但考虑到类型不同、以及业务接口一般由多个函数复合而成,那么这种碰撞的可能小之又小,基本不成问题。

    冲突的例子中,往往是一个被广泛实现的接口和一个领域特定的接口的碰撞,显然,特定领域的接口在设计的时候应该谨慎的考虑函数命名和参数等类型,避免和常见接口的碰撞。

    具体到老赵文中的例子,显然CowBoy的Draw应该被改名。而且掏枪一定要避免使用Draw这个单词,哪怕你明确的使用a.ICowBoy.Draw(),仍然有极大的可能被维护人员误以为是绘制语意,a.ICowBoy.Draw()能过编译器这关,但对读者来说却是坑。

    的确,接口的语意绝不仅仅是签名,但是,函数调用的语法却仅仅涉及函数名、参数、返回值这些签名信息。如果实际中遇到签名一致而语意不同的碰撞,那么,go理解不了的,代码维护者很可能也理解不了,需要的不是从语法上寻找解决方案,而是从需求、设计上考虑、整理,让代码容易理解。

  57. razor
    219.142.54.*
    链接

    razor 2013-06-17 11:03:56

    画家(Painter)和牛仔(Cow Boy)都可以有Draw这个行为

    关于这个,我想说的是,还得看场景,有时候牛仔也需要绘制,画家也需要开枪.

  58. 老赵
    admin
    链接

    老赵 2013-06-17 14:21:23

    @razor

    “画家”是身份,人需要开枪,“画家”这个身份不需要。

    当然这其实是小事,因为我看到这个问题,我就知道又来一个没看文章就来说套话的了……

  59. 幻の上帝
    125.116.100.*
    链接

    幻の上帝 2013-07-08 18:15:34

    “不喜欢Go语言式的接口”很容易理解,不过“即Structural Typing”看起来就有些奇怪了。Structural typing是类型系统的一类,不算具体的语言特性。“不喜欢”是说不喜欢所有这样的类型系统,还是不喜欢语言选择在特定的场合(例如Go用在方法上)选用structural typing呢……

    看到评论总算清楚了:“我的观点就是建立在“没人能表达出来”这点上的。”——不满Go这样强迫用户在描述接口时使用structural typing才是真相吧。为什么不把这个观点放在正文第一段呢。

  60. 老老赵
    125.70.0.*
    链接

    老老赵 2013-07-19 10:00:44

    一家之言不值得参考,每个人兴趣不同,没有必要将自己的思想强加与别人,更何况go语言秉承的思想与.net不同,相比而言go语言更注重实用性.net更注重花哨性,虽然也很喜欢.net但go语言的魅力确实不可势不可挡,应该冷静分析,楼主观点太过于偏激,不理智,建议读者慎重分析,理智阅读!

  61. 老赵
    admin
    链接

    老赵 2013-07-19 14:41:01

    @老老赵

    神经病,什么“一家之言不值得参考”,你现在说的不是一家之言么?谁强加于别人了?我没有冷静分析么?哪里偏激不理智了?你说的话才叫神经病。

  62. wuwei
    210.42.123.*
    链接

    wuwei 2013-08-30 11:09:14

    楼主希望接口表达出更多的约束,比如时间复杂度等,但是这些约束如果无法实现自动检测,就需要实现者遵守。从这个角度讲,楼主的观点显得牵强。我想楼主想要的是dependent type吧。

  63. linustd
    125.39.155.*
    链接

    linustd 2013-11-20 09:08:14

    一个非微软平台的,毕业3年之内,甚至还没毕业的新人,就可以把微软MVP,特约讲师,业界有名的微软/.net专家高手等,给呛得和孙子似的, 这足以说明一切。

  64. 老赵
    admin
    链接

    老赵 2013-11-20 10:02:43

    @linustd

    嗯?您在说谁把谁呛得和孙子似的?我怎么没看出来…

  65. zzzzd
    183.15.238.*
    链接

    zzzzd 2013-12-02 11:37:09

    C# 中方法不能const修饰,所以不显示要求Count方法不能修改集合内容,C++中就有这个特性,给方法加了const修饰后,如果你方法内修改了集合内容编译就会报错,我很奇怪就是C#为啥不引入这个特性,像这样

    int Count() const {get;}
    
  66. simapple
    27.184.200.*
    链接

    simapple 2014-02-22 09:42:15

    最近看了一篇go语言,感觉和其他一些语言的方式差不多,吸收模仿了其他语言的特性和语法糖,在为go添加上独特的特性

  67. Quon
    122.225.33.*
    链接

    Quon 2014-03-19 12:15:30

    这哪是在喷go啊,这是在喷duck typing啊

  68. 链接

    Raezzium 2014-04-18 09:06:05

    个人感觉,接口,本身概念,就是个公众的东西。USB是接口,随便焊两根线也算接口。这就是两者接口的区别。相信大部分人都觉得使用usb比焊线舒服多,尤其是东西复杂之后,很多细节不用自己关心太多。但是对于简单的接口来说,焊线方法是比较灵活一点。

    明显Go是轻量级的语法,.net是中/重量级的语法。但是Go定位自己为系统级语言,就有点错误了。好比拆开自己的电视机,看到板上到处焊满了线,而不是一块块精致的电路板。

  69. 链接

    Qingfeng Lee 2014-06-23 18:52:44

    在文章里看到喷Java的List接口的就感觉挺郁闷的(Go语言不太懂), 在文章里我看到的不是客观的评价,而是满满的主观偏见和嘲笑(笑话?这种单词说出来真的好么?),

    接口本身的精髓就是面向对象的继承和多态, ArrayList和LinkedList都可以get,这是他们共同拥有的能力,是用List那里继承过来的,但是这两个类的特性不同,针对get所做的事情是不同的。这才能体现出子类之间的差异。如果某个行为的实现方式不同就要起一个不同的方法名,那还要接口干什么。 当然你也可以说我的观点是主观偏见。但是我最少不会把我的主观偏见放到公众眼皮下拉仇恨值。

    再一个我的主观偏见就是: 像老赵这样的资深程序员,对于程序语言有着深刻认识的人,更不应该对各种语言有这种主观偏见和鄙视,因为我怎么想也不觉得老赵会比Go语言的设计者和设计Stack这种笑话的人更牛逼。

  70. 老赵
    admin
    链接

    老赵 2014-06-25 23:20:28

    @Qingfeng Lee

    啊哈哈,技术方面就懒得说的,话说你哪儿学来的那么多套话?

  71. 幻の上帝
    115.220.105.*
    链接

    幻の上帝 2014-07-08 18:38:13

    @Qingfeng Lee

    又是个Java学傻的?

    “接口本身的精髓就是面向对象的继承和多态”……这是在黑接口么。接口什么时候就是面向对象的专利了?多态……恐怕连inclusion polymorphism是啥都没概念吧。多态就必须通过子类型体现?

    “主观偏见放到公众眼皮下拉仇恨值”?上面这不就是拿无知当偏见来拉仇恨了么。

    Java那种Stack的设计作为笑话,差不多可以算公案了。连这个都要洗……Java学清楚了么?

  72. shihangw
    71.203.120.*
    链接

    shihangw 2014-11-10 05:57:12

    我喜欢Haskell的显式声明Structural typing

    instance Cowboy XiaoMing where
     draw Xiaoming ...
    
    instance Painter XiaoMing where
     draw XiaoMing ...
    

    不过其实即使不显式声明,像你说的出错场景其实也是很少见的……前提是你的公司全是优秀的程序员

  73. conjohn668
    114.111.167.*
    链接

    conjohn668 2014-12-25 23:22:02

    Go估计是不想把自己标记成OOP语言,为了不这样而那样

    这点语言特性,和双返回值确定err一样,有些莫名其妙

  74. dearbaba
    192.250.192.*
    链接

    dearbaba 2016-01-24 14:00:57

    看了您的博文总结的非常好,您的博客非常不错。我也推荐一个程序员必备的搜索博客问答的网站 http://www.itdaan.com 。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我