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

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

public 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的优点,说回调多么多么美妙我会深不以为然——只是没法反抗开始享受罢了嘛……

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

最新文章

  1. 深入理解css系列:css定位
  2. 《JavaScript权威指南》学习笔记 第四天 数组
  3. JavaScrip的DOM操作(13号讲)
  4. fastclick插件 导致 input[type=&quot;date&quot;] 无法触发问题解决方案
  5. Uber明年在中国将继续补贴,并大举进军100个城市!
  6. java反射入门
  7. 如何给Ubuntu 安装Vmware Tools
  8. EF Core新增迁移时无法加载程序集“System.ValueTuple”的错误
  9. 闭包,jQuery插件的写法:图片预加载
  10. Java Web应用开发中的一些概念
  11. authentication failed for xxx错误
  12. python os.path.expanduser()
  13. js点击显示隐藏
  14. Archlinux 遇到的坑
  15. c# Parallel.For 并行编程 执行顺序测试
  16. 多线程编程——ANR
  17. EF Code-First数据迁移
  18. jQuery实现淡入淡出二级下拉导航菜单的方法
  19. MVC 中 注册不成功 或其他操作不成功 提示办法
  20. Selenium(Python) ddt读取CSV文件数据驱动

热门文章

  1. netty 集成 wss 安全链接
  2. CSS----盒子模型与浮动
  3. maintenance
  4. 函数式编程语言(Functional Program Language)
  5. linux启动jmeter(二十三),执行./jmeter.sh报错解决方法(转载)
  6. java面试题:Spring
  7. mysql5.7.20更改root密码
  8. centos 6.9 NTP基准时间服务器配置
  9. (转)学习ffmpeg官方示例transcoding.c遇到的问题和解决方法
  10. MongoDB修改默认数据库