第13天!昨天谈到了class variable,class instance variable和instance variable,也发现在实务上,类别实体变数和实体变数才是主流。今天我们要多谈两个跟前一篇的变数有关的方法:instance_eval和class_eval。让每天都主题都环环相扣!

 

Ruby经典面试题目#13

instance_eval和class_eval的差别?What's the difference between instance_eval and class_eval?

 

instance_eval

昨天文章提到一个重要概念:能够读取变数的属性是非常重要的,让我们可以更方便的读取名称相同,但其实值不同的物件。eval代表着evaluation,有估值、取值的意涵。让我们把昨天的attr_accessor概念引入,马上来写程序码实验instance_eval:

 

[instance_eval案例A:attr_accessor]

由过去几天的写作经验,我发现一篇文章的开头最难下笔、也是最重要的,举例能让自己懂(还有让我的读者、观众、加油群啦啦队懂)更不是容易的事,所以我习惯从自身生活经验出发,把写程序变成像写日记一样有趣、贴近生活。:)

 

话说最近令我期待的事情是,再过10天就要跑马拉松了!因此我打算建立RunMarathon类别,new出两个物件hm半程马拉松和fm全程马拉松,并各自指定对应的km公里数值:

 

class RunMarathon

attr_accessor:km

end

 

hm = RunMarathon.new

hm.km = 21

 

fm = RunMarathon.new

fm.km = 42

 

p hm # => #<RunMarathon:0x000055f60ed4f0d0 @km=21>

p fm # => #<RunMarathon:0x000055f60ed4f0a8 @km=42>

 

p hm.km

p fm.km

p hm.instance_eval { @km } # 21和hm.km的结果相同

p fm.instance_eval { @km } # 42和fm.km的结果相同

 

p RunMarathon.instance_methods(false)#[:km=,:km]

这里用到两个instance_methods实体方法km=(写入值)和km(读出值)。

 

如果我们用.instance_eval方法取值,结果显示:

 

#<RunMarathon:0x000055f60ed4f0d0 @km=21>

#<RunMarathon:0x000055f60ed4f0a8 @km=42>

21

42

21

42

[:km=,:km]

很好!成功用instance_eval印出值了!

 

[instance_eval案例B:只用initialize()方法]

还记得第一天提到的初始化方法initialize,建立实体变数。

 

我们可以将代码改写为在RunMarathon类别加入initialize()方法,让我们在new出物件的同时传入公里数,代码变成如下:

 

class RunMarathon

def initialize(km)

@km = km

end

 

def km

@km

end

end

 

hm = RunMarathon.new(21)

fm = RunMarathon.new(42)

 

p hm

p fm

 

p hm.km

p fm.km

p hm.instance_eval { @km } # 21和hm.km的结果相同

p fm.instance_eval { @km } # 42和fm.km的结果相同

 

p RunMarathon.instance_methods(false)#[:km]

我们使用了.instance_methods确认目前用到哪些实体方法:

 

#<RunMarathon:0x000055c2a0e3eac8 @km=21>

#<RunMarathon:0x000055c2a0e3eaa0 @km=42>

21

42

[:km]

以上显示,之前使用到的一对实体方法(读日记、与写日记xyyfl),只剩下读取:km。km=(写入值)已经不见了!

 

仔细思考一下,原因是我们将案例A的RunMarathon类别中attr_accessor:km这段代码,以.initialize()方法取代。方法变了,变数的传值方式也会不同。以下的这段:

 

hm = RunMarathon.new

hm.km = 21

 

fm = RunMarathon.new

fm.km = 42

被改写成:

 

hm = RunMarathon.new(21)

fm = RunMarathon.new(42)

以上观念是把昨天+今天的一起整合复习。

 

[instance_eval案例C:只用initialize()方法,但将def km方法删除]

如果,我们把RunMarathonclass的定义公里变数方法:

 

def km

@km

end

移除,会发生什么事呢?

 

(我想你应该猜到了,会影响到hm.km和fm.km,用到km的这两行代码:)

 

class RunMarathon

def initialize(km)

@km = km

end

end

 

hm = RunMarathon.new(21)

fm = RunMarathon.new(42)

 

p hm

p fm

#p hm.km #undefined method `km'(NoMethodError)

#p fm.km #undefined method `km'(NoMethodError)

 

p hm.instance_eval { @km }

p fm.instance_eval { @km }

 

p RunMarathon.instance_methods(false)#[]

没有方法了。hm.km和hm.fm找不到方法(NoMethodError)。

 

我们用注释#消去无用的这两行。

 

然而.instance_eval如往常一样坚守岗位帮我们印出值。

 

此时.instance_methods的印出结果显示出,此时我们并没有用到任何的实体方法。

 

#<RunMarathon:0x000055cb6e5142f0 @km=21>

#<RunMarathon:0x000055cb6e5142c8 @km=42>

21

42

[]

为了更近一步了解,我去Ruby-doc.org查到这段话:

 

instance_eval evaluates a string containing Ruby source code,or the given block,within the context of the receiver(obj).In order to set the context,the variable self is set to obj while the code is executing,giving the code access to obj's instance variables and private methods.出处

 

我发现instance_eval用来定义于任何的object(包含class,因为类别也是一种物件),还可以存取到私有方法private method!立马来写code研究一下。

 

[instance_eval案例D:存取private method]

话说在我心深处藏了一个人生愿望:跑超级马拉松(ultramarathon,公里数超过50以上的马拉松),因此我决定把这个内心秘密放在private method里:

 

class RunMarathon

def initialize(km)

@km = km

end

 

private

def my_resolution

“I'm going to run ultrathon #{@km} in the future!”

end

end

 

um = RunMarathon.new(100)

p um

p um.instance_eval { @km }

p um.instance_eval { my_resolution }

结果显示为:

 

#<RunMarathon:0x0000564cf8966b58 @km=100>

100

“I'm going to run ultrathon 100 in the future!”

利用.instance_eval{private method}探寻内心深处,好热血的人生宣言啊~

 

class_eval

如果我们想要提取值很多次,又不想一直重复写这样的代码:

 

p hm.instance_eval { @km } #告诉我半马公里数!

p fm.instance_eval { @km } #告诉我全马公里数!

p um.instance_eval { @km } #告诉我超马公里数!

 

只要看到代码有重复的部分,我们就可以思考,如何将重复概念提升到class类别的层次,变成class_eval:

 

class RunMarathon

def initialize(km)

@km = km

end

end

 

RunMarathon.class_eval do #放RunMarathon类别的外面!定义新的类别方法

def km

@km #这个是类别实体变数唷!

end

end

 

hm = RunMarathon.new(21)

fm = RunMarathon.new(42)

 

p hm

p fm

p hm.km #21与hm.instance_eval {@km}值相同

p fm.km #42与fm.instance_eval {@km}值相同

 


p RunMarathon.instance_methods(false)#[:km]

结果如下:

 

#<RunMarathon:0x00005619eeb8ec88 @km=21>

#<RunMarathon:0x00005619eeb8ec60 @km=42>

21

42

[:km]

瞧!是不是跟[instance_eval案例B:只用initialize()方法]这里所举的例子一。模。一。样!

 

为什么

 

class RunMarathon

def initialize(km)

@km = km

end

end

 

RunMarathon.class_eval do #放外面!定义类别方法

def km

@km #这个是类别实体变数唷!

end

end

 

 

class RunMarathon

def initialize(km)

@km = km

end

 

def km

@km

end

end

会出现相同的结果呢?

 

我在史丹佛大学CS142课程这篇教材找到解答(npx133):

 

class_eval is equivalent to typing the code inside a class statement.

 

以更简单的构架为例好了:

 

MyClass.class_eval do

def num

@num

end

end

会等于

 

class MyClass

def num

@num

end

end

真是太神奇了!

 

总结

所以回到今天最一开头的举例[instance_eval案例A:案例B:案例C],透过移除部分的代码做实验,从instance_eval,串到class_eval,再串回到instance_eval,好像又回到初衷、豁然开朗的感觉呢!

 

我也领悟到了,其实程序写法都可以换来换去,重点是,你想实现的功能是什么?不同的写法之间又有什么优缺点比较?像在这篇提到:class_eval概念,跟module_eval是类似的,拿来用作扩充rails gem所定义的class,这也许可以当我第20天候的文章素材idea!

 

最后,来复习一下昨天的变数比一比!

 

类别实体变数class instance variable实体变数instance variable

@类别实体变数@实体变数

可用attr_accessor的方式改写可用attr_accessor的方式改写

用在类别方法,不可用在实体方法用在实体方法

刚好在今天的例子class_eval、instance_eval,昨天了解的:类别实体变数和实体变数都有派上用场:)

也许这就是一种「过去每天累积的努力,成就现在的自己」最佳的例子吧!