iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 13
1
Modern Web

30天修煉Ruby面試精選30題系列 第 13

Day13 - Ruby比一比: instance_eval 和 class_eval方法

第13天! 昨天談到了class variable, class instance variable和instance variable,也發現在實務上,類別實體變數和實體變數才是主流。今天我們要多談兩個跟前一篇的變數有關的方法:instance_evalclass_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]

以上顯示,之前使用到的一對實體方法(讀日記、與寫日記),只剩下讀取:kmkm=(寫入值)已經不見了!

仔細思考一下,原因是我們將案例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.kmfm.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.kmhm.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課程這篇教材找到解答:

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_evalinstance_eval,昨天了解的:類別實體變數實體變數都有派上用場:)
也許這就是一種「過去每天累積的努力,成就現在的自己」最佳的例子吧!

===

Ref:


上一篇
Day12 - 千變萬化的變數: class variable, class instance variable 與 instance variable
下一篇
Day14 - Ruby比一比: #each #map 和 #collect method
系列文
30天修煉Ruby面試精選30題31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言