设为首页 加入收藏

TOP

基于类型系统的面向对象编程语言Go(二)
2015-04-07 15:30:07 来源: 作者: 【 】 浏览:92
Tags:基于 类型 系统 面向 对象 编程语言
o() 效果一致。


区别于其他语言,Go语言很清晰地告诉你类的内存布局是怎么样的。在Go语言中你还可以随心所欲地修改内存布局,如:


这段代码从语义上来说,和上面给例子并无不同,但内存布局发生了改变。“基类”Base的数据被放在了“派生类”Foo 的最后。


另外,在Go语言中你还可以以指针方式从一个类“派生”:


这段Go代码仍然有“派生”的效果,只是Foo创建实例的时候,需要外部提供一个Base类实例的指针。C++ 中其实也有类似的功能,那就是虚基类。但是虚基类是非常让人难以理解的特性,普遍上来说 C++ 的开发者都会遗忘这个特性。


Go语言对关键字的增加非常吝啬。在Go语言中没有private、protected、public这样的关键字。要想某个符号可被其他包(package)访问,需要将该符号定义为大写字母开头。如:


这样,Rect类型的成员变量就全部被public了。成员方法遵循同样的规则,例如:


这样,Rect的area方法只能在该类型所在的包(package)内使用。


需要强调的一点是,Go语言中符号的可访问性是包(package)一级的,而不是类一级的。尽管area是Rect的内部方法,但是在同一个包中的其他类型可以访问到它。这样的可访问性控制很粗旷,很特别,但是非常实用。如果Go语言符号的可访问性是类一级的,少不了还要加上friend这样的关键字,以表示两个类是朋友关系,可以访问其中的私有成员。


Rob Pike曾经说,如果只能选择一个Go语言的特性移植到其他语言中,他会选择接口。


接口(interface)在Go语言有着至关重要的地位。如果说goroutine和channel 是支撑起Go语言的并发模型的基石,让Go语言在如今集群化与多核化的时代,成为一道极为亮丽的风景;那么接口(interface)是Go语言整个类型系统(type system)的基石,让Go语言在基础编程哲学的探索上,达到史无先例的高度。


我曾在多个场合说,Go语言在编程哲学上是变革派,而不是改良派。这不是因为Go语言有 goroutine和channel,而更重要的是因为Go语言的类型系统,因为Go语言的接口。因为有接口,才让Go语言的编程哲学变得完美。


Go 语言的接口(interface)不单单只是接口。


为什么这么说?让我们细细道来。


其他语言(C++/Java/C#)的接口


Go语言的接口,并不是你之前在其他语言(C++/Java/C#等)中接触到的接口。


在Go语言之前的接口(interface),主要作为不同组件之间的契约存在。对契约的实现是强制的,你必须声明你的确实现了该接口。为了实现一个接口,你需要从该接口继承:


```C++
class Foo : public IFoo { // C++ 文法
...
}


IFoo* foo = new Foo;
```


哪怕另外存在一个一模一样的接口,只是名字不同叫IFoo2(名字一样但是在不同的名字空间下,也是名字不同),上面的类Foo只实现了IFoo,但没有实现IFoo2。


这类接口(interface),我们称之为侵入式的接口。“侵入式”的主要表现在于实现类需要明确声明自己实现了某个接口。


这种强制性的接口继承,是面向对象编程(OOP)思想发展过程中的一个重大失误。我之所以这样讲,是因为它从根本上是违背事物的因果关系的。


让我们从契约的形成过程谈起。设想我们现在要实现一个简单搜索引擎(SE)。该搜索引擎需要依赖两个模块,一个是哈希表(HT),一个是HTML分析器(HtmlParser)。


搜索引擎的实现者认为,SE对哈希表(HT)的依赖是确定性的,所以他不并认为需要在SE和HT之间定义接口,而是直接import(或者include)的方式使用了HT;而模块SE对HtmlParser的依赖是不确定的,未来可能需要有WordParser、PdfParser等模块来替代HtmlParser,以达到不同的业务要求。为此,他定义了SE和HtmlParser之间的接口,在模块SE中通过接口调用方式间接引用模块HtmlParser。


应当注意到,接口(interface)的需求方是搜索引擎(SE)。只有SE才知道接口应该定义成什么样子才比更为合理。但是接口的实现方是HtmlParser。基于模块设计的单向依赖原则,模块HtmlParser实现自身的业务时,不应该关心某个具体使用方的要求。HtmlParser在实现的时候,甚至还不知道未来有一天SE会用上它。 要求模块HtmlParser知道所有它的需求方的需要的接口,并提前声明实现了这些接口是不合理的。同样的道理发生在搜索引擎(SE)自己身上。SE并不能够预计未来会有哪些需求方需要用到自己,并且实现他们所要求的接口。


这个问题在标准库的提供来说,变得更加突出。比如我们实现了File类(这里我们用Go语言的文法来描述要实现的方法,请忽略文法上的细节),它有这些方法:


那么,到底是应该定义一个IFile接口,还是应该定义一系列的IReader, IWriter, ISeeker, ICloser接口,然后让File从他们继承好呢?脱离了实际的用户场景,讨论这两个设计哪个更好并无意义。问题在于,实现File类的时候,我怎么知道外部会如何用它呢?


正因为这种不合理的设计,使得Java、C# 的类库每个类实现的时候都需要纠结:


在Go语言中,一个类只需要实现了接口要求的所有函数,那么我们就说这个类实现了该接口。例如:


这里我们定义了一个File类,并实现有Read,Write,Seek,Close等方法。设想我们有如下接口:


尽管File类并没有从这些接口继承,甚至可以不知道这些接口的存在,但是File类实现了这些接口,可以进行赋值:


Go语言的非侵入式接口,看似只是做了很小的文法调整,但实则影响深远。


其一,Go语言的标准库,再也不需要绘制类库的继承树图。你一定见过不少C++、Java、C# 类库的继承树图。这里给个Java继承树图:


http://docs.oracle.com/javase/1.4.2/docs/api/overview-tree.html


在Go中,类的继承树并无意义。你只需要知道这个类实现了哪些方法,每个方法是啥含义就足够了。


其二,实现类的时候,只需要关心自己应该提供哪些方法。不用再纠结接口需要拆得多细才合理。接口是由使用方按需定义,而不用事前规划。


其三,不用为了实现一个接口而import一个包,目的仅仅是引用其中的某个interface的定义,这是不被推荐的。因为多引用一个外部的package,就意味着更多的耦合。接口由使用方按自身需求来定义,使用方无需关心是否有其他模块定义过类似的接口。


接口(interface)的赋值在Go语言中分为如下2种情况讨论:


先讨论将某种类型的对象实例赋值给接口。这要求该对象实例实现了接口要求的所有方法。例如,在之前我们有实作过一个Integer类型,如下:


相应地,我们定义接口LessAdder,如下:


现在有个问题:假设我们定义一个Integer类型的对象实例,怎么其赋值给LessAdder接口呢?应该用下面的语句(1),还是语句(2)呢?


答案是应该用语句(1)。原因在于,Go语言可以根据


这个函数自动生成一个新的Less方法:


这样,类型 *Integer就既存在Less方法,也存在Add方

首页 上一页 1 2 3 下一页 尾页 2/3/3
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇Java 不使用工具包把json更快的转.. 下一篇12条自问让你更好地编程

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: