【Ruby】superしか呼び出していないinitializeメソッドは定義する必要ない

2020年9月8日Ruby

こんにちは、しきゆらです。
今回は、Rubocopの更新を追っていたときに初めて知ったことをメモしておきます。

Rubocopさんは、Rubyを書く方は大半使っているのではないか、と思われる静的コード解析ツールです。
設定により、メソッドの長さやネストの深さなどを制限することで読みやすいコードを書く手伝いをしてくれる頼もしいツールですね。

そんなRubocopさんですが、先日0.90.0がリリースされました。
使っているGemの更新履歴をある程度追っているため、更新内容を確認していたところ今回の更新でも新しい項目が追加されていました。
それが、Lint/UselessMethodDefinitionです。

There are at least 2 cases:
・empty constructors – this may be just overriding parent’s constructor, but this is bad anyway
・methods just calling super

https://github.com/rubocop-hq/rubocop/issues/8472

ということで、空のinitializeメソッドやsuperしか呼び出していないメソッドを怒ってくれるというものです。
内容的には妥当なcopではないかと思います。
必要ないものは書くべきではないですね。


起こったこと

自分のコードを更新したRubocopにかけたところ、このCopに引っかかりました。
引っかかったコードは以下のようなもの。

class A
  def initialize
    # 諸々の初期化
  end
  # 諸々のメソッド定義
end
 
class B < A
  def initialize
    super
  end
  # 諸々のメソッド定義
end

よくある、親クラスを継承してごにょごにょするクラスを作っていました。
その中の、サブクラスのinitializeメソッドが怒られていました。
確かに、このメソッドは親クラスを呼び出すだけなので怒られるのは分かります。

しかし、私はinitializeメソッドはサブクラス側で初期化内容を上書きする必要がある場合はもちろんですが、
上書きする必要がない場合でも明示的に親クラスを呼び出す必要があると思い込んでいました。

ただ、クラスBのinitializeメソッドを削除したとしても意図する挙動をしてくれました。
動くのは分かったのですが、なぜ必要ないのかがわからない。
ということで、色々調べてみました。

一応リファレンスも確認してみましたが、詳しいことは書いていませんでした。
https://docs.ruby-lang.org/ja/latest/method/Object/i/initialize.html

挙動を調べてみる

ということで、古い書籍ですが「プログラミング言語Ruby」を引っ張り出してきました。
内容としてはRuby1.9や2.0あたりなので、だいぶ古いですが根本的には大きく変わっていないので、今回のような場合は安心して使えます。

探してみると、P. 246の脚注にほしい情報がばっちり書かれていました。
該当部分を引用すると、

Rubyでは、initializeは通常のメソッドであり、他のメソッドと同じ様に継承されるのである。

https://amzn.to/3bzluOy

ということで、サブクラス側でinitializeメソッドをオーバーライドしていない場合は親クラスのinitializeメソッドがそのまま呼ばれるということでした。
つまり、上記のように明示的に親クラスを呼び出すだけのinitializeメソッドは必要ない、ということです。

また、superの記述に関しても引数を渡さない場合のsuperの挙動は受けた引数をそのまま親クラスのメソッドに渡すように動くようです。
ということで、ただ単に親クラスのメソッドを呼び出すだけのメソッドは本格的に必要がないということがわかりました。

書籍では、以下のようなクラスを定義して説明されていた。

class Point
  def initialize(x, y)
    @x = x
    @y = y
  end
  
  def to_s
    "(#{@x}, #{@y})"
  end
end
 
class Point3D < Point; end

上記のように定義した場合、Point3DクラスはPointクラスと同じ挙動をします。

point = Point.new(1, 2)
point3d = Point3D.new(2, 3)
 
point.to_s
#=> (1, 2)
point3d.to_s
#=> (2, 3)

この時、Point3D側ではinitializeメソッドが定義されていないが、きちんとPointクラスのinitializeメソッドを呼び出してくれているようです。

これまで、そこそこ長いことRubyさんと戯れてきましたがこれに関してはきちんと把握できていませんでした。
Rubocopの更新を追いかけていたおかげで、これまであいまいになっていた部分の知識を確認する機会を得ることができました。
思わぬところで収穫がありましたが、きちんと中身を追っていくことは大事なんだなぁ、ということを改めて思いました。

まとめ

今回は、Rubocopの更新を追っているうちに自分の知らない挙動を知ることができました。
ただ単に触れているだけではあまり詳しいことを知ることはできないということと、周りにあるコードやライブラリの中身を追っていくのは大事なんだということを改めて思い知りました。
今後もGemの更新を追っていくので、その中で自分が知らなかったことをメモしていきます。

今回は、ここまで。
おわり

Posted by しきゆら