連載
» 2018年01月10日 05時00分 公開

.NET TIPS:仮想/抽象/インタフェースを使い分けるには?[C#/VB]

複数のクラスでシグネチャが共通のメソッドなどを作るには仮想メンバ/抽象クラス/インタフェースを利用できる。それらの違いと使い分けのポイントを示す。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載「.NET TIPS」

 継承したクラスに(実装は異なるかもしれないが)シグネチャが同じメソッド/プロパティがあることを保証するには、仮想メンバ/抽象クラス/インタフェースという3通りの方法がある。本稿ではその違いを整理して解説する。

 単純にクラスを継承した場合、継承元(親クラス)にあるメソッド/プロパティは、継承先(子クラス)にも引き継がれる。ただし、そのメソッド/プロパティの実装は、親クラスにある1つだけだ。全ての子クラスで、その動作は同じである。

 そうではなく、子クラスによって実装を変えたい場合がある。例えば「BaseClass」型を継承して2つのクラス「ChildClass1」/「ChildClass2」を作ったとき、どちらにも同じシグネチャのメソッド「Method1」が存在してほしいのだが、「ChildClass1」クラスの「Method1」メソッドと「ChildClass2」クラスの「Method1」メソッドとではその動作を変えたい(異なる実装にしたい)といったようなときだ。本稿で説明する3つの方法は、このような問題を解決するためのものだ。

POINT 仮想/抽象/インタフェースの使い分け

仮想/抽象/インタフェースの使い分けまとめ 仮想/抽象/インタフェースの使い分けまとめ


 特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。

 なお、本稿ではメソッドとプロパティに限定して解説しているが、3つの方法はいずれも、イベントとインデクサにも適用できる。

 また、利用できる.NET Frameworkのバージョンは最初からであるが、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2015以降が必要である。サンプルコードはコンソールアプリの一部であり、コードの冒頭に以下の宣言が必要となる。

using static System.Console;

Imports System.Console

本稿のサンプルコードに必要な宣言(上:C#、下:VB)

仮想メンバ:既定の実装を与える

 親クラスのメソッド/プロパティを仮想メンバにすると、子クラスではそのメソッド/プロパティをオーバーライドできる(次のコード)。オーバーライドしなければ、親クラスのメソッド/プロパティが呼び出される。親クラスのメソッド/プロパティが既定の実装であり、子クラスで必要に応じてオーバーライドして挙動を変えるというイメージだ。

 仮想メンバによる方法は、次のようなケースで利用する。

  • たいていは親クラスの既定の実装で事足りる
  • 時々、子クラスで実装を変更したい場合がある

// 継承元
public class VirtualClass
{
  // 仮想メンバ:virtual修飾子を付ける
  public virtual void MethodA() => WriteLine("VirtualClass.MethodA()");
  internal virtual string PropertyA { get; set; } = "VirtualClass.PropertyA";

  // staticは不可
  //public static virtual void MethodA1() => WriteLine("VirtualClass.MethodA1()");
  // privateは不可
  //private virtual void MethodA2() => WriteLine("VirtualClass.MethodA2()");
}

// 継承先
public class ChildClass1 : VirtualClass
{
  // 仮想メンバのオーバーライド:任意、override修飾子が必要
  public override void MethodA() => WriteLine("ChildClass1.MethodA()");
  internal override string PropertyA { get; set; } = "ChildClass1.PropertyA";
}

' 継承元
Public Class VirtualClass
  ' 仮想メンバ:Overridable修飾子を付ける
  Public Overridable Sub MethodA()
    WriteLine("VirtualClass.MethodA()")
  End Sub
  Friend Overridable Property PropertyA As String = "VirtualClass.PropertyA"

  ' Sharedは不可
  'Public Overridable Shared Sub MethodA1()
  '  WriteLine("VirtualClass.MethodA1()")
  'End Sub

  ' Privateは不可
  'Private Overridable Sub MethodA2()
  '  WriteLine("VirtualClass.MethodA2()")
  'End Sub
End Class

' 継承先
Public Class ChildClass1
  Inherits VirtualClass

  ' 仮想メンバのオーバーライド:任意、Overrides修飾子が必要
  Public Overrides Sub MethodA()
    WriteLine("ChildClass1.MethodA()")
  End Sub
  Friend Overrides Property PropertyA As String = "ChildClass1.PropertyA"
End Class

仮想メンバの例(上:C#、下:VB)
仮想メンバにするには、C#ではvirtual修飾子を、VBではOverridable修飾子を付ける。
静的メンバは仮想メンバにできない。
継承先のクラスでは、仮想メンバをオーバーライドできる。オーバーライドするには、C#ではoverride修飾子を、VBではOverrides修飾子を付ける。
仮想メンバにはprivateアクセス修飾子を付けられない(public/internal(C#)/Friend(VB)/protectedを指定できる)。また、継承先でオーバーライドするメンバのアクセス修飾子は継承元と同じでなければならない。

抽象クラス:既定の実装を与えない

 親クラスを抽象クラスにすると、抽象メンバを定義できる(次のコード)。抽象メンバには実装がなく、継承先での実装が強制される。また、抽象クラスはインスタンス化できない。インスタンス化できるようにするには、継承先のクラスで抽象メンバをオーバーライドして実装し、普通のクラスにする。

 抽象クラスによる方法は、次のようなケースで利用する(2番目/3番目は、後述するインタフェースとの相違点)。

  • 既定の実装を与えたくない(子クラスごとに実装させたい)
  • 親クラスに(抽象メンバの他に)実装を持つメソッド/プロパティも持たせたい
  • publicではないメソッド/プロパティにも適用したい

// 継承元:抽象メンバを置くには、abstract修飾子を付けて抽象クラスにする
public abstract class AbstractClass
{
  // 抽象メンバ:abstract修飾子を付ける
  public abstract void MethodB();
  internal abstract string PropertyB { get; set; }

  // 抽象クラスは実装も持てる
  public int Add(int m, int n) => m + n;

  //public static abstract void MethodB1(); // staticは不可
  //private abstract void MethodB2(); // privateは不可
}

// 継承先
public class ChildClass2 : AbstractClass
{
  // 抽象メンバのオーバーライド:必須、override修飾子が必要
  public override void MethodB() => WriteLine("ChildClass2.MethodB()");
  internal override string PropertyB { get; set; } = "ChildClass2.PropertyB";
}

' 継承元:抽象メンバを置くには、MustInherit修飾子を付けて抽象クラスにする
Public MustInherit Class AbstractClass
  ' 抽象メンバ:MustOverride修飾子を付ける
  Public MustOverride Sub MethodB()
  Friend MustOverride Property PropertyB As String

  ' 抽象クラスは実装も持てる
  Function Add(m As Integer, n As Integer) As Integer
    Return m + n
  End Function

  'Public MustOverride Shared Sub MethodB1() ' staticは不可
  'Private MustOverride Sub MethodB2() ' Privateは不可
End Class

' 継承先
Public Class ChildClass2
  Inherits AbstractClass

  ' 抽象メンバのオーバーライド:必須、Overrides修飾子が必要
  Public Overrides Sub MethodB()
    WriteLine("ChildClass2.MethodB()")
  End Sub
  Friend Overrides Property PropertyB As String = "ChildClass2.PropertyB"
End Class

抽象クラスの例(上:C#、下:VB)
抽象メンバは、抽象クラスだけに置ける。抽象クラスにするには、C#ではabstract修飾子を、VBではMustInherit修飾子を付ける。抽象クラスはインスタンス化できない。
抽象メンバにするには、C#ではabstract修飾子を、VBではMustOverride修飾子を付ける。
静的メンバは抽象メンバにできない。
継承先のクラスでは、全ての抽象メンバをオーバーライドすることで、インスタンス化できるようになる(オーバーライドしなかった抽象メンバが残っている場合は、継承先のそのクラスも抽象クラスにしなければならない)。オーバーライドするには、C#ではoverride修飾子を、VBではOverrides修飾子を付ける。
抽象メンバにはprivateアクセス修飾子を付けられない(public/internal(C#)/Friend(VB)/protectedを指定できる)。また、継承先でオーバーライドするメンバのアクセス修飾子は継承元と同じでなければならない。

インタフェース:既定の実装を与えない(汎用的)

 継承元をクラスではなくインタフェースにすると、汎用的な「インタフェース(外部との接続部分)」を定義できる。インタフェースには実装を書けず、継承先での実装が強制される。また、インタフェースはインスタンス化できない。インスタンス化できるようにするには、継承先のクラスでメンバを実装し、普通のクラスにする。

 インタフェースによる方法は、次のようなケースで利用する(2番目は前述の抽象クラスとの相違点、3番目は仮想メンバ/抽象クラスとの相違点)。

  • 既定の実装を与えたくない(子クラスごとに実装させたい)
  • 汎用的な外部との接続部分を定義し、さまざまなクラスに適用したい
  • 多重継承したい

 抽象クラスとの相違として、インタフェースのメソッド/プロパティ(および継承したクラスのメソッド/プロパティ)はpublicアクセスのみである。「インタフェース(外部との接続部分)」を作るという目的から、publicアクセスだけになっているのだ。また、クラスは多重継承できないが、インタフェースは多重継承できる。クラスの継承元として、インタフェースはいくつでも同時に複数の指定ができるのである。

 ちなみに、C#の将来バージョンではインタフェースにも既定のメソッドが実装できるようになる見込みだ(本稿執筆時点ではC# 8.0で計画されている)。

// 継承元
public interface IFoo
{
  void MethodC();
  string PropertyC { get; }

  //static void MethodC1(); // staticは不可
  //public void MethodC2(); // アクセス修飾子は不可(全てpublicになる)

  // 実装は持てない
  //int Add(int m, int n) => m + n; // コンパイルエラー
}

// 継承先
public class ChildClass3 : IFoo
{
  // インタフェースの実装:必須、override修飾子は付けない(付けられない)
  public void MethodC() => WriteLine("ChildClass3.MethodC()");
  public string PropertyC { get; } = "ChildClass3.PropertyC";
}

' 継承元
Public Interface IFoo
  Sub MethodC()
  ReadOnly Property PropertyC As String

  'Shared Sub MethodC1() ' Sharedは不可
  'Public Sub MethodC2() ' アクセス修飾子は不可(全てPublicになる)

  ' 実装は持てない
  'Function Add(m As Integer, n As Integer) As Integer ' コンパイルエラー
  '  Return m + n
  'End Function
End Interface

' 継承先
Public Class ChildClass3
  Implements IFoo

  ' インタフェースの実装:必須、Overrides修飾子は付けない(付けられない)
  Public Sub MethodC() Implements IFoo.MethodC
    WriteLine("ChildClass3.MethodC()")
  End Sub
  Public ReadOnly Property PropertyC As String _
    = "ChildClass3.PropertyC" Implements IFoo.PropertyC
End Class

インタフェースの例(上:C#、下:VB)
インタフェースにはメソッド/プロパティの定義だけを書く。アクセス修飾子は付けられない(全てpublicになる)。静的メンバにもできない。実装は全く持てない。
インタフェースを継承するクラスを定義するとき、VBでは(Inheritsキーワードではなく)Implementsキーワードを使う。
継承先のクラスでは、インタフェースのメンバを実装する。クラスメンバのオーバーライドとは異なり、override修飾子(C#)/Overrides修飾子(VB)を付けない。ただしVBでは、実装するメンバごとにImplementsキーワードが必要だ。

まとめ

 既定の実装を提供するには、仮想メンバを使う。

 既定の実装を提供しない場合で、汎用的な「インタフェース(外部との接続部分)」であるならばインタフェースを用いる。そうではなくて、特定のクラスの継承先だけに限定される性質のものならば、抽象クラスを使う。

利用可能バージョン:.NET Framework 1.1以降
カテゴリ:C# 処理対象:言語構文
カテゴリ:Visual Basic 処理対象:言語構文
関連TIPS:構文:メソッドやプロパティをラムダ式で簡潔に実装するには?[C# 6.0/7.0]
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?


「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。