今回の記事ではクラスの継承についてみていきます。
Unityではコンポーネント自体がUnityEngine.MoneBehaviour
を継承したものなので実は既に使用している機能になります。
さらにUnityEngine.MonoBehaviour
はUnityEngine.Component
を継承しており、クラスの継承はUnityを用いたアプリ開発に常に使われている機能になります。
前回の記事:
継承とは
継承とはあるクラスのメンバを共有する機能のことになります。
クラスの継承元を基底クラス(Base Class)や親クラスと呼び、継承先を派生クラス(Inherited Class)や子クラスと呼びます。継承関係になるクラスの塊をクラス階層と呼びます。
継承の書き方は次の様になります。
class <クラス名> : <継承するクラス名> { ... }
継承したクラスのメンバは継承先のクラスでも使用できます。が、privateのようなアクセス制限が指定されている場合は継承先でも使用できないので注意してください。
一つのクラスに継承できるクラスは一つだけになります。既に他のクラスを継承しているクラスをさらに継承することも可能です。
また、派生クラスは基底クラスとして型変換することができます。この性質を利用することがオブジェクト指向でプログラミングする上のセオリーとなります。
1 2 3 4 5 6 7 8 9 10 11 12 |
class Base { public int Value; protected int InnerValue = 10; } class A : Base { //基底クラスBaseを継承した派生クラスAを作る public int GetInnerValue { get => InnerValue; } } var a = new A(); a.Value = 0; // <- OK var n = a.GetInnerValue; // <- OK |
isキーワードとasキーワード
is
キーワードとas
キーワードを使用すると値が指定した型なのかどうか判定できたり、型変換することができます。
これらのキーワードを使用することで、クラスの型が指定した基底クラスを継承しているのか確認したり、基底クラスを指定した派生クラスへ変換することができます。
使い方は以下の様になります。
is
キーワード:<値> is <型名>
。真偽判定を行いbool
型を返す。
as
キーワード:<値> as <型名>
。値を型名の型に変換する。変換できない場合はnull
を返す。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Base {} class A : Base {} class B : A {} class C {} //isキーワードの使い方 var inst = new B(); bool b1 = inst is B; // <- true bool b2 = inst is A; // <- true bool b3 = inst is Base; // <- true bool b4 = inst is C; // <- false //asキーワードの使い方 A a = inst as A; // <- OK Base base = inst as Base; // <- OK C c = inst as C; // <- NG c == null |
null(ヌル)について
null
キーワードについての解説を行います。
null
は参照値が何も実体データを差していないことを表す値になります。
null
になっている参照値を使用するとSystem.NullReferenceExceptionという例外が発生します。そのためメンバにアクセスすることができません。
null
は参照値が何も参照させないようにする時にも使用できます。その時は単にnull
を代入してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class A { public int Value; } var inst = new A(); bool b1 = (inst == null); // <- false inst.Value = 10; // <- OK try { inst = null; bool b2 = (inst == null); // <- true inst.Value = 0; // <- 例外発生 } catch(System.NullAccessException e) { // } |
null条件文演算子について(?演算子と??演算子)
参照値はnullの場合があるため通常はif文を使って参照値がnullでないか確認する必要があります。
ですが毎回そのように書くのは大変なので、省略した書き方として?演算子を使用すると、参照値がnullでない時のみメンバにアクセスしてくれます。参照値が文字列型の場合はnullを、値型の場合は0を返します。
また、??演算子をその後に書くと参照値がnullの時でも指定した値を返すことができます。
これらの演算子は配列の要素アクセス([]
)にも使用することができます。
?演算子の書き方:<参照値> ?. <メンバ名>
??演算子の書き方:<参照値> ?. <メンバ名> ?? <nullの時の値>
1 2 3 4 5 6 7 8 9 10 11 12 |
class A { public int Value = 100; } //?演算子の使い方 var inst = new A(); int n = inst?.Value; // <- n == 100 //nullにした時の?演算子と??演算子の挙動の確認 inst = null; n = inst?.Value; // <- nはint型のデフォルト値(0)になる。 n = inst?.Value ?? -1; // <- n == -1 |
一つ注意点として、Unityが用意している参照型には?演算子および??演算子を使用できません。間違って使用した時はコンパイルエラーが発生しますので注意してください。
newキーワードと特殊な変数thisとbaseについて
クラス継承した時に派生先のクラスでは基底クラスと同じ名前のメンバは定義できません。
どうしても定義したい場合は回避策としてメンバにnew
キーワードを指定するとできます。が、混乱をもたらす原因になりますのでnewキーワードの使用は避けましょう!
new
キーワードの使い方:new <メンバ宣言>
またC#にはthis
キーワードとbase
キーワードという特殊な変数が存在します。これらは次の様な意味合いになります。
this
:自分自身のインスタンスを表す特殊な変数。base
:継承元のクラスのインスタンスを表す特殊な変数。
この二つの変数は引数名とメンバ名が被った時やnewキーワードを使ったことにより同じメンバ名がある時など、どのクラスのメンバなのかわからない場合に所属先を明示したいときに使用できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Base { public int Value = 0; } public class A : Base { public new float Value = 0; public void Method(int Value) { var n = Value; // <- 引数のValue var n2 = this.Value; // <- AのValue(float型) var n3 = base.Value; // <- BaseのValue(int型) } } |
【実践】Unityのクラスに他のクラスのメンバーを継承させてみよう!
それでは実際にクラスの継承を試してみましょう!
次のサンプルコードでは次の二つのコンポーネントを定義しています。
- BaseComponent:
UnityEngine.MonoBehaviour
を継承したクラス。GetMove()メソッドを定義している。 - Sample:BaseComponentを継承したクラス。Inspectorからはこちらをアタッチする。
GameObjectにアタッチできるものはSampleコンポーネントの方なのでそちらを適当なGameObjectにアタッチして再生してください。
再生するとBaseComponentで定義されたGetMove()メソッドによってGameObjectが移動します。
今回のサンプルコードの注目点としてはSampleコンポーネントの中で継承したBaseComponentのメソッドを使用している点になります。
サンプルコードはとても簡単な作りですが、実践的にもクラスの継承はGetMove()メソッドの様に処理の共通化と定義を一つのクラスにまとめる役割で使われることが多いです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
using UnityEngine; //継承元のクラスを定義 // 一応、一つのファイルに複数のコンポーネントを指定できるが、 // Unityエディタからアタッチできるのはファイル名と一致するものだけになる。 public class BaseComponent : MonoBehaviour { public Vector3 GetMove(float speed) { var move = Vector3.zero; move.x = speed * Time.deltaTime; return move; } } //上で定義したBaseComponentを継承している。 // BaseComponentのpublic,internal,protectedなどの // アクセス修飾子が指定されたメンバはこのクラスでも使用できる。 public class Sample : BaseComponent { public float Speed = 0f; void Update() { //BaseComponent.GetMove(float)を使用している。 transform.position += GetMove(Speed); } } |
ちなみに今回のサンプルでは一つのスクリプト内に複数個のコンポーネントを定義していますが、実際にUnityエディターからアタッチできるのはファイル名と一致したコンポーネントになります。
なので、ここではSampleコンポーネントだけをアタッチすることができます。一応、BaseComponentもスクリプト上からGameObjectにアタッチすることができるので豆知識として覚えておくといいでしょう。
仮想メンバとオーバーライドとは
仮想メンバとはvirtual
キーワードを指定したメンバのことで、派生先のクラスでその定義を上書きすることができるものになります。メンバを上書きすることをオーバーライド(Override)と呼んだりします。
書き方の簡単な例は以下のものになります。
- 仮想メンバ:
virtual <メンバ宣言>
- オーバーライド:
override <メンバ宣言>
オーバーライドすることができるメンバは以下のものになります。
- メソッド
- プロパティ:get,setの片方だけ仮想化することができます。
- イベント(※):意味合いは異なりますがプロパティと似たような書き方になります。
メンバのオーバーライドを使用することで基底クラスの一部分をカスタマイズすることができます。
メンバのオーバーライドはオブジェクト指向の重要な考え方であるポリモーフィズムを実現する際に必須となるものなので、覚えていてください。
(※イベント(Event)はdelegate型と関係するクラスのメンバになります。詳しくは他の記事で解説します。)
抽象クラスとは
抽象クラスはabstract
キーワードを指定したクラスのことで、そのクラスはインスタンスを作成することができません。
そのため抽象クラスは継承することを前提としたクラスになります。
抽象メンバについて
クラスのメンバにもabstract
を指定することができます。使い方および指定できるメンバは仮想メンバと同じになります。こちらもオーバーライドすることができます。
仮想メンバとの大きな違いとしては、抽象メンバはその宣言の時に定義が不必要な点になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
abstract class Base { public abstract void Method(); } var base = new Base(); // <- NG class A : Base { public override void Method() { // ... } } var inst = new A(); inst.Method(); // <- OK |
sealedキーワードについて
sealed
キーワードをクラスに指定するとそのクラスを基底クラスとして使用できなくなります。
抽象クラスは継承することを前提としたものでしたが、sealedキーワードをつけたものは反対に継承を禁止します。
sealedキーワードは仮想メンバにも指定することができます。こちらも同様で指定した仮想メンバはそれ以降の派生先ではオーバーライドすることができなくなります。
1 2 3 4 5 6 7 8 9 10 11 |
class Base { public virtual void Method() {} } sealed class A : Base { public sealed override void Method() {} } class B : A { // <- NG public override void Method() {} // <- NG } |
【実践】Unityで抽象メソッドとオーバーライドを実装する
それでは次は抽象メソッドとオーバーライドを試してみましょう!
次のサンプルコードは先に出てきたサンプルコードを次のように修正したものになります。
- BaseComponentを抽象クラスとして定義している。
- BaseComponent.GetMove()メソッドの中でGetMoveImpl()メソッドを呼び出しその中でmoveを設定している。
- BaseComponent.GetMove()メソッドの最後でmoveにTime.deltaTimeを掛けている。これは継承先のGetMoveImpl()メソッドでの掛け忘れを防ぐためのものである。
- BaseComponentの中でUpdate()メソッドを定義しているので派生クラス先でUpdate()を毎回定義しなくても良くなっている。
抽象クラスを使うことでスクリプトの内容が複雑になった感じですが、派生先のクラス(Sampleコンポーネント)ではGetMoveImpl()メソッドをオーバーライドするだけでGameObjectを好きに移動させることができるようになりました。
BaseComponentの中で派生先で似たような処理を毎回書かなくてもいいように処理の共通化を行ったわけですね。
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 |
using UnityEngine; //抽象クラスとしてBaseComponentを定義する。 // このクラスのインスタンスは生成できないので注意。 public abstract class BaseComponent : MonoBehaviour { protected abstract void GetMoveImpl(ref Vector3 move, float speed); public float Speed = 0f; //細かいmoveの値の設定は継承先に任せて、 // 必ずTime.deltaTimeをmoveにかけるようにしている。 public Vector3 GetMove(float speed) { var move = Vector3.zero; GetMoveImpl(ref move, speed); move *= Time.deltaTime; return move; } void Update () { transform.position += GetMove(Speed); } } public class Sample : BaseComponent { //moveの値を設定している。 // BaseComponent.GetMove()の中で呼び出されるので、moveには設定した後にTime.deltaTimeがかけられることに注意。 protected override void GetMoveImpl(ref Vector3 move, float speed) { move.y = speed; } } |
まとめ
今回の記事をまとめますと以下の様になります。
- クラス定義の際に他のクラスを継承することができる。
- 継承元を基底クラスや親クラスと呼ぶ。
- 継承したクラスを派生クラスや子クラスと呼ぶ。
- クラス継承によって構築された型同士の関係をクラス階層と呼ぶ。
- あるクラスを継承した時はそのクラスのメンバも使用することができる。
- 基底クラスに同名メンバがあってもnewキーワードを使用するとメンバ宣言できる。
- thisキーワードはそのクラスのインスタンス自身を表す特殊な変数。
- baseキーワードはそのクラスの基底クラスを表す特殊な変数。
- 仮想メソッドは派生先のクラスでメソッドの定義を上書きできると指定したメソッド。
- 抽象クラスはインスタンス生成できず、継承することを前提としたクラス。
- 抽象メソッドは派生先のクラスで定義することを強要するメソッド。
- 抽象メソッドの定義はいらない。
- 仮想メソッドおよび抽象メソッドはoverrideキーワードを使用することで上書きできる。
以上になります。それでは次の記事に行ってみましょう!
次回の記事:
コメント