より良いエンジニアを目指して

1日1つ。良くなる!上手くなる!

「Virtual member call in constructor」について調べて考えたこと

docs.microsoft.com

私がやりがちでありながらも解消できない警告に「Virtual member call in constructor」というのがあります。

f:id:rimever:20190502070824p:plain

職場でも指摘されたことがあって何が悪いのでしょうか。

以下の記事を読んでみたりしました。

c# - Virtual member call in a constructor - Stack Overflowでは簡潔に回避方法まで回答されています。

https://www.codeproject.com/Tips/641610/Be-Careful-with-Virtual-Method:embed:titileでは、C++Javaについても触れられています。

Why Do Initializers Run In The Opposite Order As Constructors? Part Two – Fabulous Adventures In Codingは様々なページから引用されている記事です。

この記事のサンプルコードでは、Base<-Derived<-MostDerivedという継承になっており、

  • Baseで継承可能なメソッドBlahが宣言されている
  • MostDerivedでBlahをオーバーライドして、DerivedのメソッドであるDoItを呼ぶ

よって、コードを追いかけると

  1. Base.Constructor
  2. Base.Blah
  3. MostDerived.Blah
  4. Derived.DoIt

と巡りに巡ってDoItに行きつくことになります。確かにややこしいです。

ダメではないけど、コンストラクタ内部で例外が発生する危険性があり、ややこしくなってバグの温床になる汚いコードになるからオススメしませんよ、ということのようです。

ただ、設計上、どうしてもコンストラクタ内部でオーバーライド可能なメソッドを呼び出してしまうんですよね。

回避方法としては最上位のクラスをsealedにして継承できなくするとか、そもそもオーバーライド可能なメソッドにせずに直接呼び出すとかあるようです。

うーん。

私の設計が悪いのか。なるべく避けるようにはします。

どうして、この警告に引っかかってしまうのか

ゲームで言うSceneとか、画面にあたるオブジェクトのコンストラクタで起きがちな気がします。

基盤となる継承元クラスを作っておき、そこから各画面に最適した処理を実装する過程で、イレギュラーなケースが登場して、継承可能メソッドにして警告に引っかかってしまうという流れです。

using System;

abstract class SceneBase
{
    public SceneBase()
    {
        A();
        B();
    }
    public void A() { Console.WriteLine("A");}
    public virtual void B() { Console.WriteLine("B");}
} 
class SceneA:SceneBase {
}

class SceneB:SceneBase {
    public override void B() {
        Console.WriteLine("やっぱりCにしたい");
    }
}
public class Program {
public static void Main() {
    var scene = new SceneB();
}
}

対策として考えられるのはメソッドに逃す、だろうけど・・・

警告に引っかからないだけの対応ならば、メソッドを作成して、別途呼び出されるようにする方法です。

using System;

abstract class SceneBase
{
    public SceneBase()
    {
    }
    public void A() { Console.WriteLine("A");}
    public virtual void B() { Console.WriteLine("B");}
    
    public void Initialize() {        
        A();
        B();
    }
} 
class SceneA:SceneBase {
}

class SceneB:SceneBase {
    public override void B() {
        Console.WriteLine("やっぱりCにしたい");
    }
}


public class Program {
public static void Main() {
    var scene = new SceneB();
    // 追加したInitializeメソッドを呼ぶ
    scene.Initialize();
}
}

これはわかるんですが、ただ警告に引っかからなければ良いというような感じで自分としては釈然としません。

インスタンスの生成が確実に行われることは保証されるのですが、その分、逃したメソッド(Initialize)を別途呼び出す手間が増えるのが引っかかります。

もしくはメソッドに逃さず、コンストラクタの手続きは各々で呼び出す?これも継承の旨味がなくなってしまうので、うーん。