第5章 木曜日:クラス定義
- JavaやC#におけるクラスの定義は、あなたが「これがオブジェクトに期待する動作です」と言うと、コンパイラが「了解。やってみます」と答える。クラスのオブジェクトを生成して、メソッドを呼び出すまでは何も起こらない。
- Rubyにおけるクラスの定義は違っており、classキーワードは、オブジェクトの動作を規定しているだけではなく、実際にコードを実行している。
- クラスマクロ(クラスを修正する方法)
- アラウンドエイリアス(メソッドでコードをラップする方法)
クラス定義
- クラス定義にはメソッドの定義だけでなく、あらゆるコードを置くことができる。
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_eval
はself
とカレントクラスを変更する。 -
class_eval
はselfを変更することによって、classキーワードと同じようにクラスを再オープンできる。 -
Module#class_eval
はclassよりも柔軟。- classが定数を必要とするのに対して、
class_eval
はクラスを参照している変数なら何でも使える。
- classが定数を必要とするのに対して、
- classは現在の束縛を捨てて、新しいスコープをオープンするのに対し、
class_eval
はフラットスコープを持っている。- つまり「スコープゲート」で学んだように、
class_eval
ブロックのスコープの外側にある変数も参照できる。
- つまり「スコープゲート」で学んだように、
-
instance_eval
にinstance_exec
という双子のメソッドがあるように、module_eval
やclass_eval
にも、外部のメソッドをブロックに渡せるmodule_exec
やclass_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
という名前の変数があるが、それぞれ異なるスコープで定義されており、別々のオブジェクトに属している。- クラスインスタンス変数がアクセスできるのはクラスだけであり、クラスのインスタンスやサブクラスからはアクセスできない。
- 2つの
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_class
かclass <<
という変わった構文を使わなければ見ることができない。 - 次に、インスタンスを一つしか持てない。
- だから特異クラスはシングルトンクラスとも呼ばれる。
- そして、継承ができない。
- 最後に、特異クラスはオブジェクトの特異メソッドが住んでいる場所。
- まず、
def obj.my_singleton_method; end
singleton_class.instance_methods.grep(/my_/) # => [:my_singleton_method]
特異クラスのメソッド探索
- 特異メソッドも通常のメソッド探索に当てはまる。
- オブジェクトが特異メソッドを持っていれば、Rubyは通常のクラスではなく、特異クラスのメソッドから探索を始める。
- これで特異メソッドを呼び出せる。
- 特異クラスにメソッドがなければ、継承チェーンを上ヘ進み、特異クラスのスーパークラスにたどり着く。
- これはオブジェクトの通常のクラスになる。
- オブジェクトが特異メソッドを持っていれば、Rubyは通常のクラスではなく、特異クラスのメソッドから探索を始める。
特異クラスと継承
-
obj
-> (singleton)class ->#obj
-
#obj(singleton-method(特異メソッド))
-> superclass ->D
-
D
-> (singleton)class ->#D
- -> superclass ->
C
- -> superclass ->
-
#D
-> superclass ->#C
-
C
-> (singleton)class ->#C
- -> superclass ->
Object
- -> superclass ->
-
#C
-> superclass ->#Object
-
Object
-> (singleton)class ->#Object
- -> superclass ->
BasicObject
- -> superclass ->
-
#Object
-> (singleton)class ->#BasicObject
-
BasicObject
-> (singleton)class ->#BasicObject
-
#BasicObject
-> superclass ->Class
大統一理論
- 「Rubyのオブジェクトモデルには、クラス、特異クラス、モジュールがある。インスタンスメソッド、クラスメソッド、特異メソッドがある。」
- オブジェクトは1種類しかない。それが通常のオブジェクトかモジュールになる。
- モジュールは1種類しかない。それが通常のモジュール、クラス、特異クラスのいずれかになる。
- メソッドは1種類しかない。メソッドはモジュール(大半はクラス)に住んでいる。
- 全てのオブジェクトは(クラスも含めて)「本物のクラス」を持っている。それが通常のクラスか特異クラスである。
- 全てのクラスは(BasicObjectを除いて)ひとつの祖先(スーパークラスかモジュール)を持っている。つまり、あらゆるクラスがBasicObjectに向かって1本の継承チェーンを持っている。
- オブジェクトの特異クラスのスーパークラスは、オブジェクトのクラスである。クラスの特異クラスのスーパークラスはクラスのスーパークラスの特異クラスである(3回唱えてみよう)。
- メソッドを呼び出すときは、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_eval
はself
を変更し、class_eval
はself
とカレントクラスの両方を変更していた。 -
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_method
をMyModule
の普通のインスタンスメソッドとして定義し、それから、MyClass
の特異クラスで、モジュールをインクルードする。 -
my_method
はMyClass
の特異クラスのインスタンスメソッドである。つまり、my_method
はMyClass
のクラスメソッドである。- この技法は クラス拡張 と呼ばれる。
クラスメソッドと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
キーワードもあり、トップレベルでメソッドにエイリアスをつけるときに便利。
- Rubyには
# 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#size
はString#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つの手順で書ける。
- メソッドにエイリアスをつける。
- メソッドを再定義する。
- 新しいメソッドから古いメソッドを呼び出す。
-
アラウンドエイリアスの欠点
- 新しいメソッド名でクラスを汚染してしまうこと -> メソッドをエイリアスした後にprivateにすれば解決できる。
- メソッドの変更を考えていない既存のコードを破壊しかねないこと -> 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#prepend
はinclude
と似ているが、継承チェーンでインクルーダーの上ではなく下にモジュールが挿入されるところが違う。- つまり、プリペンどしたモジュールがインクルーダーのメソッドをオーバーライドできる。
- そして、元のメソッドは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