(2019.10) Ruby 2.5で確認。
「Rubyには多重継承はない」とよく言われる。しかし、そうではない。菱形継承もある。
Ruby のモジュールは、メソッドを定義できてクラスと似ています。ただ、次の制限があります。
include
メソッドで)。
ほかのクラスにinclude
したり、オブジェクトにextend
して、モジュールのメソッドを追加させることができます。
クラス/オブジェクトには複数のモジュールを付けられます。多重継承とほぼ同じ, メソッド定義の共有ができます。
クラスの関係を見てみると、モジュールはModule
クラスのインスタンスになります。Class
クラスの直接のインスタンスではないので、モジュールをインスタンス化できないのは明らかです。
余談。
Class
クラスはModule
クラスのサブクラスになっています。ありがちな設計上の誤りで、本来継承関係にないものを継承させてしまっていると思います。
例えば、次の現象が起こっています;
Class
クラスで、Module
クラスで定義されたメソッドのいくつかを削除している
Module
クラスで定義されているメソッドについても、モジュールに関係ないものが多くあります。だいぶ混乱しています。
include
モジュールは次のようにして定義します。メソッドはインスタンスメソッドの形で定義します。クラスメソッドは後述。
定義したモジュールは、クラスでinclude
します。モジュールのメソッドがオブジェクトのメソッド呼び出し経路に挿入されます。ancestors
クラスメソッドで確認できます。自クラス -> includeされたモジュール -> スーパークラス、の順です。
よくある解説ではクラス E
にある include M
は無視される、とあるが、正しくは菱形継承になっている。
スーパークラスよりも includeされたモジュールのほうが呼び出される優先順位が高いにも関わらず、スーパークラスで include
するとサブクラスのそれが無視される、という組み合わせなので、スーパークラスでのデザイン変更が、サブクラスに影響してしまう。
先に宣言したほうが優先になっていないことによる混乱。
一つのクラス内で複数のモジュールを別のinclude
文で includeした場合は、後からincludeされた方が、先に検索されます。
Ruby のインスタンス変数の領域は、クラスのインスタンスでただ一つだけが確保されます。includeされたモジュールのメソッドでインスタンス変数を使うと、異常な挙動になる危険があります。
次の例は, クラス、モジュールの各メソッドで, @hoge
を使っています。
メソッドM1#z()
は期待に反して "str"
になり, D#g()
も同様です。
かなり不味い。エラーも出ないので、分かりにくいバグの温床です。
運用ルールとして, モジュールのインスタンスメソッドでは, (1) インスタンス変数を使うことを禁止するか, (2) インスタンス変数名にモジュール名を含めるなどしてほかのクラスやモジュールと名前が被らないようにしなければなりません。
多重継承における状態の持ち方の問題です。
Common Lisp は, 多重継承を認めるがメンバ変数が共有される。Ruby に似た挙動 (運用で気をつける)。Python3 では, 多重継承がありメンバ変数が共有されるのも同じだが、プライベート変数は自動的にクラス名が補われるので, 衝突しにくく実害が起きにくい。
Java8 からの interface
のデフォルト実装は、ほぼ多重継承だが, 変数は自動的に public static final
になり、状態を持てない。C# 8 (.NET Core 3.0 / .NET Standard 2.1) からのinterface
のデフォルト実装も、Java8と同じ考え方。
C++は、多重継承ありでクラスごとにメンバ変数を持つので、上記の問題とは世界が違う。
include
できるクラスだけでなく、モジュール内で include
できます。多重継承です。菱形継承もできます。
この例では、Common Lisp と同じになる。
(defclass D () ()) (defclass B (D) ()) (defclass C (D) ()) (defclass E () ()) ; 左が優先 (defclass A (B C E) ()) (setq obj (make-instance 'A)) (print (sb-mop:class-precedence-list (find-class 'A))) (format t "~%") ; ~% = newline
実行結果。Rubyと同じ順序です。
(#<STANDARD-CLASS COMMON-LISP-USER::A> #<STANDARD-CLASS COMMON-LISP-USER::B> #<STANDARD-CLASS COMMON-LISP-USER::C> #<STANDARD-CLASS COMMON-LISP-USER::D> #<STANDARD-CLASS COMMON-LISP-USER::E> #<STANDARD-CLASS COMMON-LISP:STANDARD-OBJECT> #<SB-PCL::SLOT-CLASS SB-PCL::SLOT-OBJECT> #<SB-PCL:SYSTEM-CLASS COMMON-LISP:T>)
extend
)モジュールのメソッドは、extend
で、オブジェクトに付けることもできます。
すでに見てきたように、オブジェクトに直接付けるメソッドは、特異クラスのインスタンスメソッドですから、extend
は、次と同じ意味です。
include
, extend
はモジュールしか引数に取れません。すでに述べたように、Class
がModule
のサブクラスになっているのがおかしいのです。
モジュールのクラスメソッドは、定義したモジュールをレシーバにしてしか呼び出せません。
これを逆手に取って、関数の名前空間を明確にするのに使えます。
レシーバを明記しないと呼び出せないので、インスタンス変数は、常にインスタンスとしての M
のものが使われます。