共通の機能だけど、違う動作をさせたい
今回も、継承@の例を元に説明していきます。
継承@では、戦闘機・自機・敵機の関係を継承の機能を用いて記述しました。以下のコードは継承@で載せたサンプルコードです。
public class Combat { public void Move(int X, int Y); //移動 public void Shot(); //レーザーを打つ public void OnDraw();//描画 } public class Enemy : Combat { public void AI(); //AI制御 } public class Player : Combat { public void KeyControl(); //キーコントロール } |
このコードは、戦闘機を表す「Combat」クラスには移動・レーザーを打つ・描画する機能を持たせ、派生した各クラスには別の名前で機体を制御するメソッドが実装されています。「Enemy」と「Player」クラスで別の動作をさせるためなのですが、これは、実質「AI()」と「KeyControl()」メソッドは機体をコントロールするという同じ機能を実装しているだけです。他にも、レーザーを打つという動作を「Enemy」と「Player」クラスで分けたいかもしれませんし、描画も別の動作をさせたいかもしれません。
このような機能を実装できるのが今回紹介するオーバーライドです。オーバーライドは、派生元のメソッドの内容を書き換えてしまう機能です。
簡単なオーバーライド
まずは、上で紹介した例をコードに記述してみます。オーバーライドに関連するキーワードは以下の3つを知っておけば大丈夫でしょう。
アクセス修飾詞 | 意味 |
---|---|
override | メソッドがオーバーライドしていること(仮想を実体化すること)を示す |
virtual |
仮想メソッドであることを示す |
abstract | 抽象メソッドであることを示す |
仮想やら、抽象やら何か嫌な言葉が出てきます。仮想メソッドと抽象メソッドの違いは後で紹介するとして、とりあえず「virtual」と「override」を使ってサンプルを作ってみます。
まずは、書き換えられる可能性のあるメソッドに「virtual」キーワードを指定します。
public class Combat { public void Move(int X, int Y) //移動 { //移動処理 } public void Shot() //レーザーを打つ { //レーザーを打つ処理 } public virtual void OnDraw() //描画 { //オーバーライド予定なので何もしない } public virtual void Control() //戦闘機を操作する { //オーバーライド予定なので何もしない } } |
今回は、「Combat」クラスのうちで機体を操作する「Control()」と描画する「OnDraw()」の2つを後(派生先)で書き換える予定なので、この二つに「virtual」を指定します。次に、派生先でこの二つのメソッドをオーバーライドします。
public class Enemy : Combat { public override void Control() { //敵機のコントロール処理 } public override void OnDraw() { //敵機の描画処理 } } public class Player : Combat { public override void Control() { //自機のコントロール処理 } public override void OnDraw() { //自機の描画処理 } } |
これで、オーバーライドの完了です。これで、PlayerとEnemyオブジェクトは、同じ名前の「Control()」メソッドを呼び出しても別の動きをします。結構簡単ですね。
抽象クラス・抽象メソッド
さて、ここで「Combat」クラスの役割を考えてみましょう。「Combat」クラスは「これを継承したクラスは全て戦闘機である」ことを表すクラスなのですが、はたしてゲーム中に戦闘機というものが存在する必要があるでしょうか?おそらく、敵味方良く分からないただの戦闘機というのはあまり必要はないでしょう。つまり、「Combat」クラスは実体化(インスタンスを作る)必要のないクラスであり、戦闘機という概念を表すためだけのクラスなのです。
このようなクラスを抽象クラスと呼び(ホントかな)、クラスの前に「abstract」キーワードを追加します。抽象クラスはインスタンスを作ることが出来ません。new〜と出来ないのです。つまり、継承するためのクラスになります。
また、抽象クラスの中だけで作れるメソッドがあります。それが、抽象メソッドです。抽象メソッドもメソッドの前に「abstract」キーワードを追加します。抽象メソッドは派生先で必ずオーバーライドする必要があり、メソッドの中身を書くことは出来ません。また、派生先のクラスでは必ず抽象メソッドをオーバーライドしなくてはいけません。
説明だけしても分かりにくいので、Combatクラスを抽象クラス化してみます。
public abstract class Combat { public void Move(int X, int Y) //移動 { //移動処理 } public void Shot() //レーザーを打つ { //レーザーを打つ処理 } public abstract void OnDraw(); //描画 public abstract void Control(); //戦闘機を操作する } |
これで、Combatクラスは抽象クラスに、Control()とOnDraw()クラスは抽象メソッドになりました。抽象メソッドには中身の動作が書かれていませんね。抽象メソッドではこのようにメソッドの宣言だけしておけばいいのです。
何がうれしい?オーバーライド
さて、これでオーバーライドを実装し、クラス同士の関係やメソッドの名前を綺麗に記述できたわけです。しかし、ただ名前を統一したいだけなら何もオーバーライドする必要はありません。たとえば、継承@の例のようにCombatクラスを作り、「AI()」と「KeyControl()」の名前を「Contorl()」としてしまえばいいだけです。では、何故わざわざオーバーライドをするのでしょうか?
継承はクラスの関係性を表せます。「Enemy」も「Player」も「Combat」としての属性を持っており「Combat」の一種と見なせるわけです。このような扱い方をC#ではキャストを使って表現できます。例えば、
Player p = new Player(); //Combat型に(暗黙)キャスト Combat combat = p; //Combat型変数にPlayer型のオブジェクトを代入 Combat combat1 = new Player(); //Combat型変数にEnemy型のオブジェクトを代入 Combat combat2 = new Enemy(); |
と、「Player」も「Enemy」も生成したインスタンスを「Combat」型の変数に代入して扱うことが出来ます。この機能を知っていてこそ、オーバーライドの真価ポリモーフィズムの効果が発揮されます。これを利用するとこんな動作が出来ます。
Combat combat; //combatにPlayerオブジェクトを代入 combat = new Player(); //Playerで定義されたControl()が呼び出される combat.Control(); //combatにEnemyオブジェクトを代入 combat = new Enemy(); //Enemyで定義されたControl()が呼び出される combat.Control(); |
一回目の「combat.Control();」では、combatがPlayer型のオブジェクトをさしているのでPlayerクラス内で定義されたControl()メソッドが呼び出され、2回目では、combatがEnemy型のオブジェクトをさしているのでEnemyクラス内で定義されたControl()メソッドを呼び出します。もし、Control()をオーバーライドしていなかったらこのようなことは出来ません。これは、CombatクラスにControlメソッドがあることが保証されているからこと出来るのです。
まとめ
今回は、オーバーライド・抽象クラス・ポリモーフィズムとちょっとボリュームが多かったですね。ここら辺をうまく使ってプログラムすると、様々なときに便利になります。(投げやりな感想)
次回は…ネタが思いつきませんね…とりあえず、配列、リスト、あとは〜C#2.0のジェネリックでも扱いましょうか。