クラスの継承②

動作の書き換え

オーバーライド

overrideキーワードを付与することで、継承元のメソッドと同名・同シグネチャのメソッドを継承先で定義(オーバーライド)することができます。
これに伴い、継承元のメソッドは外部から使用できなくなります。
オーバーライド可能なメソッドには以下のキーワードを付与します。

virtual

継承元で規定の動作を定義しつつ、継承先でのオーバーライドを許容する場合に付与します。
継承先でオーバーライドしない場合、常に継承元のメソッドが呼び出されます。

public virtual void Fuga()
{
     // 規定の動作を実装
}
abstract

継承元で規定の動作を定義せず、継承先でのオーバーライド必須とする場合に付与します。
abstractなメソッドは本体を記述しません。

public abstract void Fuga();

継承元で規定の動作を定義せず、継承先に委ねるケースで使用します。

base呼び出し

baseキーワードを用いて継承元のメソッドを呼び出すことができます。

public override void Fuga()
{
    base.Fuga();
}

継承元の処理を呼び出しつつ、継承先独自の処理も行いたいケースなどで有用です。

new

newキーワードを付与することで、継承元のメソッドを上書きできます。
継承元の同名・同シグネチャのメソッドにvirtualキーワードが付与されていなくても可能です。
ただし、継承元でvirtualキーワード付与されていないということは、継承先で動作を書き換えられることを想定していないということなので、使用には注意が必要です。

オーバーライドとnewの動作の違い

オーバーライドされたメソッドの呼び出しは、呼び出し時の型に関係なく、インスタンス本来の型で決定されます。
newされたメソッドの呼び出しは、インスタンス本来の型に関係なく、呼び出し時の型で決定されます。

以下のようなクラスを用意して説明します。

public class HogeBase1
{
    public virtual void Fuga() => Console.WriteLine("HogeBase1");
    public void Piyo() => Console.WriteLine("HogeBase1");
}

public class HogeBase2
{
    public override void Fuga() => Console.WriteLine("HogeBase2");
    public new void Piyo() => Console.WriteLine("HogeBase2");
}

public class Hoge
{
    public override void Fuga() => Console.WriteLine("Hoge");
    public new void Piyo() => Console.WriteLine("Hoge");
}

Fugaメソッドについて、実体がHogeの場合は常に"Hoge"が出力されます。

var hoge = new Hoge();
hoge.Fuga();
((HogeBase2)hoge).Fuga();
((HogeBase1)hoge).Fuga();

Piyoメソッドについて、呼び出し時の型で出力結果が変わります。

var hoge = new Hoge();
hoge.Piyo();
((HogeBase2)hoge).Piyo();
((HogeBase1)hoge).Piyo();

実行結果はこうなります。

Hoge
HogeBase2
HogeBase1

継承の抑止

sealedキーワードを付与することで、継承を抑止できます。
メソッドにsealedキーワードを付与した場合、継承先クラスで当該メソッドをオーバーライドできなくなります。
基底クラスでsealedを付与することはありません。
多段継承していて、中間層のクラスで継承を止めたい場合に有効です。

public class HogeBase1
{
    public virtual Fuga()
    {}
}

public class HogeBase2 : HogeBase1
{
    // 以降の継承を抑止
    public override sealed Fuga()
    {}
}

public class Hoge
{
    // コンパイルエラー
    public override Fuga()
    {}
}

クラスにsealedキーワード付与した場合、当該クラスを継承したクラスを定義できなくなります。

public sealed class HogeBase
{}

// コンパイルエラー
public class Hoge : HogeBase
{}

クラスの継承①

概要

クラスは単一の継承元クラスを指定することができます。
それにより、継承元クラスの持つ機能・情報を共用、更には拡張することができます。
抽象的な情報を持つクラスを継承元とし、継承先でより具体的な情報を持たせるといった形で使用します。

継承の宣言

クラス定義の後に「: {継承元クラス名}」を指定することで継承を宣言できます。

public class HogeBase
{ }

public class Hoge : HogeBase
{ }

コンストラクター

継承先クラスのインスタンスを生成する際、継承元クラス→継承先クラスの順でコンストラクターが呼び出されます。
継承元クラスに明示的にコンストラクター実装されていない場合、自動生成の引数なしコンストラクターが自動的に呼び出されます。
引数ありコンストラクターのみが明示的に実装されている場合、baseキーワードを使用して該当コンストラクターを明示的に呼び出す必要があります。

public class HogeBase
{
    public HogeBase(object obj)
    { }
}

public class Hoge : HogeBase
{
    // HogeBaseのコンストラクターを明示的に呼び出し
    public Hoge() : base(new object())
    { }
}

継承元機能の利用

継承先クラス型のインスタンスから継承元クラスの機能を呼び出すことができます。
また、継承先クラス内で継承元クラスの機能を利用できます。

以下のようなクラス構成の場合、HogeインスタンスからFuga()を呼び出すことができ、
また、内部でMoge()も呼び出すことができます。

public class HogeBase
{
    public void Fuga()
    { }

    protected void Moge()
    { }
}

public class Hoge : HogeBase
{
    public void Piyo()
    { 
        Moge();
    }
}
var hoge = new Hoge();
hoge.Fuga();
hoge.Piyo();

型の変換

暗黙的型変換

暗黙的型変換は明示的に型の変換を指示することなく、自動的に型変換する仕組みです。
以下条件を満たす場合に暗黙的型変換が利用できます。

  1. 情報量の少ない型から大きい型への変換
  2. 派生型から基底型への変換
  3. インターフェース型への変換
// 1
int a = 1;
long b = a;

// 2
Hoge hoge = new Hoge();
HogeBase hogeBase = hoge();

// 3
IHoge iHoge = hoge;

例として変数への代入を示していますが、メソッドのパラメーターとして渡す場合なども同様です。

明示的型変換(キャスト)

明示的型変換はキャスト演算子を用いることで明示的に型変換する仕組みです。
以下条件を満たす場合に明示的型変換が利用できます。

  1. 情報量の大きい型から小さい型への変換
  2. 基底型から派生型への変換
  3. インターフェース型からの変換
// 1
long a = 1;
int b = (int)a;

// 2
HogeBase hogeBase = new HogeBase();
Hoge hoge = (Hoge)hogeBase;

// 3
IHoge iHoge = new Hoge();
Hoge hoge = (Hoge)iHoge;

上記の例では、キャストを外すとコンパイルエラーになります。

演算子オーバーロード

暗黙的型変換と明示的型変換は演算子オーバーロードすることで動作をカスタムマイズできます。
以下に互換性のないクラス間での型変換を可能にする演算子オーバーロードを例示します。

class Hoge
{
    public int Num { get; set; }

    // 暗黙的型変換の演算子オーバーロード
    public static implicit operator Piyo(Hoge hoge)
    {
        return new Piyo { Num = hoge.Num };
    }

    // 明示的型変換の演算子オーバーロード
    public static explicit operator Fuga(Hoge hoge)
    {
        return new Fuga { Num = (byte)hoge.Num };
    }
}

class Fuga
{
    public byte Num { get; set; }
}

class Piyo
{
    public long Num { get; set; }
}

上記の演算子定義により、以下のように型変換が可能になります。

Hoge hoge = new Hoge();
Fuga fuga = (Fuga)hoge;
Piyo piyo = hoge;

暗黙的型変換のオーバーロードは利用側で意図しない変換が行われてしまう可能性があるため、オーバーロードする場合は注意が必要です。
また、オーバーロードした演算子は後述するas・isでは使用されません。

as

{変換対象} as {変換先型名}のフォーマットで型変換することができます。
変換に失敗した場合はnullが返されます。
当然ですが、変換先型名には参照型・null許容の値型のみ指定可能です。

var hoge = new Hoge();
// 変換できない場合はfugaがnullになる。
var fuga = hoge as Fuga;

is

{検証対象} is {期待値型名}のフォーマットで型チェックが可能です。

Hoge hoge = new Hoge();
bool result = hoge is Fuga;

また、{検証対象} is {期待値型名} {変換結果格納変数}のフォーマットで型チェックと変換を行うことができます。 これはifの条件で使用可能です。

if (hoge is Fuga fuga)
{
    // ステートメント内で変数fugaが使用可能
}

概要

型には値型と参照型の2種類があります。

値型

データの値そのものを保持する型を値型といいます。
構造体・列挙型が該当します。

構造体(struct)

structキーワードを付与して定義されたものは構造体となり、値型として扱われます。

public struct Hoge
{
     public int Value;
}

列挙型(enum)

enumキーワード付与して定義されたものは列挙型となり、値型として扱われます。

public enum Hoge
{
     Value,
}

値型の動作

値型は値そのものなので、nullになりません。
変数などで宣言した時点で初期値を持ちます。
内包する要素も各々の型のデフォルト値で初期化されます。
以下コードでは「0」が出力されます。

Hoge hoge;
Console.WriteLine(hoge.Value);

また、他の変数・プロパティなどに代入した時点で、それぞれ別物として扱われます。
以下のコードでは各変数のValueはそれぞれ異なる値になります。

Hoge hoge1 = new Hoge();
Hoge hoge2 = hoge1;
Hoge hoge3 = hoge2;
hoge1.Value = 1;
hoge2.Value = 2;
hoge3.Value = 3;

値型はスタックメモリ上で管理されます。
ただし、値のスコープがメソッド内など明確に限定されるケースのみで、
クラスメンバ・クロージャなどの場合はヒープメモリ上で管理されます。

参照型

データへの参照を保持する型を参照型といいます。
クラスなどが該当します。

クラス(class)

classキーワードを付与して定義されたものはクラスとなり、参照型として扱われます。

public class Hoge
{
    public int Value;
}

参照型の動作

参照型は参照を保持するため、宣言しただけでは何の参照も保持しておらず、nullになります。

Hoge hoge;
// 未初期化変数へのアクセスでコンパイルエラーになる
Console.WriteLine(hoge.Value);

また、他の変数・プロパティに代入しても全て同じインスタンスを参照します。
以下のコードでは各変数のValueは全て3になります。

Hoge hoge1 = new Hoge();
Hoge hoge2 = hoge1;
Hoge hoge3 = hoge2;
hoge1.Value = 1;
hoge2.Value = 2;
hoge3.Value = 3;

参照型はヒープメモリ上で管理されます。

スタック・ヒープ

かなりざっくりした解釈ですが、まとめておきます。

スタック
  • 先入れ後出しで情報を管理するメモリ領域
  • 高速だが、領域サイズに制限があるため、大きいデータを扱うには不向き
  • 先入れ後出しのため、メモリ開放順序がランダムになるようケースでは使えない
    →クラスメンバ・クロージャが扱えないのはこのため
ヒープ
  • 任意の順序で情報を管理するメモリ領域
  • 低速だが、領域サイズを大きく取れるため、大きいデータを扱える
  • 先入れ先出しのような制約がないため、メモリ開放順序がランダムになるケースで使える

例外

概要

処理中に発生した異常を例外(Exception)という形で上流の処理に通知できます。
通知された側は例外に応じて適切なハンドリングを行うことで、プログラム実行中の異常をコントロールします。
ただし、例外は重たい処理なので、要否を適切に判断する必要があります。

例外の定義

すべての例外はExceptionクラスを継承して定義されます。

public class HogeException : Exception
{
}

発生した異常の種類に応じた例外クラスを定義します。

  • IOException
  • SqlException etc...

例外の通知

例外クラスのインスタンスをスローします。
メッセージなどを指定可能です。

throw new HogeException("Exception occurred");

例外のハンドリング

try/catchステートメントでハンドリングします。

try
{
    throw new HogeException();
}
catch
{
    Console.WriteLine("Exception occurred")
}

例外の種類を指定したり、フィルターすることができます。
複数の例外のハンドリング処理を記述することもできま す。

catch (HogeException e)
{
     // 例外がHogeExceptionの場合に処理する
     Console.WriteLine(e.Message);
}
catch (FugaException e) when (e.Message == "a")
{
     // 例外がFugaExceptionかつ、Messageの値が"a"の場合に処理する
}
catch (FugaException e) when (e.Message == "b")
{
    // 例外がFugaExceptionかつ、Messageの値が"b"の場合に処理する
}
catch (Exception e)
{
     // それ以外の場合に処理する
}

上流で確実に処理される場合を除き、漏れを防ぐためにException型のハンドリング処理を入れます。
例外がハンドリングされなかった場合、アプリケーションは強制終了されてしまいます。

例外の再スロー

例外をcatchし、ログ出力などの処理を行った後に再スローすることができます。
ただし、再スロー方法次第では、スタックトレースのオリジナルの例外発生箇所の情報が失われます。
以下の方法でそれを回避することができます。

  • 何も指定せずにスローする
catch (HogeException ex)
{
     throw;
}
catch (HogeException ex)
{
     throw new HogeException(ex);
}
  • ExceptionDispatchInfoを使用する
catch (HogeException ex)
{
     ExceptionDispatchInfo.Capture(ex).Throw();
}

catchした例外インスタンスを指定してスローした場合、スタックトレースが失われます。
再スローされた例外は再スロー箇所を例外発生箇所としてスタックトレースに記録します。

catch (HogeException ex)
{
     throw ex;
}

デリゲート

概要

デリゲートはメソッドなどの処理自体を持つことができる型です。
デリゲート型のインスタンスに処理を登録し、そのインスタンスを経由することでどこからでも処理を呼び出すことができます。
Action・Func・EventHandlerなどのデリゲート型が用意されており、そちらを使用することが多いです。

デリゲートの定義

delegateキーワードを指定し、メソッドの要領で定義します。

delegate void HogeDelegate();

引数・戻り値を持つ場合、以下のようにします。

delegate string HogeDelegate(string param);

デリゲートの初期化

newする際に、デリゲートと同一シグネチャのメソッド名または、ラムダ式を指定します。
また、「+」「-」「+=」「-=」演算子で結合・解除 が可能です。

// メソッド指定で初期化
var hogeDelegate1 = new HogeDelegate(HogeMethod);

// ラムダ式指定で初期化
var hogeDelegate2 = new HogeDelegate(() => {});

// 結合
var hogeDelegate3 = hogeDelegate1 + hogeDelegate2;

// 結合解除
var hogeDelegate4 = hogeDelegate3 - hogeDelegate2;

// 結合
hogeDelegate4 += hogeDelegate2;

// 結合解除
hogeDelegate4 -= hogeDelegate2;

デリゲートの実行

例として以下のようなデリゲートを用意します。

delegate void HogeDelegate(string param);

上記デリゲートを以下のように初期化します。

var hogeDelegate = new HogeDelegate(
    param =>
    {
        Thread.Sleep(1000);
        Console.WriteLine(param);
    });
同期実行

Invoke()で登録された処理が同期実行されます。 実行すると、1000ミリ秒待機の後、コンソール出力されます。

hogeDelegate.Invoke("Delegate Execute");

// これでもOK
hogeDelegate("Delegate Execute");
非同期実行

BeginInvoke()で登録された処理が非同期実行されます。 1000ミリ秒待機せずに、後続処理に制御が移ります。 非同期で1000ミリ秒待機後、コンソール出力されます。

hogeDelegate.BeginInvoke("Delegate Execute", new AsyncCallback(ar => Console.WriteLine(ar.AsyncState), "Async Executed");

BeginInvoke()の戻り値を使用することで、実行を同期的に待機することができます。 以下の場合、後続処理がある場合でも1000ミリ秒待機します。

var result = hogeDelegate.BeginInvoke("Delegate Execute", new AsyncCallback(ar => Console.WriteLine(ar.AsyncState), "Async Executed");
hogeDelegate.EndInvoke(result);
動的実行

デリゲートの型がわからない場合、DynamicInvoke()で動的実行することができます。 パラメーターはobject型の可変長引数となります。

hogeDelegate.DynamicInvoke("Delegate Execute");

指定したパラメーター型がデリゲートのパラメーター型と一致しない場合、MemberAccessExceptionをスローします。

// intを渡すと例外
hogeDelegate.DynamicInvoke(0);

デリゲートに複数の処理が登録されている場合、登録された順に順次処理されます。
戻り値がある場合、最後に登録された処理の戻り値が返されます。

例外処理

デリゲートに登録された処理が例外をスローする場合、呼び出し元でcatchできます。
結合されている場合、例外発生時点で処理は打ち切られます。

var del1 = new HogeDelegate(() => throw new InvalidOperationException("Exception 1"));
// 上記が処理されて打ち切られるので、これは処理されない
del1 += () => throw new InvalidOperationException("Exception 2");
try
{
    del1.Invoke();
}
catch (InvalidOperationException e)
{
    Console.WriteLine(e.Message);
}

イベント

概要

任意インスタンスからのイベント発行を、他インスタンスで購読し、それをトリガーに処理を動かすことができます。

イベントの定義

eventキーワードを用いて定義します。

public event EventHandler HogeEvent;

eventキーワードとともに指定する型はデリゲート型である必要があります。
Actionなども指定可能ですが、イベントに特化したデリゲート型であるEventHandler/EventHandlerを主に使用します。

eventキーワードを付与することで、以下の成約が生まれます。

  • デリゲート実行は定義しているクラス内からしかできない
// コンパイルエラー
hoge.HogeEvent.Invoke();
  • 外部クラスからはデリゲートへの処理の登録・解除しかできない
// OK
hoge.HogeEvent += HandleHoge;
hoge.HogeEvent -= HandleHoge;

// コンパイルエラー
hoge.HogeEvent = null;

イベントの登録・実行

上記の通り、イベントのデリゲートに対して「+=」「-=」で処理の登録・解除ができます。
定義クラス内でイベントを実行することで、登録された処理が実行されます。

public sealed class Publisher
{
    public event EventHandler TestEvent;

    public void RaiseTestEvent()
    {
        TestEvent?.Invoke(this, EventArgs.Empty);
    }
}

public static void Main()
{
    var publisher = new Publisher();
    publisher.TestEvent += (sender, e) => Console.WriteLine("TestEvent Published");

    publisher.RaiseTestEvent();
}

補足

イベントの仕組みは、外部にデリゲート型のフィールド・プロパティを公開するだけで実現はできます。
ただし、外部からクリアされる可能性や、イベントと関係なくデリゲートが実行される可能性があります。