後に改めて記事を書きますが、最近はRubocopのコードを読み進めています。
そんな中で「面白いなぁ...」と感じたコードがあったので紹介がてら動作を確認してみたい思います。
なんと定義されたクラスがArrayクラスを継承させているではありませんか。
module RuboCop module Formatter # Arrayクラスを継承させている! class FormatterSet < Array : end end end
rubocop/formatter/formatter_set.rb#L10
FormatterSetクラスは別ファイルに定義された、様々なFormatter(結果をどのように出力するか管理しているクラス)をコレクションとして扱うためのクラスです。つまり、使用したいFormatterがあればFormatterSetの内部にadd_formatter関数経由で追加されて保持しているようです。
def add_formatter(formatter_type, output_path = nil) : self << formatter_class(formatter_type).new(output, @options) end
しれっとself
に対して<<
を呼び出して配列へ要素の追加を行っていますが、ここでのself
は一体、何を参照しているのでしょうか。
自分だったら、この手の実装はインスタンス変数(@formatters
みたいな)を作成してFormatterクラスを管理しそうなものですが、Arrayクラスを継承させる方法を選んだのなぜなのでしょうか。
selfが参照するもの
まずはArrayクラスを継承した場合にselfが参照するものから見ていきましょう。
class Sample < Array def whats_self self end end self_ = Sample.new.whats_self puts "self: #{self_}" # self: [] puts "self.class: #{self_.class}" # self.class: Sample puts "self.class: #{Sample.superclass}" # self.class: Array
予想通り...ではありますがSampleクラスのself
が参照しているのは初期状態では空の配列([]
)でした。
先ほどFormatterSetクラスの内部でself << formatter_class(...)
が記述されていたことにも納得です。実質、空のインスタンス変数を作成して値を追加するという動作と非常に似たものであると考えて良いでしょう。
この結果を踏まえて、以下のようなコードが通りました。
Arrayクラスを継承すると、そのクラスは配列として扱うことが可能であると考えるのが自然な気がしてきました。
class Sample < Array def set_val self << 1 self << 2 self << 3 self end end self_ = Sample.new.set_val puts "self: #{self_}" # self: [1, 2, 3]
Arrayクラスを継承させる理由
self
が参照しているのは初期状態では空の配列であることが分かりました。
しかし、これだけのためにArrayクラスを継承させるというのは少しやり過ぎな気がします。他に何か理由はあるのでしょうか。
先ほどのFormatterSetクラスを見てみると各所でeach
関数を呼び出していることに気づきました。
def file_started(file, options) @options = options[:cli_options] @config_store = options[:config_store] each { |f| f.file_started(file, options) } end
rubocop/formatter/formatter_set.rb#L44
確かにArrayクラスを継承しているのでArrayクラスに定義された関数を呼び出すことが可能です。
また一部の関数の振る舞いを変化させたいのであればオーバーライドすることも可能と...使いようによっては非常なパワフルな働きをしてくれると思います。FormatterSetの内部ではeach関数をそのまま呼び出しているだけですが、今後、他の関数や振る舞いを変化させたいときは変更がしやすそうです。
class Sample < Array def initialize self << 1 self << 2 self << 3 end end Sample.new.each { |v| puts "elm: #{v}" } # elm: 1 # elm: 2 # elm: 3
eachのオーバーライド
class Sample < Array : def each(&block) puts "each!" end end ins = Sample.new ins.each { |v| puts "elm: #{v}" } # each!
インスタンスの作成
Arrayクラスを継承することで継承先クラスのインスタンス作成時に引数が渡せるようになります。
インスタンス作成時にはArrayクラスのnew関数を呼び出すわけですが、ドキュメントを見てみるとnew関数は引数が異なるものが3つ定義されています。それらに倣って実行してみます。
class Sample < Array end puts "pattern1: #{Sample.new(5, 1)}" puts "pattern2: #{Sample.new([1,1,1,1,1])}" puts "pattern3: #{Sample.new(5) { 1 } }" # pattern1: [1, 1, 1, 1, 1] # pattern2: [1, 1, 1, 1, 1] # pattern3: [1, 1, 1, 1, 1]
class Array (Ruby 3.2 リファレンスマニュアル)
RubocopのFormatterSetクラスでもnew関数から初期値のFormatterを指定しているのかと思いきや、FormatterSetクラスではinitializeがオーバーライドされており、new関数経由での初期値の登録はしていませんでした。
class FormatterSet < Array def initialize(options = {}) super() @options = options # CLI options end end # 呼び出し元 @optionsはHash # rubocop/lib/rubocop/runner.rb:416 set = Formatter::FormatterSet.new(@options)
まとめ
Arrayクラスを継承すると...
- selfの初期値には空の配列が保持される
self << value
を実行するとselfに値の追加が可能- Arrayクラスに定義された関数が利用可能
- new関数に初期値の指定が可能
- Arrayクラスの関数をオーバーライドすることで振る舞いを簡単に変えることが可能
組み込みクラスを継承したことがなかったので非常に興味深い結果になりました。
同じようにデータをコレクションとして扱いたいのであれば、FormatterSetクラスのようにArrayクラスを継承してみるとスマートな実装ができるかもと感じました。
少しでも「ええな〜」と思ったらイイネ!・シェア!・はてなブックマークを頂けると励みになります。