Ruby 学习笔记簿:Metaprogramming Quizzes

Quiz: Missing Lines

题目说明:

下面程序码以图表方式呈现,可看出物件与类别之间的关联:

class MyClass
end

obj1 = MyClass.new
obj2 = MyClass.new

https://ithelp.ithome.com.tw/upload/images/20201025/20120868yX4mA39vUY.png

请回答以下问题:

1. What’s the class of Object? 答案: Class

2. What’s the superclass of Module? 答案: Object

3. What’s the class of Class? 答案: Class

4. 执行以下程序码并画出以上答案的关联?

  obj3 = MyClass.new obj3.instance_variable_set('@x', 10)

答案

https://ithelp.ithome.com.tw/upload/images/20201025/20120868nLI0LAS6XG.png

Quiz: Tangle of Modules

题目说明:

请先看以下程序码:

module Printable 
  def print
    "Printable#print"
  end

  def prepare_cover
    # ...
  end
end

module Document
  def print_to_screen
      prepare_cover
      format_for_screen
      print
  end

  def format_for_screen
    # ...
  end
  def print
    "Document#print"
  end
end

class Book
  include Document 
  include Printable 
  
  # ...
end

当我们建立了 Book 的实例物件 b,并呼叫 print_to_screen()方法。

b = Book.new
b.print_to_screen

假设这个时候问题出现了,因为输出的字串是错误的,这代表呼叫了不对的 print()方法

请回答以下问题:

1. 请问我们到底呼叫了哪ㄧ个版本的 print()? Printable or Document?

2. 请在纸上画出 ancestors chain

3. 请帮忙解 Bug,让 print_to_screen() 可以呼叫正确的 print()

题目讨论

首先我们可以请 Ruby 给点提示:

Book.ancestors # => [Book, Printable, Document, Object, Kernel, BasicObject]

从提示里,我们得知 ancestor's chain 正确的面貌。在问题中并没有特别说 Book 是继承自哪个类别,因此如果没有引入任何模组的话,在预设情况下,Book 是继承自 Object。

但是当 Book 用 include 引入了 Document 模组,Ruby 会把 Document 模组加在 Book 的上面,也就成为 Book 的父层。接着又再 include 了 Printable 模组,Ruby 会再把 Printable 模组插入在 Book 的上面。此时的 ancestor's chain 的图就会变成这样:

https://ithelp.ithome.com.tw/upload/images/20201025/20120868qtcTZ2MP8T.png

还记得 Chapter 2 介绍的 Method Lookup 吗?

当呼叫 b.print_to_screen 时,实例物件 b 会变成是当下执行的物件,也就是 self。接着依 『 黄金法则:往右ㄧ步,再往上 』 "One step to the right, then up",往右会先找到 Book 类别,再往上ㄧ直找到 Document#print_to_screen()。

进入 print_to_screen() 内,就准备开始执行里面的方法。不过 print_to_screen()里面的方法(包括 print)都没有明确的接收者(receiver),因此 method lookup 只能再次从 Book 往上找。以 print() 方法来说,最接近 Book 的 print() 方法会在 Printable 模组找到。

要解决掉 Bugs 可以有两种做法:

  1. 重新命名 Printable 模组里的 print() 方法: 如此一来 method lookup 就只会在 Document 找到 print()。
  2. 把 Book 类别内引入模组的顺序互换: 让 Document 模组变成最接近 Book 就会先在 Document 找到 print()。

Quiz: Bug Hunt

题目说明:

请找出以下程序码的错误并加以修正:

1     class Roulette
2       def method_missing(name, *args)
3         person = name.to_s.capitalize
4         3.times do
5           number = rand(10) + 1
6           puts "#{number}..."
7         end
8         "#{person} got a #{number}"
9       end
10    end

number_of = Roulette.new
puts number_of.bob
puts number_of.frank

正确的输出应该是这样:

#   5...
#   6...
#   10...
#   Bob got a 10
#   7...
#   4...
#   3...
#   Frank got a 3

题目讨论

如果我们直接拿以上错误程序码去跑跑看,会得到类似下面无限回圈的错误讯息 (SystemStackError)。

找出这个错误讯息应该不算是太难,但是要了解错误发生的原因就不是那麽浅显易见了。首先可以看出 number 变数是在 do...end 的 block 中定义的,然後会再传给 times() 方法。

还记得在 Chapter 4 中提到 Scope 的概念,在block内定义的变数,其作用域只限於 block 里。因此当程序跑到第8行的 number 时,Ruby 其实会认为 number 是方法,而不是在第 5 行所定义的变数。

假设我们在 Roulette 类别内定义的方法不是命名为 method_missing,其实错误讯息就是常见的名称错误(NameError),但题目的陷阱或是有趣的地方就在於方法名称是 method_missing

我们知道当 Ruby 找不到方法或是无法回应当物件时,就会呼叫 BasicObject 所内建 method_missing() 方法,因此程序跑到第 8 行的 number 时,会错认为是方法,又在 self (Roulette的实例物件) 里找不到 number 方法,所以呼叫 method_missing(),才造成了无穷回圈的问题。

解决方案

分享自己快速但觉得不是很好的解法:设定 number 为全域变数,程序就不会错认为第 8 行的 number 是方法了。

1     class Roulette
2       def method_missing(name, *args)
3         person = name.to_s.capitalize
4         3.times do
5           $number = rand(10) + 1
6           puts "#{$number}..."
7         end
8         "#{person} got a #{$number}"
9       end
10    end

书中提供的方法

1      class Roulette
2        def method_missing(name, *args)
3          person = name.to_s.capitalize
4          super unless %w[Bob Frank Bill].include? person
5
6          number = 0
7          3.times do
8            number = rand(10) + 1
9            puts "#{number}..."
10         end
11         "#{person} got a #{number}"
12        end
13      end

参考资料:

本篇所有题目都是来自於 Metaprogramming Ruby 2nd


<<:  39.vue.config.js2

>>:  BEM 基础介绍 DAY41

TailwindCSS 从零开始 - 每个 Utility class 都支援响应式与伪类

手刻响应式网站,对我来说其实没有到很困难,但如果页面一多,时程又赶,就会很麻烦,而 Tailwin...

[Day2] Jetpack Compose: UI要怎麽排列?

#布局 接续昨天的例子,我们如果新增一个Greeting("Jetpack Compose...

27 - EditorConfig + Prettier + ESLint + Stylelint + Markdownlint - All you can lint

将各式 linter 与 formatter 工具整合於同个专案中,让开发者可以: 使用 Edito...

33岁转职者的前端笔记-DAY 1 前言

这是第一次参加铁人赛 原本是想要在转职期间来挑战写个 30天 的学习笔记 但在开赛前找到相关接案工作...

鬼故事 - 这东西真烂

鬼故事 - 这东西真烂 Credit: Corentin Penloup 灵感来源:https://...