第2章 Ruby核心语言特性高频面试题:对象模型、元编程、块与闭包
问答卡 1 · 对象模型
面试问题:「请解释Ruby中一个类找方法的过程,包括eigenclass、继承链和ancestors的区别。」
考察点:
- 是否真正理解方法查找的完整路径(对象 → eigenclass → 类 → 继承链)
- 能否清晰区分eigenclass与普通类的层叠方式
- 对include/prepend/module扩展的底层差异
回答框架:① 一句话定义:方法查找按「右-上-左」规则,从对象的eigenclass开始,沿继承链向上;② 手写示例展示:用obj.singleton_class观察eigenclass位置;③ 深入原理:Eigenclass插入在对象和类之间,ancestors包含所有模块;④ 边界意识:告知prepend优于include的场景。
简短示例回答:「方法查找首先检查对象的eigenclass(单例类),然后沿着eigenclass的superclass链查找,直到BasicObject。Eigenclass在obj.singleton_class可见。例如obj.define_singleton_method(:foo){}添加的方法即存在于此。继承链通过ancestors查看,注意prepend会将模块插入在当前类之前,带来方法查找优先权。」
可能追问:
- 「如果两个模块都用include,查找顺序怎么定?」
- 「
super在eigenclass方法中怎么调用?」 - 「Ruby 3中Module#ancestors引入了哪个新类型?」
class << self与instance_eval的作用域差异。另外,不要忘记提及include和prepend在ancestors中的位置顺序。
问答卡 2 · 元编程
面试问题:「写一个attr_accessor的手动实现(用元编程),并解释define_method与method_missing的使用场景及风险。」
考察点:
- 能否手写出
define_method生成getter/setter - 是否清楚
method_missing的动态派发性能损耗和幽灵方法问题 - 对
send/respond_to_missing?的配合使用
回答框架:① 手写def self.attr_accessor(*names) ... end并用define_method实现;② 对比method_missing:适合大量动态方法但必须重写respond_to_missing?;③ 深入原理:define_method创建闭包方法,性能优于method_missing;④ 边界意识:滥用method_missing导致调试困难和内存泄漏。
简短示例回答:「手写实现:class Module; def my_attr(name); define_method(name){ instance_variable_get("@#{name}") }; define_method("\#{name}="){|v| instance_variable_set("@#{name}", v) }; end; end。method_missing用于类似ActiveRecord动态查找器,但必须配合respond_to_missing?,且避免在性能敏感路径使用。元编程核心是“在运行时控制结构”,但不要绕开明确的接口定义。」
可能追问:
- 「
define_method和def的闭包差异是什么?」 - 「
method_missing如何影响respond_to??」 - 「如何测试元编程生成的动态方法?」
method_missing又调自身)。另外,用define_method时注意变量捕获——如果使用块内局部变量可能产生意想不到的闭包绑定。Rails中的delegate实现是一个很好的元编程正向案例。
问答卡 3 · 块与闭包
面试问题:「解释Ruby中Proc和lambda的区别,以及闭包如何捕获外部变量?请给出一个产生意外的闭包陷阱的例子。」
考察点:
- 是否能准确说出
return和arity检查的不同 - 是否理解闭包通过binding保留外部作用域的局部变量
- 能否识别常见闭包陷阱(如循环中创建Proc的shared variable问题)
回答框架:① 明确区别:lambda严格检查参数数,return从lambda返回;Proc不检查参数,return从包围方法返回。② 闭包捕获机制:Proc/lambda内部保持对外部局部变量的引用,变量生命周期被延长。③ 陷阱示例:arr = []; [1,2,3].each { |i| arr << ->{ i } }在作用域内正常工作,但如果在循环后i值变化则可能出问题;更经典的是使用define_method在循环中捕获循环变量。
简短示例回答:「lambda是真正的闭包,类似方法:return从自身返回;Proc更像代码片段,return会跳出创建它的方法。闭包通过binding保留局部变量,例如def counter; x=0; ->{ x+=1 }; end每次调用都共享同一个x。陷阱:methods = (1..3).map { |n| Proc.new { n } }这里每个Proc持有独立n,但若用define_method在循环中就可能所有方法返回同一个变量值(需配合块参数修复)。」
可能追问:
- 「闭包中的局部变量如果被修改,对其他闭包的影响?」
- 「在并发场景下闭包共享变量如何保证安全?」
- 「
Method对象与Proc的闭包行为是否相同?」
before_action或回调里直接引用循环变量,导致所有回调都使用最后一个值。正确做法是用块参数重新绑定变量。另外,不要在Proc中使用return,除非你明确要跳出外层方法。
attr_accessor、手写一个简单的delegate实现;在IRB中用ObjectSpace观察闭包绑定的变量。对比class_eval与instance_eval的区别。建议花15分钟画出对象模型的完整方法查找路径图。