Ruby リソースはブロックとensureで管理しよう
File::openの話にとどまらず、リソースを確保するというコードの抽象化の紹介。
(File::openについてはここでは省略)
- 作者: Peter J.Jones
- 出版社/メーカー: 翔泳社
- 発売日: 2015/01/19
- メディア: Kindle版
- この商品を含むブログ (5件) を見る
項目24 リソースはブロックとensureで管理しよう
ensureを使ってリソースの解放を行う例
class Lock def self.acquire lock = new lock.exclusive_lock! yield(lock) ensure lock.unlo0ck if lock end end Lock.qcquire do |lock| # ここで例外が発生しても問題なし end
しかしこれだとblockがない場合が考慮されていない。
改良版
class Lock def self.acquire lock = new lock.exclusive_lock! if block_given? yield(lock) else lock # Lock::newのように動作する end ensure if block_given? lock.unlo0ck if lock end end end
本書の例だと、少しblock_given?
の所が見辛いですが、まぁ簡単に実装できます。
覚えておくべき事項
- 確保したリソースを解放するためにensure説を書こう。
- リソース管理を抽象化するために、クラスメソッドでblockとensureパターンを使おう。
- ensure節で変数を使うときには、その前に変数が初期化さレテいるかどうかを確かめよう。
Rubyの例外Tips
みんな最初は迷う例外のTips。
- 作者: Peter J.Jones
- 出版社/メーカー: 翔泳社
- 発売日: 2015/01/19
- メディア: Kindle版
- この商品を含むブログ (5件) を見る
項目22 raiseにはただの文字列ではなくカスタム例外を渡そう
raiseに文字列を渡すとRuntimeErrorとなる
>> raise("coffee machine low on water") RuntimeError (coffee machine low on water)
カスタムエラークラスの名称
これは大抵どんな本でも記載があるのでみんな知っていると思うが、
rubyのExceptionは低水準エラーを含むので、通常の例外はStandardErrorを継承するのが一般的。
名称も〜Errorにしておこうという話。
名前は〜Exceptionとしてしまっていたこともあるので、今後はErrorを使うようにしよう。
カスタム例外クラスの構成
メッセージ以外に補助情報を持たせたいときカスタム例外を使うと便利。
本書では3Dプリンタを操作するユーティリティを例にし、温度を格納するようにしている。
class TemperatureError < StandardError attr_reader :temperature def initialize(temperature) @temperature = temperature super("invalid temperature: #{@temperature}") end end raise(TemperatureError.new(180)) TemperatureError (invalid temperature: 180)
1プロジェクトに複数あるなら独自のクラス階層にまとめることを検討すべき。
覚えておくべき事項
- 例外としてraiseに文字列を渡すのは避けよう。この場合汎用のRuntimeErrorオブジェクトが使われる。そうではなく、カスタム例外クラスを作ろう。
- カスtクァむ例外クラスはStandardErrorを継承し、クラス名がErrorで終わるようにしよう。
- 一つのプロジェクトのために複数の例外クラスを作るときには、まずStandardErrorを継承する基底クラスを作り、ほかの例外クラスはそのカスタム基底クラスを継承するように構成しよう。
- カスタム来街クラスのためにinitializeメソッドを書くときには、superを呼び出すようにしよう。super呼び出しにエラーメッセージを渡せばなおよい。
- initializeでエラーメッセージを設定するときには、raiseでエラーメッセージを設定すると、initializeのメッセージが上書きされてしまうことに注意しよう。
RubyのSet#include? で効率化
項目18 要素が含まれているかどうかの処理を効率よく行うために集合を使うことを検討しよう
先ずはArray
Array#include? は計算量がO(n) 。
class Role def initialize(name, permissions) @name, @permissions = name, permissions end def can?(permission) @permissions.include?(permission) end end
Hashを使うと高速化できる
メモリを消費するが、要素へのアクセスはO(log n)。
ハッシュの値はtrue
を使うとイミュータブルなグローバル変数となり効率的。
class Role def initialize(name, permissions) @name = name @permissions = Hash[permissions.map { |p| [p, true] } ] end def can?(permission) @permissions.include?(permission) end end
ただし、注意点としては、重複が失われることと、配列をHash化するためにさらに大きな配列を作成していること。#can? の効率化を図ったが、結果 #initialize のコストが増加しているので #can? の呼び出し回数が少なければ効果は少ない。
そこでSetですよ
Hashの例はかっこが多くて見にくい。そしてHashの特別な機能を使っているわけではない。
SetクラスはHashに要素を格納するので、Hashと同等のパフォーマンスが出る。
require "set" class Role def initialize(name, permissions) @name, @permissions = name, Set.new(permissions) end def can?(permission) @permissions.include?(permission) end end
覚えておくべき事項
- 要素が含まれているかどうかの高速チェックではSetを使うことを検討しよう
- Setに挿入されるオブジェクトは、ハッシュキーとしても使えなければならない。