今回はC#で継承を使ってみたいと思います。
手続き型のプログラミング言語から始めた人は苦手意識を持っている人も多いのではないでしょうか?
わたくしも苦手ですね。(^_^;)
今回は継承とは何か?といったところから、インタフェース、抽象クラスを作成し、それらを継承して新たなクラスを作成してみようと思います。
継承とは?
継承とはいったいなんでしょうか?何の為に継承という機能があるんでしょうか?
まずはそれを考えてみます。
例えばキャラクタークラスを作成するとして、キャラクターは名前、性別、攻撃力、防御力、特技などのパラメータやそれらを操作するメソッドが必要になります。
それらをCharacterという名前のクラスに全部詰め込んで定義する事も出来ます。
しかしこのキャラクターとは別のキャラクターを作成しようと思った時に一部のパラメータや操作メソッドが違う為にCharacter2クラスを作成する事になりました。
さらに、また別のキャラクターも作る事になり合計100の違うクラスを作成する必要が出ました・・・・となったら100人それぞれパラメータやメソッドが微妙に変化するほとんど同じクラスを100個作成する事になります。
ほとんど同じクラスなのに別に100個のクラスを作成するなんて面倒ですし、中身を把握するのもそれぞれ把握しなければならない為、管理が大変です。
また処理の内容を変更しようと思ったら100個のクラス全てに修正を入れていく必要があります。・・・( ・_;)( ;_;)( ;_;)
そんな時に継承の機能を使えば一部のパラメータやメソッドだけを修正するだけで実現出来ます。
クラスで共通するパラメータや操作を抜き出して、それを継承する形でそれぞれのクラスを作成する。
とすれば、同じパラメータや同じ操作をクラス毎に記述する必要がなくなりますし、修正が入っても共通のクラスは1つのクラスを修正するだけで終わりですね。(^^)/
そんなわけで継承とはクラスで共通するクラスを纏めて別のクラスにし、そのクラスの機能を受け継いだ新しいクラスを作成するということですね。
継承元はスーパークラス、親クラス、基底クラス、継承先はサブクラス、子クラス、派生クラスと呼ばれます。
親クラスになればなるほど機能が抽象化されていき、子クラスになればなるほど具象化されたクラスとなります。
子クラスから親クラスのフィールドやメソッドを使う事が出来る上に、子クラスで新しく定義したフィールドやメソッドも使えるというわけです。
これは便利ですね!
継承をする時の注意点
継承をする時にはある決まりを持って継承を行うかどうかを決める必要があります。
子クラスは親クラスの一種である必要があります。
例えば親クラスをHumanクラス、子クラスをMasashiとした場合はMasashiは人間なので継承は可能。
という風に、現実の世界で子クラスは親クラスの一種である必要があります。
親クラスをHuman、子クラスをPenとした場合、ペンは人間の一種ではありませんので、継承は使わないという感じです。
継承なんて使わずにプログラミングしたい!
継承はよくわからないから継承を使わずにプログラミングしてやる!
なんて方もいるかもしれませんが、実はこれ最初から出来ません。(^_^;)
https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/object
すべての型 (定義済み、ユーザー定義、参照型、および値型) が、直接または間接的に Object を継承します。
なのでクラスを定義した時点ですでにObjectクラスを何らかの形で継承しているんですね。
Unityでスクリプトを作った場合もデフォルトではMonoBehaviourクラスを継承してスクリプトが作られています。
例えばC#スクリプトでSampleClassを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | using UnityEngine; using System.Collections; public class SampleClass : MonoBehaviour { // Use this for initialization void Start () { } // Update is called once per frame void Update () { } } |
SampleClassの:の右に継承元のクラスを記述するので、この場合はSampleClassはMonoBehaviourクラスを継承して作られている事がわかります。
MonoBehaviourが親クラスでSampleClassが子クラスということになります。
SampleClassでStartやUpdateメソッドを記述した時、ゲームオブジェクトが登場した時にStart、ゲームオブジェクトが存在している時にUpdateがフレーム毎に呼ばれるのは、MonoBehaviourクラスでそれらが定義されているからです。
↑のページを見るとAwakeやStart、UpdateやさらにはOnTriggerEnterといったメソッドも定義されている事がわかります。
そのページの上の方に継承という部分でMonoBehaviourはBehaviourを継承して作られている事がわかります。
継承のBehaviourの部分をクリックすると、BehaviourはComponentを継承して作られ、ComponentはObjectを継承して作られている事がわかりました。
最終的にObjectクラスを継承して作ったクラスをさらに継承していってこのSampleClassが作られたというわけですね。
SampleClassでは独自のフィールドやメソッドを定義出来ますが、MonoBehaviourで定義されているフィールドやメソッドはそのまま使えるってわけですね。
継承を使わずにプログラミングは出来ないですが、実はすでに継承を使っていたんですね。(^^)/
すでに使っていると思えば敬遠する必要はなさそうですね。
Unityでインタフェース、抽象クラスを継承したサンプル作り
継承の説明ばかりではわからない事もあるので、実際にインタフェースと抽象クラスを作成し、それらを継承して新しいクラスを作成してみます。
インタフェース
インタフェースはクラスを抽象化していった最終形態で、インタフェースでは定数とpublicな抽象メソッドを宣言出来ます。
インタフェースは何のために作るかというと、最低限これらの機能を持たせるといった注文書やお店のメニューみたいなものです。
キャラクタークラスが持つコマンドをインタフェースとして作成してみます。
1 2 3 4 5 6 7 8 9 10 11 | using UnityEngine; using System.Collections; interface ICommand { // メソッドを実装する必要がある void Attack(); void Defence(); void Item(); } |
interfaceの後にIを付けてインタフェース名を記述します。
インタフェースは最初にIを付けるみたいです(インタフェースとわかりやすくする為)。
ICommandインタフェースではAttack、Defence、Itemという抽象メソッドを宣言しています。
インタフェースではメソッドは自動でpublicになります。
またメソッドの中身(実装)を記述する事は出来ず、{}を記述しないで;を書きます。
インタフェースは継承先のクラスで必ずこれらのメソッドを実装しなければいけないものを記述していますので、継承先でそれぞれのメソッドを実装(処理の記述)する必要があります。
またインタフェースはインスタンスを生成して使うクラスではない為、インスタンス化しようとするとエラーが発生します。
C#ではクラスの多重継承は許されていませんが、インタフェースの多重継承は可能です。
抽象クラス
抽象クラスは1つ以上の抽象メソッドを持つクラスでabstractを付けて宣言します。
この機能は必ず持たせ、実装するのは子クラスで、というような時に使用します。
例えば、Humanという抽象クラスを作り、その中には抽象メソッドとしてTalkを宣言したとします。
(抽象メソッドは子クラスで必ず実装しなければいけなくなります)。
Humanクラスを継承して作ったMasashiとMusashiは抽象クラスの抽象メソッドであるTalkメソッドを必ず実装しなければいけない為、Talkメソッドを記述します。
それぞれ別の処理を書いておけば、話す内容を別にする事が出来ます。
抽象クラスであるHumanクラスをobjectクラスを継承する形で作成してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | using UnityEngine; using System.Collections; public abstract class Human : object { // 名前 private string name; // 性別 private string gender; public Human(string name = "DefaultName", string gender = "man") { this.name = name; this.gender = gender; } // Humanクラス特有のメソッド(名前を取得) public string GetName() { return name; } // Humanクラス特有のメソッド(性別を取得) public string GetGender() { return gender; } // override出来るメソッド public virtual void Talk(string talk) { Debug.Log ("これから話します"); } // 抽象メソッド(中身は継承先で実装する必要がある) public abstract void Walk(); } |
Humanクラスでは名前と性別のフィールドとそれらを初期化するコンストラクタ、フィールドのゲッターを用意します。
Humanクラスでは継承先でオーバーライド(上書き)するべきTalkメソッドをvirtualを付けて宣言しています。
Walkは抽象メソッドで、継承先で実装する必要があります。
継承先で継承元と同じメソッドを定義出来るのは(しなくてはいけない物も含む)は継承元でvirtualかabstractが付けられたメソッドに限ります。
抽象クラスもインタフェースと同じように抽象メソッドを含みますので、インスタンス化しようと思うとエラーが発生します。
インタフェース(ICommand)と抽象クラス(Human)を継承してCharaクラスを作成
インタフェースと抽象クラスの作成が出来たので、それらを継承してCharaクラスを作成します。
インタフェースは多重継承可能ですが、通常のクラスや抽象クラスの多重継承は出来ません。
今回の場合はインタフェースと抽象クラスを継承するので、:の後に,を使って継承するクラスを指定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | using UnityEngine; using System.Collections; // 抽象クラスHumanとインタフェースICommandを継承してCharaクラスを作成 public class Chara : Human, ICommand { // 職業 private string job; // nameとgenderは親クラスのコンストラクタを呼び出して設定 public Chara(string job, string name, string gender) : base(name, gender) { this.job = job; } public string GetJob() { return job; } public void Attack() { Debug.Log ("攻撃するざんす"); } public void Defence() { Debug.Log ("防御するなり"); } public void Item() { Debug.Log ("アイテムつかっちゃうよん"); } // HumanクラスのTalkメソッドをオーバーライドする public override void Talk(string talk = "初期設定の話") { base.Talk (talk); } // Humanクラスの抽象メソッドを実装する public override void Walk() { Debug.Log ("歩きましょ"); } } |
Charaクラスのコンストラクタでは:の後に親クラスのコンストラクタを呼び出し引数の値を親クラスのフィールドにも設定しています。
抽象クラスであるHumanでTalkメソッドはvirtual、Walkはabstractで宣言されている為に、Charaクラスでoverrideを付けてメソッドをオーバーライドしています。
インタフェースで宣言された抽象メソッドや抽象クラスで宣言された抽象メソッドは継承先の子クラスで実装する必要があります。
子クラスで実装されなかった場合はそのクラスを継承するクラスで実装します。
抽象メソッドが実装されていない場合インスタンス化が出来ません。
Charaクラスをインスタンス化して実行する
Charaクラスはインスタンス化して使用するクラスなのでそれを使うクラスを作成し、インスタンス化してみます。
Charaクラスをインスタンス化するクラスをMakeCharacterクラスとし、Unityのゲームオブジェクトに取り付けて使用する為、MonoBehaviourクラスを継承して作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | using UnityEngine; using System.Collections; public class MakeCharacter : MonoBehaviour { // Use this for initialization void Start () { // インタフェースと抽象クラスのインスタンス化(そもそも出来ない) //ICommand iCommand = new ICommand(); //Human human = new Human (); // Charaクラスの生成 Chara masashi = new Chara ("Wizard", "Masashi", "man"); // CharaクラスのGetJobメソッドの呼び出し Debug.Log(masashi.GetJob ()); // HumanクラスのGetNameメソッドとGetGenderメソッドの呼び出し Debug.Log(masashi.GetName ()); Debug.Log(masashi.GetGender ()); // overrideしているのでCharaクラスで定義されたTalkとWalkが実行される masashi.Talk ("こんちは"); masashi.Walk (); // 親のクラスに入れる事が出来る Human hanako = new Chara("Soldier", "Hanako", "woman"); // CharaクラスのGetJobメソッドの呼び出し(エラーになる) // Debug.Log(hanako.GetJob()); // HumanクラスのGetNameメソッドとGetGenderメソッドの呼び出し Debug.Log(hanako.GetName ()); Debug.Log (hanako.GetGender ()); // TalkはHumanクラスで定義されているが、CharaクラスでoverrideしてるのでCharaクラスのTalkが実行される hanako.Talk ("こんにちは"); // WalkはHumanクラスで抽象メソッドとして定義されている為、Charaクラスで実装し、そのWalkが実行される hanako.Walk (); // 元はCharaクラスだがHumanクラスのhanakoの参照変数に入れているので呼び出せない //hanako.Attack (); // キャストすれば呼び出せる ((Chara) hanako).Attack (); } } |
↑のようにCharaクラスからmasashiとhanakoを作成してます。
masashiの場合はChara型の変数に参照を入れている為、自身や親クラスのメソッドを呼び出せています(アクセス修飾子がpublicな為)。
hanakoの場合はCharaの親クラスのHuman型の変数に参照を入れています。
その為Charaクラスで定義されているAttackメソッド等はキャストをしないとエラーになります(親クラスのメソッドにvirtualが付いて子クラスでoverrideしていれば出ない)。
親クラスの型の変数に代入する利点は同じ親クラスを持つ2つのクラスがあったとして、親クラスのメソッドを共通して使えたりする事です。
これに関しては後で実験してみます。
MakeCharacterスクリプトをMainCamera等に取り付けUnityを実行して確認してみてください。
メソッドのオーバーライドについて
親クラスの抽象メソッドを子クラス側でオーバーライドする際、親クラスのメソッドでvirtualかabstractのキーワードが付いている必要があります。
virtualは仮想メソッドのキーワードでこれが付いているメソッドは、子クラスで同じメソッドをoverrideキーワードを付けて上書きする必要があります。
この場合、子クラスを親クラスの参照変数に入れて親クラスのメソッドを呼び出した場合子クラスのメソッドが実行されます。
試しに親クラスのHumanにvirtualのSleepメソッド、子クラスのCharaにoverrideしたSleepメソッドを書きます。
1 2 3 4 5 6 | // Humanに新しくSleepを作る public virtual void Sleep() { Debug.Log ("HumanクラスのSleepメソッド"); } |
1 2 3 4 5 6 | // CharaにHumanのSleepをオーバーライドして作る public override void Sleep() { Debug.Log ("CharaクラスのSleepメソッド"); } |
↑のようにし、MakeCharacterクラスで、masashi、hanakoでそれぞれSleepを呼び出してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | using UnityEngine; using System.Collections; public class MakeCharacter : MonoBehaviour { // Use this for initialization void Start () { Chara masashi = new Chara ("Wizard", "Masashi", "man"); masashi.Sleep (); Human hanako = new Chara("Soldier", "Hanako", "woman"); hanako.Sleep (); } } |
↑を実行すると、ともに「CharaクラスのSleepメソッド」が表示されます。
メソッドをoverrideキーワードでオーバーライドすると自身のクラスのメソッドが呼ばれるようです。
hanakoの場合はHuman型の参照変数に入れているので、Humanクラスのメソッドを呼び出すようにしたい場合もあります。
そんな時はCharaクラスのSleepメソッドをoverrideではなくnewキーワードを付けて記述します。
1 2 3 4 5 6 | // CharaクラスのSleepメソッド public new void Sleep() { Debug.Log ("CharaクラスのSleepメソッド"); } |
先ほどのスクリプトを実行すると、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | using UnityEngine; using System.Collections; public class MakeCharacter : MonoBehaviour { // Use this for initialization void Start () { Chara masashi = new Chara ("Wizard", "Masashi", "man"); masashi.Sleep (); Human hanako = new Chara("Soldier", "Hanako", "woman"); hanako.Sleep (); } } |
masashi.Sleepでは「CharaクラスのSleepメソッド」
hanako.Sleepでは「HumanクラスのSleepメソッド」
がコンソールに表示されます。
このようにすれば、Humanクラスを継承した子クラスをHuman型の参照変数に入れておけば、Humanクラスのメソッドを実行する事が出来ます。
newキーワードはポリモーフィズム(多態性)が使えなくなるので使わない方がいいみたいです・・・(^_^;)
virtualはoverrideして親クラスのSleepを使う時は、親クラスにキャストして使った方が良さそうですね。