Rspecでモックを作りたくて動的なクラスをClass.new
で生成していた時のことです。
関数で受け取った配列を.map
で処理していたところ、do |student|
で定義したブロック変数がClass.new
内部に定義した関数のスコープ外になるという現象に遭遇しました。
def create_students(students) students.map do |student| Class.new do def studnet_info puts "生徒名: #{student}" end end end.new end student_classes = create_students(['太郎', '二郎', '三郎']) student_classes.each { |s| s.studnet_info } # 生徒名: # 生徒名: # 生徒名:
ブロックのスコープ内部でClass.new
と関数定義を行っていたので、ブロック変数のstudent
を参照できると思ったのですが、そうではないようです。
同じような現象に遭遇している方達を見てみると、解決策としてはClass.new
で定義したクラスのインスタンス作成時に値を受け渡し、クラス内変数として参照するという方法が多数派のようでした。
def create_students(students) students.map do |student| Class.new do def initialize(student) @student = student end def studnet_info puts "生徒名: #{@student}" end end.new(student) end end student_classes = create_students(['太郎', '二郎', '三郎']) student_classes.each { |s| s.studnet_info } # 生徒名: 太郎 # 生徒名: 二郎 # 生徒名: 三郎
これでも良いのですが、値を受け渡すのにわざわざクラス内変数を宣言しないといけないのが面倒に感じたので何か良い方法はないものかと考えてみました。
define_methodを使う
過去にOSSのコードを読んだ際に知ったdefine_method
が使えるんじゃないかと思って試してみました。
def create_students(students) students.map do |student| Class.new do define_method(:studnet_info) { puts "生徒名: #{student}" } end.new end end student_classes = create_students(['太郎', '二郎', '三郎']) student_classes.each { |s| s.studnet_info } # 生徒名: 太郎 # 生徒名: 二郎 # 生徒名: 三郎
いい感じです。
Class.new
の内部では外部のスコープの値を参照できないのかと思っていたのですが、どうやら違いました。
Class.new
内部で定義された関数からは外部のスコープの値を参照できないというのが正しいですね。ただし、定数値などは参照できるためブロック変数などスコープが限定されるような変数に限ると考えておけば間違いなさそうです。
CONSTANT_VALUE = 'ちくわ' def create_chikuwa Class.new do def chikuwa puts CONSTANT_VALUE end end.new end create_chikuwa.chikuwa # ちくわ