2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

メタプログラミングRuby(第I部・金曜日まで)を読んでいく

Last updated at Posted at 2020-11-29

第5章 木曜日:クラス定義

  • JavaやC#におけるクラスの定義は、あなたが「これがオブジェクトに期待する動作です」と言うと、コンパイラが「了解。やってみます」と答える。クラスのオブジェクトを生成して、メソッドを呼び出すまでは何も起こらない。
  • Rubyにおけるクラスの定義は違っており、classキーワードは、オブジェクトの動作を規定しているだけではなく、実際にコードを実行している。
  1. クラスマクロ(クラスを修正する方法)
  2. アラウンドエイリアス(メソッドでコードをラップする方法)

クラス定義

  • クラス定義にはメソッドの定義だけでなく、あらゆるコードを置くことができる。
class MyClass
  puts 'Hello'
end

# => Hello
  • メソッドやブロックと同じように、クラス定義も最後の命令文の値を返す。
result = class MyClass
  self
end

result # => MyClass
  • クラス(やモジュール)定義の中では、クラスがカレンとオブジェクトselfになる。
    • クラスやモジュールは単なるオブジェクトなため、クラスもselfになれる。

カレントクラス

  • Rubyのプログラムは、常にカレントオブジェクトselfを持っている。

  • それと同様に、常にカレントクラス(あるいはカレントモジュール)も持っている。

    • メソッドを定義すると、それはカレントクラスのインスタンスメソッドになる。
  • カレントオブジェクトはselfで参照を獲得できるが、カレントクラスを参照するキーワードはない。

    • カレントクラスを追跡するには、コードを見る。
  • プログラムのトップレベルでは、カレントクラスはmainのクラスのObjectになる

    • トップレベルにメソッドを定義すると、Objectのインスタンスメソッドになるのはこのため。
  • classキーワードでクラス(あるいはmoduleキーワードでモジュール)をオープンすると、そのクラスがカレントクラスになる。

  • メソッドのなかでは、カレントオブジェクトのクラスがカレントクラスになる。

class C
  def m1
    def m2; end
  end
end

class D < C; end

obj = D.new
obj.m1

C.instance_methods(false) # => [:m1, :m2]

class_eval

  • Module#class_eval は、そこにあるクラスのコンテキストでブロックを評価する。
def add_method_to(a_class)
  a_class.class_eval do
    def m; 'Hello!'; end
  end
end

add_method_to String
"abc".m # => "Hello!"
  • Module#class_eval は、BasicObject#instance_eval とはまったく別物。

    • instance_eval はselfを変更するだけだが、 class_evalself とカレントクラスを変更する。
    • class_eval はselfを変更することによって、classキーワードと同じようにクラスを再オープンできる。
    • Module#class_eval はclassよりも柔軟。
      • classが定数を必要とするのに対して、class_eval はクラスを参照している変数なら何でも使える。
    • classは現在の束縛を捨てて、新しいスコープをオープンするのに対し、class_eval はフラットスコープを持っている。
      • つまり「スコープゲート」で学んだように、class_eval ブロックのスコープの外側にある変数も参照できる。
    • instance_evalinstance_exec という双子のメソッドがあるように、 module_evalclass_eval にも、外部のメソッドをブロックに渡せる module_execclass_exec というメソッドがある。
  • クラス以外のオブジェクトをオープンしたいなら、instance_eval を使う。

  • クラス定義をオープンして、defを使ってメソッドを定義したいなら、class_eval を使う。

    • 「このオブジェクトをオープンしたいが、クラスのことは気にしない」なら instance_eval がいいし、「ここでオープンクラスを使いたい」なら class_eval の方がいい。

カレントクラスまとめ

  • Rubyのインタプリタは、常にカレントクラス(あるいはカレントモジュール)の参照を追跡している。defで定義された全てのメソッドは、カレントクラスのインスタンスメソッドになる。
  • クラス定義の中では、カレントオブジェクトselfとカレントクラス(定義しているクラス)は同じである。
  • クラスへの参照を持っていれば、クラスは class_eval (あるいは module_eval )でオープンできる。

クラスインスタンス変数

  • カレントクラスの理論が役立つ。

  • Rubyのインタプリタは、全てのインスタンス変数はカレントオブジェクトselfに属しているが、これはクラスでも同じ。

class MyClass
  @my_var = 1
end
  • クラスのインスタンス変数とクラスのオブジェクトのインスタンス変数は別物
    • 2つの @my_var という名前の変数があるが、それぞれ異なるスコープで定義されており、別々のオブジェクトに属している。
      • クラスインスタンス変数がアクセスできるのはクラスだけであり、クラスのインスタンスやサブクラスからはアクセスできない。
class MyClass
  @my_var = 1
  def self.read; @my_var; end
  def write; @my_var = 2; end
  def read; @my_var; end
end

obj = MyClass.new
obj.read  # => nil
obj.write
obj.read  # => 2
MyClass.read  # => 1

クラス変数

class C
  @@v = 1
end
  • クラス変数はサブクラスや通常のインスタンスメソッドからもアクセスできる。
  • クラス変数はクラスではなく、クラス階層に属している。
class D < C
  def my_method; @@v; end
end

D.new.my_method  # => 1

クラスのトリック

  • 以下のコードをclassキーワードを使わずに書くことができる。
class MyClass < Array
  def my_method
    'Hello!'
  end
end
  • クラスはClassクラスのインスタンスなので、Class.newを呼び出して作ることができる。
    • Class.newは引数(新しいクラスのスーパークラス)と、新しいクラスのコンテキストで評価するブロックを受け取る。
c = Class.new(Array) do
  def my_method
    'Hello!'
  end
end
  • クラス名は単なる定数であるので、上記の無名関数を割り当てることができる。
    • また、Rubyはクラスの名前をつけようとしていることを理解して、定数がClassオブジェクトを参照し、Classオブジェクトも定数を参照するようになる。
c.name # => "MyClass"

特異メソッド

  • 単一のオブジェクトに特化したメソッドのこと。
    • Rubyでは特定のオブジェクトにメソッドを追加できる。
    • 特異メソッドは下記の構文もしくは Object#define_singleton_methods で定義できる。
str = "just a regular string"  # 普通の文字列

def str.title?
  self.upcase == self
end

str.title?  # => false
str.methods.grep(/title/)  # => [:title?]
str.singleton_methods  # => [:title?]

ダックタイピング

  • 静的言語
    • 「あるオブジェクトがTの型を持つのは、それがTクラスに属している。あるいはTインターフェイスを実装している。」
  • 動的言語
    • 「オブジェクトの型はそのクラスとは厳密に結びついていない。型はオブジェクトが反応するメソッドの集合に過ぎない。」
      • 流動的な型のことをダックタイピングと呼ぶ。

クラスメソッド

  • クラスは単なるオブジェクト
  • クラス名は単なる定数

=> クラスのメソッド呼び出しは、オブジェクトのメソッドの呼び出しと同じ。

  • クラスメソッドはクラスの特異メソッド
def obj.a_singleton_method; end
def MyClass.another_class_method; end

def object.method
  # メソッドの中身
end
  • 上記の object の部分には、オブジェクトの参照、クラス名の定数、selfのいずれかが使える。

クラスマクロ

  • Rubyのオブジェクトにはアトリビュートがない。
    • アトリビュートのようなものが欲しいときは、読み取り用と書き込み用の2つのミミックメソッドを定義する。
class MyClass
  def my_attribute=(value)
     @my_attribute = value
  end

  def my_attribute
    @my_attirbute
  end
end

obj = MyClass.new
obj.my_attribute = 'x'
obj.my_attribute  # => x
  • このようなメソッド(アクセサ)を書いていると、すぐに退屈になる。

    • だが、Module#attr_* 族のメソッドを使えば、一気にアクセサを生成できる。
    • Module#attr_reader は読み取り用を生成し、Module#attr_writer は書き込み用を生成し、Module#attr_accessor は読み書き両用を生成する。
  • attr_* 族のメソッドはModuleクラスで定義されているので、selfがモジュールであってもクラスであっても使える。attr_accessorのようなメソッドをクラスマクロと呼ぶ。

  • クラスマクロを使って古い名前を廃止する方法

class Book
  def title # ...
  
  def subtitle # ...

  def lend_to(user)
    puts "Lending to #{user}"
    # ...
  end

  def self.deprecate(old_method, new_method)
    define_method(old_method) do |*args, &block|
      warn "Warning: #{old_method}() is deprecated. Use #{new_method}()."
      send(new_method, *args, &block)
    end
  end

  deprecate :GetTitle, :title
  deprecate :LEND_TO_USER, :lend_to
  deprecate :title2, :subtitle
end

特異クラス

  • 特異メソッドは、オブジェクトモデルのどこに住むことができない。

    • Rubyのオブジェクトは、見えているクラスとは別に、裏に特別なクラスを持っており、それはオブジェクトの特異クラス(メタクラス、シングルトンクラス)と呼ばれる。
  • Object#class などのメソッドは、特異クラスを丁寧に隠してしまう。

  • しかし、classキーワードを使った特別な構文を使うと、特異クラスのスコープに入ることができる。

class << an_object
  # あなたのコードをここに
end
  • 特異クラスの参照を取得したければ、スコープの外にselfを返せればいい。
obj = Object.new

singleton_class = class << obj
  self
end

singleton_class.class  # => Class
  • または、特異クラスを参照するselfを戻さなくても、 Object#singleton_class という便利なメソッドが使える。
"abc".singleton_class  # => #<Class:#<String:0x3331df0>>
  • 先ほどの例から、特異クラスはクラスであるが、特別なクラスであることがわかる。
    • まず、 Object#singleton_classclass << という変わった構文を使わなければ見ることができない。
    • 次に、インスタンスを一つしか持てない
      • だから特異クラスはシングルトンクラスとも呼ばれる。
    • そして、継承ができない。
    • 最後に、特異クラスはオブジェクトの特異メソッドが住んでいる場所
def obj.my_singleton_method; end
singleton_class.instance_methods.grep(/my_/)  # => [:my_singleton_method]

特異クラスのメソッド探索

  • 特異メソッドも通常のメソッド探索に当てはまる。
    • オブジェクトが特異メソッドを持っていれば、Rubyは通常のクラスではなく、特異クラスのメソッドから探索を始める。
      • これで特異メソッドを呼び出せる。
    • 特異クラスにメソッドがなければ、継承チェーンを上ヘ進み、特異クラスのスーパークラスにたどり着く。
      • これはオブジェクトの通常のクラスになる。

特異クラスと継承

  • obj -> (singleton)class -> #obj
  • #obj(singleton-method(特異メソッド)) -> superclass -> D
  • D -> (singleton)class -> #D
    • -> superclass -> C
  • #D -> superclass -> #C
  • C -> (singleton)class -> #C
    • -> superclass -> Object
  • #C -> superclass -> #Object
  • Object -> (singleton)class -> #Object
    • -> superclass -> BasicObject
  • #Object -> (singleton)class -> #BasicObject
  • BasicObject -> (singleton)class -> #BasicObject
  • #BasicObject -> superclass -> Class

大統一理論

  • 「Rubyのオブジェクトモデルには、クラス、特異クラス、モジュールがある。インスタンスメソッド、クラスメソッド、特異メソッドがある。」
  1. オブジェクトは1種類しかない。それが通常のオブジェクトかモジュールになる。
  2. モジュールは1種類しかない。それが通常のモジュール、クラス、特異クラスのいずれかになる。
  3. メソッドは1種類しかない。メソッドはモジュール(大半はクラス)に住んでいる。
  4. 全てのオブジェクトは(クラスも含めて)「本物のクラス」を持っている。それが通常のクラスか特異クラスである。
  5. 全てのクラスは(BasicObjectを除いて)ひとつの祖先(スーパークラスかモジュール)を持っている。つまり、あらゆるクラスがBasicObjectに向かって1本の継承チェーンを持っている。
  6. オブジェクトの特異クラスのスーパークラスは、オブジェクトのクラスである。クラスの特異クラスのスーパークラスはクラスのスーパークラスの特異クラスである(3回唱えてみよう)。
  7. メソッドを呼び出すときは、Rubyはレシーバの本物のクラスに向かって「右へ」進み、継承チェーンを「上へ」進む。Rubyのメソッド探索について知るべきことは以上だ。
  • Rubyプログラマなら「この複雑な階層で最初に呼ばれるメソッドはどれだろう?」「あのオブジェクトからこのメソッドを呼び出せるだろうか?」といったオブジェクトモデルに関する難しい質問に出会うことがあるだろう。
    • このような質問に出会った時には、先ほどの7つのルールを見返して、オブジェクトモデルの図をさっと描いてみれば、すぐに答えが見つかるはずだ。

クラスメソッドの構文

  • クラスメソッドはクラスの特異クラスにあるメソッド
# 1. クラス名が重複した定義
def MyClass.a_class_method; end

# 2. クラス定義の中のselfがクラス自身になることをうまく活用している。
class MyClass
  def self.another_class_method; end
end

# 3. 特異クラスをオープンして、そこにメソッドを定義している。(特異クラスを明示的に意識しているため、Ruby界隈で一目置かれるコード)
class MyClass
  class << self
    def yet_another_class_method; end
  end
end

特異クラスとinstance_eval

  • instance_evalself を変更し、 class_evalself とカレントクラスの両方を変更していた。
  • instance_eval もカレントクラスを変更する。
    • カレントクラスをレシーバの特異クラスに変更する。
s1, s2 = "abc", "def"

s1.instance_eval do
  def swoosh!; reverse; end
end

s1.swoosh! # => "cba"
s2.respond_to?(:swoosh!) # => false
  • とはいえ、カレントクラスを変更するために instance_eval を使うことはほとんどない。
    • instance_eval の意味は「 self を変更したい 」のまま。

クラスのアトリビュート

# 動くには動くが、全てのクラスにアトリビュートが追加されてしまう。
class MyClass; end

class Class
  attr_accessor :b
end

MyClass.b = 42
MyClas.b  # => 42

# MyClassにだけアトリビュートを追加するには、特異クラスにアトリビュートを追加する。
class MyClass
  class << self
    attr_accessor :c
  end
end

MyClass.c = 'It works!'
MyClass.c  # => "It works!"

# 特異クラスにメソッドを定義するとクラスメソッドになるので、以下のように書くのと同じ。
def MyClass.c=(value)
  @c = value
end

def MyClass.c
  @c
end

モジュールの不具合

# クラスがモジュールをインクルードすると、モジュールのインスタンスメソッドが手に入るが、
# クラスメソッドはモジュールの特異クラスの中にいる。
module myModule
  def self.my_method; 'hello'; end
end

class MyClass
  include MyModule
end

MyClass.my_method  # NoMethodError!

# my_methodをMyModuleの普通のインスタンスメソッドとして定義し、
# それから、MyClassの特異クラスで、モジュールをインクルードする。
module MyModule
  def my_method; 'hello'; end
end

class MyClass
  class << self
    include MyModule
  end
end

MyClass.my_method  # => "hello"
  • クラスがモジュールをインクルードすると、モジュールのインスタンスメソッドが手に入るが、クラスメソッドはモジュールの特異クラスの中にいる
  • my_methodMyModule の普通のインスタンスメソッドとして定義し、それから、 MyClass の特異クラスで、モジュールをインクルードする。
  • my_methodMyClass の特異クラスのインスタンスメソッドである。つまり、 my_methodMyClass のクラスメソッドである。
    • この技法は クラス拡張 と呼ばれる。

クラスメソッドとinclude

  • クラス拡張を使ってクラスの特異クラスにメソッドを定義すれば、それはクラスメソッドになる。
    • クラスメソッドは特異メソッドの特殊なケースにすぎないので、このトリックはどんなオブジェクトにも適用できる。
  • 普通のオブジェクトに適用した場合、これはオブジェクト拡張と呼ばれる。
module MyModule
  def my_method; 'hello'; end
end

obj = Object.new

class << obj
  include MyModule
end

obj.my_method  # => "hello"
obj.singleton_methods  # => [:my_method]

Object#extend

  • クラス拡張とオブジェクト拡張はよく使われる機能なので、そのためだけにメソッドが提供されている。
  • Object#extend は、レシーバの特異クラスにモジュールをインクルードするためのショートカットである
module MyModule
  def my_method; 'hello!'; end
end

obj = Object.new
obj.extend MyModule
obj.my_method  # => 'hello'

class MyClass
  extend MyModule
end

MyClass.my_method  # => 'hello'

メソッドラッパー

  • メソッドの中にメソッドをラップする方法は3つある。

アラウンドエイリアス

  • Module#alias_method を使えば、Rubyのメソッドにエイリアス(別名)をつけることができる。
    • Rubyには alias キーワードもあり、トップレベルでメソッドにエイリアスをつけるときに便利。
# alias_method
class MyClass
  def my_method; 'my_method()';  end
  alias_method :m, :my_method
end

obj = MyClass.new
obj.my_method # => "my_method()"
obj.m  # => "my_method()"

# alias
class MyClass
  alias_method :m2, :m
end

obj.m2  # => "my_method()"
  • String#sizeString#length のエイリアス。
  • Interger クラスには少なくとも5つの異なる名前を持つメソッドがある。

メソッドの再定義

  • 新しいメソッドを定義して、元のメソッドの名前をつけること。
    • これなら元のメソッドをエイリアスで呼び出すことができる。
class String
  alias_method :real_length, :length

  def length
    real_length > 5 ? 'long' : 'short'
  end
end

"War and Peace".length  # => "long"
"War and Peace".real_length  # => 13
  • Thorというコマンドラインユーティリティを構築するRuby gemの例。
    • Thorにはrake2thorというRakeビルドファイルをThorスクリプトに変換するプログラムが含まれており、その処理の中でrake2thorは、Rakefileをloadして、Rakefileがrquireする全てのファイルを保管する必要がある。
input = ARGV[0] || 'Rakefile'
$requires = []

module Kernel
  def require_with_record(file)
    # グローバルの配列に、Rakefileからrquireされるファイルの名前を保管している。
    # Kernel#callerメソッドで呼び出し側のスタックを取得し、スタックの2番目がrake2thorであれば、
    # スタックの1番目はrequireを呼び出すRakefileになる。
    $requires << file if caller[1] =~ /rake2thor:/
    require_without_record file
  end
  # メソッド名をrequire_with_recordにしているが、
  # 実際にはKernel#requireを変更している。
  alias_method :require_without_record, :require
  alias_method :require, :require_with_record
end
  • アラウンドエイリアスとは、新しいrequireが、古いrequireの「周囲(アラウンド)をラップ」しているトリックのこと。

  • アラウンドエイリアスは3つの手順で書ける。

    1. メソッドにエイリアスをつける。
    2. メソッドを再定義する。
    3. 新しいメソッドから古いメソッドを呼び出す。
  • アラウンドエイリアスの欠点

    1. 新しいメソッド名でクラスを汚染してしまうこと -> メソッドをエイリアスした後にprivateにすれば解決できる。
    2. メソッドの変更を考えていない既存のコードを破壊しかねないこと -> Ruby2.0からは、既存のメソッドの周囲に機能を追加する方法が1つではなく2つ導入された。

Refinementsラッパー

  • Refinementsはクラスのコードにパッチを貼り付けるようなものであるが、アラウンドエイリアスの代わりに使うことができる。

    • リファインしたメソッドからsuperを呼び出すと、元のリファインしていないメソッドが呼び出せる。
  • Stringクラスをリファインして、lengthメソッドの周囲に機能を追加した例

module StringRefinements
  refine String do
    def length
      super > 5 ? 'long' : 'short'
    end
  end
end

using StringRefinements
"War and Peace".length  # => "long"
  • Refinementsラッパーが適用されるのは、ファイルの最後まで(Ruby2.1からはモジュール定義の終わりまで)。
    • あらゆるところに適用されるアラウンドエイリアスよりも、こちらの方が一般的に安全であると言える。

Prependラッパー

  • Module#prependinclude と似ているが、継承チェーンでインクルーダーの上ではなく下にモジュールが挿入されるところが違う。
    • つまり、プリペンどしたモジュールがインクルーダーのメソッドをオーバーライドできる。
      • そして、元のメソッドはsuperで呼び出せる。
module ExplicitString
  def length
    super > 5 ? 'long' : 'short'
  end
end

String.class_eval do
  prepend ExplicitString
end

"War and Peace".length  # => "long"
  • Refinementsラッパーのようにローカルなものではないが、Refinementsラッパーやアラウンドエイリアスよりも明示的で綺麗な方法だとされている。
module AmazonWrapper
  def reviews_of(book)
    start = Time.now
    result = super
    time_taken = Time.now - start
    puts "reviews_of() took more than #{time_taken} seconds" if time_taken > 2
    result
  rescue
    puts "reviews_of() failed"
    []
  end
end

Amazon.class_eval do
  prepend AmazonWrapper
end

クイズ

  • 数値の + 演算子は Fixnum#+ のシンタックスシュガー

    • 1 + 1 と書けば、パーサーが内部で 1.+(1) に変換している。
  • Fixnum#+ を再定義して、結果に常にプラス1する例。

class Fixnum
  alias_method :old_plus, :+

  def +(value)
    self.old_plus(value).old_plus(1)
  end
end

まとめ

  • クラス定義がself(あなたが呼び出したメソッドのデフォルトのレシーバ)とカレントクラス(あなたが定義したメソッドのデフォルトのいばしょ)に与える影響について調べた。

  • 特異メソッドや特異クラスと仲良くなり、オブジェクトモデルとメソッド探索に関する新たな知見を得た。

  • 新魔術をいくつか覚えた。クラスインスタンス変数、クラスマクロ、Prependラッパーなど。

  • 「クラス」は、「クラスやモジュール」の短縮形であり、クラスに関することは全てモジュールにも当てはまる。

    • 例えば、「カレントクラス」はモジュールにも当てはまり、「クラスインスタンス変数」は「モジュールインスタンス変数」にもなる。

第5章 金曜日:コードを記述するコード

Kernel#eval

  • Kernel#eval は、渡されたコード文字列を実行して、その結果を戻す。
array = [10, 20]
element = 30
eval("array << element")  # => [10, 20, 30]

REST Clientの例

  • REST Clientはコード文字列を作成及び評価することで、ループの中で一気に全てを定義している。
POSSIBLE_VERBS = ['get', 'put', 'post', 'delete']

POSSIBLE_VERBS.each do |m|
  eval <<-end_eval
    def #{m}(path, *args, &b)
      r[path].#{m}(*args, &b)
    end
  end_eval
end
  • ヒアドキュメント
    • 上記のコードで、evalの後に続いているのは通常のRubyの文字列。
    • クオートの代わりに、<<-と任意の終端子(ここではend_eval)で文字列が開始する。そして、その終端子のみが含まれる行で文字列が終了する。
    • このコードが、こうした文字列の代わりになるものを使い、4つのコード文字列を生成してevalした結果、それぞれがget、put、post、deleteになる。

Bindingオブジェクト

  • スコープをオブジェクトにまとめたもの。

    • Bindingを作ってローカルスコープを取得すれば、そのスコープを持ちまわすことができる。
    • evalと一緒に組み合わせて使えば、後からそのスコープでコードを実行することができる。
  • Bindingオブジェクトは、Kernel#bindingメソッドで生成できる。

  • Bindingオブジェクトにはスコープは含まれているが、コードは含まれていないため、ブロックよりも「純粋」なクロージャと考えることができる。

    • 取得したスコープでコードを評価するには、evalの引数にBindingを渡せばいい。
class MyClass
  def my_method
    @x = 1
    binding
  end
end

b = MyClass.new.my_method

eval "@x", b  # => 1

irbの例

  • irbは、標準入力やファイルをパースして、それぞれの行をevalに渡すシンプルなプログラム。

    • こうしたプログラムはコードプロセッサと呼ばれる。
  • irbのソースコードの深いところにあるworspace.rbというファイルでevalを呼び出している。

# statementsはRubyのコード行。
# @bindingはirbがコードを異なるコンテキストで実行するときにこの引数を使う(instance_evalとよく似ている)。
# fileとlineは例外が発生したときにスタックとレースを調整するために使う。
eval(statements, @binding, file, line)

「コード文字列」対「ブロック」

  • Kernel#evalは、eval族の特殊ケースである。

    • class_evalやinstance_evalのようにブロックを受け取るのではなく、コード文字列を評価する
  • instance_evalやclass_evalはコード文字列またはブロックのいずれかを受け取ることができる。

  • 文字列にあるコードは、ブロックにあるコードと大きな違いはない。

    • コード文字列はブロックと同じように、ローカル変数にアクセスすることもできる。
array = ['a', 'b', 'c']
x = 'd'
array.instance_eval "self[1] = x"

array # => ["a", "d", "c"]
  • ブロックとコード文字列はよく似ているので、多くの場合はどちらも使うことができる。
    • しかし、可能であればコード文字列を避けるべき。

evalの問題点

  • コード文字列は、シンタックスハイライトや自動保管といったエディタの機能が使えないことが多い。
  • 誰にでも使えるものだが、コード文字列は読むのも修正するのも難しいことが多い。
  • Rubyは評価するまでコード文字列の構文エラーを報告しない。
    • そのため、実行時に予期せずに失敗するような脆弱性のあるプログラムになる可能性もある。

コードインジェクション

  • Arrayのメソッドを確認する手っ取り早い方法は、evalを使ったユーティリティを書いて、サンプルの配列にメソッドを呼び出して、その結果を確かめること。
    • これを配列探索と呼ぶ。
def explore_array(method)
  code = "['a', 'b', 'c'].#{method}"
  puts "Evaluating: #{code}"
  eval code
end

loop { p explore_array(gets.chomp) }

find_index("b")
# => Evaluating: ['a', 'b', 'c'].find_index('b')
# 1

map! { |e| e.next }
# => Evaluating: ['a', 'b', 'c'].map! {|e| e.next }
# ['b', 'c', 'd']
  • 上記のコードは、悪意あるユーザーが、あなたのコンピュータの脅威になる任意のコードを実行できてしまう。
    • ハードディスクを消去されるかもしれないし、アドレス帳の全ての宛先に情熱的なラブレターを送られてしまうかもしれない。
  • このような脆弱性を吐く行為のことをコードインジェクション攻撃と呼ぶ。
object_id; Dir.glob("*")
# => ['a', 'b', 'c'].object_id; Dir.glob("*")
# => [プライベートな情報がズラズラと表示される]

コードインジェクションから身を守る

  • 悪質なコードを書く方法は無数に存在する。
  • 自分で書いた文字列にだけevalを使うように制限すればいいかもしれないが、複雑なケースになると、文字列がどこから来たかを追跡するのは驚くほど難しい。
  • evalは完全に追放すべきだと唱えるプログラマもいる。
  • evalを追放するのであれば、状況に応じて代替となる技法を探さなければいけない。
    • 動的メソッドと動的ディスパッチによって置き換えることができる。
POSSIBLE_VERBS = ['get', 'put', 'post', 'delete']

# POSSIBLE_VERBS.each do |m|
#   eval <<-end_eval
#     def #{m}(path, *args, &b)
#       r[path].#{m}(*args, &b)
#     end
#   end_eval
# end

POSSIBLE_VERBS.each do |m|
  define_method m do |path, *args, &b|
    r[path].send(m, *args, &b)
  end
end
# def explore_array(method)
#   code = "['a', 'b', 'c'].#{method}"
#   puts "Evaluating: #{code}"
#   eval code
# end

def explore_array(method, *arguments)
  ['a', 'b', 'c'].send(method, *arguments)
end

オブジェクトの汚染とセーフレベル

  • Rubyは、潜在的に安全ではないオブジェクト(特に外部から来たオブジェクト)に自動的に汚染の印をつけてくれる。
    • 汚染オブジェクトには、ウェブフォーム、ファイル、コマンドライン、さらにはシステム変数から、プログラムが呼び込んだ文字列が含まれる。
  • 汚染された文字列を操作して新しい文字列を作ると、その新しい文字列も汚染される。
# オブジェクトが汚染されているかどうかをtainted?メソッドを呼び出して確認している。

# ユーザー入力を読み込む
user_input = "User input: #{gets()}"
puts user_input.tainted?

# <= x = 1
# => true
  • 全ての文字列が汚染されているかどうかを自分で確認しなければいけないのであれば、安全ではない文字列を自分で追跡するのと大差ない。

    • 一方、Rubyはセーフレベルという考えも一緒に提供している。
  • セーフレベルは、オブジェクトの汚染を上手く補完してくれるもの。

    • セーフレベルを設定する(グローバル変数$SAFEに値を設定する)と、潜在的に危険な操作をある程度は制限できる。
  • セーフレベルはデフォルトの0から3の4つから選択できる。

    • 例えばセーフレベル2では、ファイル操作はほとんど認められていない。
    • 0より大きいセーフレベルでは、Rubyは汚染した文字列を評価できない。
$SAFE = 1
user_input = "User input: #{gets()}"
eval user_input

# <= x = 1
# => SecurityError: Insecure operation - eval
  • 安全性を自分で調整するには、コード文字列を評価する前に明示的に汚染を除去してから( Object#untaint を呼び出す)、あとはセーフレベルに頼って、ディスクアクセスのような危険な操作を抑止すればいい。
  • セーフレベルを慎重に扱えば、eval用に制御した環境を作ることができる。
    • そのような環境をサンドボックスと呼ぶ。

ERBの例

  • 標準ライブラリのERBは、Rubyのデフォルトのテンプレートシステムである。

  • このライブラリはコードプロセッサであり、Rubyをどのようなファイルにも埋め込むことができる。

  • <%= ... %> タグの部分にRubyのコードが含まれている。このテンプレートをERBに渡すと、コードが評価される。

<p><strong>Wake up!</strong>It's a nice sunny <%= Time.new.strftime("%4") %>.</p>
require 'erb'
erb = ERB.new(File.read('template.rhtml'))
erb.run

# => <p><strong>Wake up!</strong>It's a nice sunny Friday.</p>
  • テンプレートから抜き出したRubyのコードを受け取り、それをevalに渡すメソッドがERBのソースにある。
    • new_toplevel は、TOPLEVEL_BINDING のコピーを戻すメソッド。
    • インスタンス変数 @src は、ERBのタグの中身。
    • インスタンス変数 @safe_level は、ユーザーが必要とするセーフレベル。
      • セーフレベルが設定されていなければ、タグの中身がそのまま評価される。
      • セーフレベルが設定されていれば、ERBはサンドボックスを作る。その中で、グローバルのセーフレベルをユーザーの指定と一致させ、Procをクリーンルームにして、別のスコープでコードを実行している($SAFEの新し値が有効なのはProcの中だけ。他のグローバル変数とは違い、callの後に以前の値にリセットされる)。
class ERB
  def result(b=new_toplevel)
    if @safe_level
      proc {
        $SAFE = @safe_level
        eval(@src, b, (@filename || '(erb)'), 0)
      }.call
    else
      eval(@src, b, (@filename || '(erb)'), 0)
    end
  end
end

add_checked_attributeの例1

  • カーネルメソッドであり、コード文字列で書かれ、アトリビュートがnilまたはfalseの時に、実行時例外を発生させる add_checked_attribute メソッドの実装。
def add_checked_attribute(klass, attribute)
  eval "
    class #{klass}
      def #{attribute}={value}
        raise 'Invalid attribute' unless value
         @#{attribute} = value
      end

      def #{attribute}()
        @#{attribute}
      end
    end
  "
end

add_checked_attributeの例2

  • evalを使って定義し、このメソッドが将来的に公開された場合、コードインジェクション攻撃の標的になる。
  • また、コード文字列を使わないメソッドで書き直したほうが、人間の読み手にとって読みやすく、もっと洗練されたものができる。
def add_checked_attribute(klass, attribute)
  klass.class_eval do
    define_method "#{attribute}=" do |value|
      raise 'Invalid attribute' unless value
      instance_variable_set("@#{attribute}", value)
    end

    define_method attribute do
      instance_variable_get "@#{attribute}"
    end
  end
end

add_checked_attributeの例3

  • ブロックを使ってアトリビュートの妥当性を確認することで、柔軟に妥当性を確認できるようにしたい。
def add_checked_attribute(klass, attribute, &validation)
  klass.class_eval do
    define_method "#{attribute}=" do |value|
      raise 'Invalid attribute' unless validation.call(value)
      instance_variable_set("@#{attribute}", value)
    end

    define_method attribute do
      instance_variable_get "@#{attribute}"
    end
  end
end

add_checked_attributeの例4

  • カーネルメソッドをすべてのクラスで使える クラスマクロ に変更する。
    • add_checked_attribute メソッドを attr_checked メソッドに変更する。
    • attr_checked をあらゆるクラス定義で使うには、ClassまたはModuleのインスタンスメソッドにすればいい。
    • メソッドが実行される時にはクラスがselfの役割を担うので、class_evalを呼び出す必要すらない。
class Class
  def attr_checked(attribute, &validation)
    define_method "#{attribute}=" do |value|
      raise 'Invalid attribute' unless validation.call(value)
      instance_variable_set("@#{attribute}", value)
    end

    define_method_attribute do
      instance_variable_get "@#{attribute}"
    end
  end
end

フックメソッド

  • GUIでマウスクリックのイベントをキャッチするように、クラスの継承、モジュールのクラスへのミックスイン、メソッド定義などのイベントをキャッチする。
  • クラスが継承された時や新しいメソッドを獲得した時に、何らかのコードを実行sルウ。

inheritedメソッド

class String
  def self.inherited(subclass)
    puts "#{self}#{} に継承された"
  end
end

class MyString < String; end

# => String は MyString に継承された
  • inherited はClassのインスタンスメソッド。
    • クラスが継承された時にRubyが呼び出してくれる。
    • Class#inheritedはデフォルトでは何もしないので、上記の例のように自分のコードでオーバーライドして使う。
    • Class#inheritedのようなメソッドは、特定のイベントにフックを掛けることから、 フックメソッド と呼ばれる。

その他のフック

  • Module#included や Module#prepend
module M1
  def self.included(othermod)
    puts "M1 は #{othermod} にインクルードされた"
  end
end

module M2
  def self.prepended(othermod)
    puts "M2 は #{othermod} にプリペンドされた"
  end
end

class C
  include M1
  prepend M2
end

# => M1 は C にインクルードされた
# => M2 は C にプリペンドされた
  • Module#extend

    • Module#extendをオーバーライドすれば、モジュールがオブジェクトを拡張した時にコードを実行できる。
  • Module#method_added、method_removed、method_undefined

module M
  def self.method_added(method)
    puts "新しいメソッド:M##{method}"
  end

  def my_method; end
end

# => 新しいメソッド:M#my_method
  • これらのフックは、オブジェクトのクラスに住むインスタンスメソッドにしか使えない。

    • オブジェクトの得意クラスに住む特異メソッドでは動作しない。
  • 特異メソッドのイベントをキャッチするには、

    • Kernel#singleton_method_added、singleton_method_removed、singleton_method_undefinedを使う。
  • Module#includedは、おそらく最も広く使われているフック。

標準メソッドにプラグイン

  • 「フックメソッド」では、モジュールがインクルードされた時にコードを実行するために、Module#includedをオーバーライドすることを学んだ。
  • だが、そのイベントそのものをプラグインする(= 反対側から操作する)こともできる。
    • includeメソッドでモジュールをインクルードするのだから、Module#includedの代わりにModule#includeをオーバーライドすればいい。
module M; end

class C
  def self.include(*modules)
    puts "Called: C.include(#{modules})"
    super
  end

  include M
end

# => Called: C.include(M)
  • Module#includedのオーバーライドとModule#includeのオーバーライドの重要な違い
    • Module#includedはフックメソッドとして使われるだけなので、デフォルトの実装は空。
    • Module#includeは、モジュールに実際にインクルードしなければならない。
      • そのため、自分で絵作るフックコードからベースとなるModule#includeの実装をsuperを使って呼び出す必要がある。
      • superを忘れると、イベントはキャッチできるが、モジュールのインクルードができなくなってしまう。

VCRの例

  • VCRは、HTTP呼び出しを記録および再生するgem。
    • VCRのRequestクラスは、Normalizers::Bodyモジュールをインクルードしている。
module VCR
  class Request # ...
  
  include Normalizers::Body
  # ...
  • Bodyモジュールは、HTTPメッセージボディーを扱う body_from などのメソッドを定義している。

    • モジュールをインクルードすると、これらのメソッドがRequestクラスのメソッドになる。
    • つまり、RequestがNormalizers::Bodyをインクルードすることにより、クラスメソッドを手に入れた。
  • 一方クラスがモジュールをインクルードすると、通常はクラスメソッドではなく、インスタンスメソッドが手に入る。

    • Normalizers::Bodyなどのミックスインは、以下のようにインクルーダーのクラスメソッドを定義している。
module VCR
  module Normalizers
    module Body
      def self.included(klass)
        klass.extend ClassMethods
      end

      module ClassMethods
        def body_from(hash_or_string)
          # ...
  • BodyにはClassMethodsと言う名前の内部クラスがあり、そこにbody_fromなどの通常のインスタンスメソッドが定義されている。

  • Bodyにはincludedというフックメソッドがある。

  • RubyがBodyをインクルードすると、一連のイベントがトリガーされる。

    • Rubyが、Bodyのincludedフックを呼び出す。
    • フックがRequestに戻り、ClassMethodsモジュールをエクステンドする。
    • extendメソッドが、ClassMethodsのメソッドをRequestの特異クラスにインクルードする。
  • その結果、body_fromなどのインスタンスメソッドがRequestの特異クラスにミックスインされ、実質、Requestのクラスメソッドになる。

add_checked_attributeの例5

  • add_checked_attributeの例4では、attr_checkedと言う名前のクラスマクロを定義していた。

    • このクラスマクロは、Classのインスタンスメソッドで、すべてのクラスで利用可能。
  • attr_checkedへのアクセスを制限する。

    • CheckedAttributesモジュールをインクルードしたクラスだけがアクセスできるようにしたい
module CheckedAttributes
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def attr_checked(attribute, &validation)
     define_method "#{attribute}=" do |value|
       raise 'Invalid attribute' unless validation.call(value)
       instance_variable_set("@#{attribute}", value)
     end

     define_method_attribute do
       instance_variable_get "@#{attribute}"
     end
    end
  end
end
2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?