Ruby でも型チェック
動的型付け (スクリプト) 言語では、データ型のチェックが実行時にしか行われないため、プログラムの妥当性検証・デバッグといった作業が困難になります。 例えば、Ruby でプログラムを書いていて、次のようなバグに悩まされたことのある人は多いのではないでしょうか。
- Integer オブジェクトを参照しているべき変数が、他の型のオブジェクトを参照している。
- そのオブジェクトが「いつ」「どこで」代入されたものなのか分からない。
この手のバグは、問題の発生 (不正な型の代入) と発覚 (エラーの発生) の位置が離れてしまうので、非常に厄介。 発生箇所を絞り込むのが難しいため、プログラムを広範囲に渡って見直すハメになります。
check_type の導入
こうしたバグへの対処を容易にするため、私は以下に示す check_type
メソッドをプログラム全体で使用しています。
# 型チェック def check_type(type, instance, nilable =false) if (instance.nil?) unless nilable raise ArgumentError::new("non-nil constraint vioration") end # nilable else unless instance.kind_of?(type) raise ArgumentError::new("type mismatch: #{instance.class} for #{type}") end # instance, type end # instance.nil? return instance end |
これを各メソッドの配置して、引数の型チェックを実施。
チェックをパスすれば、対象のインスタンスがそのまま戻り値として返されるので、initialize
や set
~ 系のメソッドでは、そのままインスタンス変数に代入することができます。
# ブログエントリ class Entry attr :id # 識別子 attr :title # 表題 attr :content # 本文 attr :time_register # 登録日時 # 初期化 def initialize(id, title, content, time_register) @id =check_type(Integer, id, false) @title =check_type(String, title , false) @content =check_type(String, content , true) @time_register =check_type(Time, time_register, false) end # [DB] 項目の単一選択 def self.db_select_one(dbh, id) check_type(DBI::DatabaseHandle, dbh, false) check_type(Integer, id, false) row =dbh.select_one('SELECT * FROM entry WHERE id = ?;', id) return row ? db_gen(dbh, row) : nil end # [DB] 項目の生成 def self.db_gen(dbh, r) check_type(DBI::DatabaseHandle, dbh, false) check_type(DBI::Row, r, false) id =r[:id] title =r[:title] content =r[:content] time_register =dbi_dt2t(r[:time_register]) return Entry::new(id, title, content, time_register) end end # class Entry |
論理型 (Boolean) への対応
check_type
は「instance が type で指定された型ないしその派生型のオブジェクトかどうか」を検証するだけなので、共通の親クラスをもたない複数の型のインスタンスを参照し得るような変数には適用できません。
時に厄介なのは、true (TrueClass
のインスタンス) / false (FalseClass
クラス) のいずれかを受け入れるような引数を持つメソッドです。
例えば、String
/ Symbol
/ Regexp
のいずれかを引数として渡せるようなメソッドを作ろうとする場合などは、メソッド名を変えることなどによって (ダックタイピングに慣れ切っている人は気持ち悪いと思うようですが)、回避策を講じることが可能です。
しかし、true と false については「別の型」ではなく、同じ型の「別の値」として扱われるべきものなので、型違いの引数を取る場合と同じ対処をするのはかなり不自然です。
そこで私は、論理型の引数については、仕方なく、代入を伴う場合とそうでない場合についてそれぞれ以下のような記述で妥協し、自分を納得させることにしています。(涙)
# リンク class Link attr :href # 参照先 (URL) attr :text # アンカテキスト attr :disabled # 状態 # 初期化 def initialize(href, text, disabled =false) @href =check_type(String, href, false) @anchor =check_type(String, text, false) @disabled =disabled ? true : false end # 文字列 (HTML) 変換 def to_s(escape =false) # check_type(Boolean, escape, false) return sprintf( '<a href="%s">%s</a>', escape_html(@href), (escape ? escape_html(@text) : @text)) end end # class Link |
個人的には、そもそも true, false は Boolean
::true
, Boolean
::false
, のエイリアスとして定義すべきではなかったのかと思うのですが...。
Comments