回想起咱门初学C跟Java语言的时候,或许会以为这个世界上只有这两门语言。 当时老师或者教科书肯定不是一上来就教你如何用这门语言去连接数据库,而是要求你用这门语言去实现一些简单的数据结构,以及排序这类简单的算法。Oh,sorry,我可能让你回忆起一些不太好的事情了。特别是后来背弃了编程这条路的同学,你们一定觉得这个过程特别的枯燥,而且艰辛。
还记得那时候我们为了实现一种叫做先进先出的被呼唤为队列的数据结构,以及一种后进先出的被呼唤为栈的数据结构耗费了我们多少时间了吗?现在的你肯定不会手动去写这些数据结构了,别人早就写好的代码很容易就能够在网上找到,我们直接用便可(当然那时候我们不知道有Github这种东西)。
后来,我决定要走一条不寻常的路(研习一门动态语言)的时候。我发现更不用做这些事情了,所谓的栈,队列,这些数据结构,在许多语言的内置库里面都已经有类似的实现。今天,请允许我用Ruby来讲这个事情,另外我会结合Ruby的Forwardable
模块,优雅地实现方法代理。
1. 简单模拟栈跟队列
Ruby的内置类 Array
的存在,为我们简单操作序列型数据提供了可能,我们只需要用字面量的方式就能够很容易地创建一个序列
array = [1,2,3,4]
=> [1, 2, 3, 4]
然后?我们可以基于Array
的实例来简单地模拟栈或者队列的行为。
1)模拟栈
我只需要简单地使用Array#shift
以及Array#unshift
两个实例方法就能够很简单地模拟栈的功能
# 栈的实现
pry(main)> array
=> [1, 2, 3, 4]
pry(main)> array.unshift(1)
=> [1, 1, 2, 3, 4]
pry(main)> array.shift()
=> [1]
pry(main)> array
=> [1, 2, 3, 4]
是不是很简单?我只需要反复调用上面两个方法,就能够完成栈能完成的事情了。
2)模拟队列
接下来模拟队列,队列也比较简单,我们只需要调用Array#push
方法就可以在队列的末尾插入一个元素。然后通过Array#shift
方法,移除并获取队列头部的元素。
# 队列的实现
pry(main)> array
=> [1, 2, 3, 4]
pry(main)> array.push(1)
=> [1, 2, 3, 4, 1]
pry(main)> array.shift()
=> [1]
pry(main)> array
=> [2, 3, 4, 1]
但是如果在平时的业务代码中我们给队友们提供这样的队列,或者栈的话。你可能就见不到明天的太阳了,我相信软件行业里面烂代码吸引的仇恨,并不亚于英雄联盟里面的猪队友。那我们一般会怎么做?请接着往下读。
2. 定义队列或者栈的相关类
更加语义化的方式是定义相关的类(Stack,Queue),然后封装对应的操作方法。这样,当别人用到我们定义好的有特定行为的类的时候心情就会更加舒坦。我们代码可读性也更高。我开篇一直在扯语言的事情,就是提醒一下在我们使用动态语言的时候,请尽量跳出静态语言的思维定势。很多事情其实可以更简单。动态语言往往更关注行为,而不是类型。
1) 屌丝方案
class Queue
def initialize
@q = []
end
def enq(*argument_list)
@q.push(*argument_list)
end
def deq(*argument_list)
@q.shift(*argument_list)
end
end
我知道现在的代码并不是很好看,但是请相信我,最起码它是可以运行的
pry(main)> q = Queue.new
=> #<Thread::Queue:0x007ff00f0ccfe0 @q=[]>
pry(main)> q.enq(1,2,3,4)
=> [1, 2, 3, 4]
pry(main)> q
=> #<Thread::Queue:0x007ff00f0ccfe0 @q=[1, 2, 3, 4]>
pry(main)> q.deq()
=> 1
pry(main)> q
=> #<Thread::Queue:0x007ff00f0ccfe0 @q=[2, 3, 4]>
pry(main)> q.deq(2)
=> [2, 3]
pry(main)> q
=> #<Thread::Queue:0x007ff00f0ccfe0 @q=[4]>
虽然有点粗糙,但最起码它也是一个队列。它能够完成队列的基本行为。
2) 高富帅的写法
初入Ruby难免会写出一些比较矮穷搓的代码,没事,随着我们经验的累积,我相信我们会慢慢写出高富帅的代码出来。为了让上述的代码更简短一些,我们可以增强原有类的功能。Ruby经常会使用Module
来完成增强任务。下面介绍一个叫做Forwardable
的模块,它提供了一些好用的类方法,可以帮我们简单地定义对应类的一些实例方法,并且,把定义好的实例方法代理到实例变量相关的某个方法上。这样说可能有点迷糊,我这里列举一个官方文档的例子。
require 'forwardable'
class Queue
extend Forwardable
def initialize
@q = []
end
def_delegator :@q, :push, :enq
def_delegator :@q, :shift, :deq
end
没错,已经写完了。这就是Forwardable
模块的最直接的用法,类Queue
通过扩展模块Forwardable
,就会得到Forwardable::def_delegator
这样的类方法,通过这个类方法定义实例方法Queue#enq
并把功能代理到Array#push
这个实例方法上。在解析环境中运行以上代码。得到的结果跟之前的例子是一样的。
pry(main)> q = Queue.new
=> #<Thread::Queue:0x007ff00f0ccfe0 @q=[]>
pry(main)> q.enq(1,2,3,4)
=> [1, 2, 3, 4]
pry(main)> q
=> #<Thread::Queue:0x007ff00f0ccfe0 @q=[1, 2, 3, 4]>
pry(main)> q.deq()
=> 1
pry(main)> q
=> #<Thread::Queue:0x007ff00f0ccfe0 @q=[2, 3, 4]>
pry(main)> q.deq(2)
=> [2, 3]
pry(main)> q
=> #<Thread::Queue:0x007ff00f0ccfe0 @q=[4]>
好吧,我也知道废话很多,其实主要想说明两件事情
- 有些东西用动态语言来写虽然运行效率会稍微慢一点,但是胜在灵活,很多时候我们可以更优雅地去实现一些功能。
- Ruby以
Plugin
的方式来增强原来的类的行为,提供一些方便我们日常使用的语法糖让我们的代码更精炼。*
如上面的Forwardable
模块提供了Forwardable#def_delegator
类方法,让我们可以方便地实现代理功能,而不需要手动地去实现对应的细节。毕竟我们都知道写得越多错的越多。
3. 写在最后
这篇文章作为技术文章似乎有点短,并且瞎扯淡的成分居多。我就是想黑一下Java,然后用Ruby的方式来实现一个简单的队列。主要是为了展示一下Ruby的优雅。至于Forwardable
模块,我对它了解目前还只是停留在应用层面。说实在的我对它的源码很感兴趣,希望后面有机会可以再写一篇文章深入剖析它的源代码。(好吧,我也承认我不在这篇文章剖析它的源代码是因为我根本就还没看懂它源代码,他们写得有点深奥。)
如果您还觉得意犹未尽时间尚早,请用Java去实现相应的队列以及栈数据结构吧。