Unity:組み込み関数を継承させないようにする

Prevent inherit MonoBehaviour's built-in methods in sub-class

(24/3/26) 無駄に遠回りなことをしてたので内容を訂正しました。


Unityを使っていて何回も悩んだのが、継承関係を持つクラスを作るうえで、基底クラスのStart、Update等の組み込み関数を派生クラスから遮蔽したいのにできない、ということでした。

制約付きながら、やっと自分なりの落としどころを見つけることができたので対策を残しておきます。

(※csc.rspを書き換えるとかメソッドをvirtual化するとかいう工程が入るため、他者と共有するプロジェクトとか、配布前提のものでは使えません)


問題点は簡単にまとめると次の2つです。

これらについて、

「基底クラスの実装をvirtualにする」

「警告をエラー扱いにする」

で対処しました。


実例

こんなことをしたいことはありませんか?という実装例です。


基底クラスの実装

class BaseClass : MonoBehaviour

{

    //※StartはBaseクラス内でしかアクセスできないようにしたい

    void Start()

    {

        // 何らかの前処理

        SubStart(); // ←前処理と後処理の間に行われることを保証する

        // 何らかの後処理

    }


    protected virtual void SubStart(){ }

}


継承クラスの実装

class SubClass : BaseClass

{

    protected override void SubStart()

    {

        //前処理と後処理の間に行いたい処理を記述する

Debug.Log("Called via BaseClass.Start")

    }

}

BaseClass.Start()内で、特定の前処理と後処理を記述し、継承クラスでのStartに相当する処理(SubStart)は、あくまでこの基底クラスのStart()内の二つの前後処理の間に行わせたいとします。

そこでSubStart()という継承前提のメソッドをBaseクラス内に作り、あらかじめ前処理と後処理に挟んでしまうことにしたわけです。
あとは継承クラスで、実装をStart()ではなくSubStart()内に記述するようにすればOK!

これで目的が果たされました。


…なのですが、例えば派生クラスがこんな実装だと、全部だめになってしまいます。

class SubClass : BaseClass

{

    protected override void SubStart()

    {

        //前処理と後処理の間に行いたい処理を記述する

Debug.Log("Called via BaseClass.Start")

    }


    void Start()//Start自体も上書き宣言してしまっている(警告「CS0108」が出る)

    {

        //するとBaseClass.Start()自体がもみ消されてしまう

//結果、SubStart()は呼ばれない

    }

}

このコード例では、派生クラスで独自にStart()自体も宣言してしまっています。これだとコンポーネントとしてゲームオブジェクトにアタッチしても、SubStart()は呼ばれずにもみ消されてしまいます。

派生クラス側が新たににStart()を定義してしまった場合、BaseClassの方のStart()メソッドが「遮蔽」されてしまうからです。

(ゲームオブジェクトにアタッチしたのがSubClassならSubClass.Start()が呼ばれ、BaseClass.Start()の方は呼ばれません)


つまり基底クラスにすでにStart()がある場合、派生クラスではStart()を実装させないようにしたいということ。

前述の通り、基底クラスに既に存在するメソッドと同名のメソッドを宣言した場合「CS0108」の警告が出ます。

警告:CS0108 'SubClass.Start' は継承されたメンバー 'BaseClass.Start' を非表示にします。非表示にする場合は、キーワード new を使用してください。

これは警告ではあるもののエラーではないため見逃してしまう可能性があります。そこで以下の手順で警告をエラー扱いにします。


対策:Unity側でCS0108の警告をエラー扱いにする

csc.rsp

-warnaserror+:0108

このファイルをAssetフォルダ直下においておくことでUnityが毎回特別に読み込んでくれます。今回書いたコマンドはCS0108警告をエラー扱いにするものです。

これでUnityを再起動すれば、CS0108はUnity側でエラーと見做してくれるようになります。CS0108が出ている箇所をすべて修正しない限りコンパイルエラーとなってプレイモードにも入れないので、newなしに上書きすることはもうできなくなります。

(*1) Player>Api Compatibility Levelが「.NET4.x」の場合。ここが「.Net Standard 2.0」になっている場合は代わりに「msc.rsp」を作成。

なお、基底クラスのメソッドがvirtualである場合にはCS0108ではなくCS0114の警告になります。

(蛇足)virtualなメソッドのオーバーライドを封じる

上は基底クラスのメソッドが普通の場合ですが、virtualキーワード付きで宣言された仮想メソッドの場合、派生クラスでoverrideは出来てしまいます。


仮想メソッドのオーバーライドを禁止するには、sealed(封印)キーワードを使います。virtualなメソッドをsealed overrideすることで、その後の派生クラスではオーバーライドできなくなります。


しかし、BaseClassのStartを

public sealed override Start() {...}

のようにしてもコンパイルエラーになります。封印オーバーライドしようにも、組み込みのStart()はもともとvirtualメソッドではないためです。

そのため、Start()をvirtualなメソッドとして宣言しなおすためだけのクラスを噛ませる必要があります。幸い、このクラスは一度作れば使いまわせるのでさほど無駄な努力にはならないでしょう。


組み込みメソッドをvirtualで宣言しなおしただけのクラス「SealeableMonoBehaviour」なるものを作りました。

(仮想メソッドのアクセス修飾子はprivateではだめなのでprotectedにしました。もちろんpublicでもOK)

SealableMonoBehaviour.cs

public class SealableMonoBehaviour : MonoBehaviour

{

    //Startをvirtualで宣言

    protected virtual void Start(){ }

}

Update、OnEnable/Disable等の他の組み込みメソッドも同様のやり方でseal可能にすることができます。


このクラスをMonoBehaviourの代わりに継承することで、Start()をsealすることができるようになりました!

class BaseClass : SealableMonoBehaviour

{

    //sealed overrideすることで派生クラスでのオーバーライドを防ぐ

    protected sealed override void Start()

    {

        //何らかの前処理

        SubStart();

        //何らかの後処理

    }


    protected virtual void SubStart(){}

}

これによって派生クラスでは、Start()をオーバーライドしようとするとエラーで弾かれるようになります。

class SubClasss : BaseClass

{

    protected override void SubStart()

    {

        //前処理と後処理の間に行いたい処理を記述する

    }


    protected override void Start() { }//エラー:CS0239

}


ちょっと一見わかりづらいのですが、次のような三段構成です。
  1. SealableMonoBehaviour:protected virtual void Start()で仮想化宣言
  2. (1を継承した)BaseClass:protected sealed override void Start()でこれ以降オーバーライドできないようにする
  3. (2を継承した)SubClass:Start()はBaseClass以降オーバーライドできなくなってるのでSubStart()を使うしかない

BaseClassでsealedするためにはそれより基底の段階で仮想メソッドでなければならないので、仮想メソッドにするためだけにSealableMonoBehaviourという層を加えたということですね。

(蛇足)VisualStudio側でも警告をエラー扱いにする方法(※永続しないので無意味)

※以下は蛇足というか、一時的にしか意味ないです。(パソコン再起動したら元に戻ってしまう)

先に挙げたcsc.rspでUnityにCS0114をエラー扱いにさせることができましたが、(自分の環境では)Unity側ではエラーにしてくれるものの、VisualStudioの方はエラーにしてくれませんでした(相変わらず警告扱いのままコンソールに表示される)。

一応書いてしまったので、VisualStudio側で警告をエラー扱いにする方法も残しておきます。

←プロジェクトのプロパティを開きます。

開かない!?

たぶん、大体の人はここをクリックしても何も開かないと思います。

これは、Tools for Unityがデフォルトではプロジェクトのプロパティを開けないようにしてあるためです。

もし開かない場合は、「ツール>オプション」を開き、その中の「Tools for Unity>全般>プロジェクト プロパティへのアクセス」が「false」になっているはずなのでそれを「true」にします。

そしてOKで適用した後、VisualStudioを再起動します(Unityまで再起動する必要はなし)。

そうすれば、プロジェクトのプロパティがちゃんと開けるようになっているはずです。


CS0114をエラー扱いにする

プロジェクトのプロパティの中の「ビルド>警告をエラーとして扱う」で「特定の警告」を選択し、0114を入力します。