内部クラスの使いどころとは?[C#/VB].NET TIPS

内部クラスを利用して、あるクラスに関連するコードをそのクラスに取り込んで隠蔽したり、クラスのコードが肥大化するのを避けたりする方法を解説する。

» 2018年05月09日 05時00分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載「.NET TIPS」

 C#/Visual Basic(以降、VB)には、「入れ子にされた型」(Nested Types)がある。長い名前なので、C#のinternalアクセス修飾子と紛らわしくない場面では「内部クラス」などと呼ぶことが多い。入れ子にされた型としては、クラスの他に、インタフェース/構造体/列挙型なども使える。この入れ子にされた型は、どんなときに使えばよいのだろうか? 本稿では内部クラスを取り上げて、その使いどころを紹介する。

POINT 内部クラスの書き方

内部クラスの書き方まとめ 内部クラスの書き方まとめ


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

 なお、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2015以降が必要である。

内部クラスの使いどころ

 もちろん内部クラスを使えるところならどのように利用してもよいのだが、使いどころとしては「隠蔽(いんぺい)したいとき」である。つまり、「別のクラスを内部に取り込んで隠蔽する(⇒IComparableインタフェースを実装する例)」あるいは、「クラスの一部を切り出して別のクラスにまとめたいのだが、内部に隠蔽したままにしておきたい(⇒内部定数を分類する例画面の制御をまとめる例)」、といった具合だ。

 別のクラスを内部に取り込みたいという例を示しておこう(次のコード、C#のみ)。このPersonクラスはIComparable<T>インタフェースの実装例である。インタフェースの実装としては、ソート時の並び順を名前の比較で行うようにした。ところが、オプションとして、年齢順でもソートしたいものとする。そのためにAgeComparerクラスを別に定義した。既定の並べ替えはPersonクラスに実装されているのに、オプションの並べ替えは別のクラスに実装されているのだ。これはちょっと奇妙な構造ではなかろうか。どちらの実装もPersonクラスに実装されているべきではないだろうか。これを解決するには、AgeComparerクラスをPersonクラスに取り込んで内部クラスにしてしまえばよいのである(IComparableインタフェースを実装する例を参照)。

using System;
using System.Collections.Generic;

// 内部クラスを使わないIComparable<T>インタフェースの実装
public class Person : IComparable<Person>
{
  // プロパティ
  public string Name { get; }
  public DateTime Birthday { get; }

  // コンストラクタ
  public Person(string name, DateTime birthday)
  {
    Name = name; Birthday = birthday;
  }

  // 既定の比較は名前で行う
  public int CompareTo(Person other)
    => string.Compare(this.Name, other?.Name);
}

// 年齢でソートするためのIComparer<T>インタフェースの実装
class AgeComparer : IComparer<Person>
{
  public int Compare(Person p1, Person p2)
    => DateTime.Compare(p2.Birthday, p1.Birthday);
  // p1/p2の一方または両方がnullの場合の処理は省略した
}

別のクラスを内部に取り込みたい例(C#)
Personクラスのインスタンスを並べ替えるための実装が、PersonクラスとAgeComparerクラスに分かれてしまっている。AgeComparerクラスをPersonクラスの内部クラスにすれば、並べ替えロジックは全てPersonクラスにまとめられる。
なお、ここではC#のコードだけを示したが、「解決編」となるIComparableインタフェースを実装する例にはVBのコードも載せてある。このPersonクラスを実際に並べ替える方法も「解決編」をご覧いただきたい。

内部クラスの書き方

 クラス/構造体/VBのモジュールの中に、クラスを記述できる。これが内部クラスである(次のコード)。通常のクラスと違ってprivate/protectedアクセス修飾子も指定できる(継承できないシールクラスの中ではprotectedは不可)。また、アクセス修飾子を省略するとprivateになる(通常のクラスではinternal/Friend)。

 マイクロソフトの公式な呼称は「入れ子にされたクラス」(Nested Class)となるが、「内部クラス」や「インナークラス」などと呼ぶことが多い。

// 包含クラス(外側のクラス/外部クラス)
class Container // 省略時のアクセシビリティーはinternal
{
  // 入れ子にされたクラス(内部クラス)
  class Nested // 省略時のアクセシビリティーはprivate
  {
    public Nested() { /* コンストラクタ */ }
  }
}

' 包含クラス(外側のクラス/外部クラス)
Class Container ' 省略時のアクセシビリティーはFriend

  ' 入れ子にされたクラス(内部クラス)
  Class Nested ' 省略時のアクセシビリティーはPrivate
    Public Sub New()
      ' コンストラクタ
    End Sub
  End Class
End Class

内部クラスの例(上:C#、下:VB)
この例では、Nested内部クラスのアクセシビリティーはprivateなので、Containerクラス内部からだけアクセスできる。Nestedクラスのコンストラクタはpublicにしてあるので、Containerクラス内部ではNestedクラスをインスタンス化できる。

 なお、ローカル関数はそのメソッド内から外側のメソッドのローカル変数などにアクセスできるが、それとは違って、内部クラスから外側のクラスのインスタンスメンバに対するアクセスは自動的にはできない。必要ならば、外側のクラスのインスタンスを内部クラスのコンストラクタの引数として渡すようにする(後述の「画面の制御をまとめる例」を参照)。

IComparableインタフェースを実装する例

 内部クラスの利用例としてよく挙げられるものである。内部クラスの使いどころで紹介したC#のコードのように、IComparable<T>インタフェースを実装したクラスには、IComparer<T>インタフェースを実装した別のクラスが付随することが多い。前述したように後者を前者の中に隠蔽しておきたい場合、内部クラスを使って次のコードのようにする。

using System;
using System.Collections.Generic;

// 内部クラスを使ったIComparable<T>インタフェースの実装
public class Person : IComparable<Person>
{
  // プロパティ
  public string Name { get; }
  public DateTime Birthday { get; }

  // コンストラクタ
  public Person(string name, DateTime birthday)
  {
    Name = name; Birthday = birthday;
  }

  // IComparable<T>インタフェースの実装
  // 既定の比較は名前で行う
  public int CompareTo(Person other)
    => string.Compare(this.Name, other?.Name);

  // 年齢でソートするためのIComparer<T>インタフェースのインスタンス
  public static IComparer<Person> AgeComparer { get; }
    = new AgeComparerInner();

  // 入れ子クラス:年齢でソートするためのIComparer<T>インタフェースの実装
  class AgeComparerInner : IComparer<Person>
  {
    public int Compare(Person p1, Person p2)
      => DateTime.Compare(p2.Birthday, p1.Birthday);
    // p1/p2の一方または両方がnullの場合の処理は省略した
  }
}

' 内部クラスを使ったIComparable(Of T)インタフェースの実装
Public Class Person
  Implements IComparable(Of Person)

  ' プロパティ
  Public ReadOnly Property Name As String
  Public ReadOnly Property Birthday As DateTime

  ' コンストラクタ
  Public Sub New(name As String, birthday As DateTime)
    Me.Name = name
    Me.Birthday = birthday
  End Sub

  ' IComparable(Of T)インタフェースの実装
  ' 既定の比較は名前で行う
  Public Function CompareTo(other As Person) As Integer _
      Implements IComparable(Of Person).CompareTo
    Return String.Compare(Me.Name, other?.Name)
  End Function

  ' 年齢でソートするためのIComparer(Of T)インタフェースのインスタンス
  Public Shared ReadOnly Property AgeComparer As IComparer(Of Person) _
    = New AgeComparerInner()

  ' 入れ子クラス:年齢でソートするためのIComparer(Of T)インタフェースの実装
  Class AgeComparerInner
    Implements IComparer(Of Person)

    Public Function Compare(p1 As Person, p2 As Person) As Integer _
        Implements IComparer(Of Person).Compare
      ' p1/p2の一方または両方がnullの場合の処理は省略した
      Return DateTime.Compare(p2.Birthday, p1.Birthday)
    End Function
  End Class
End Class

IComparableインタフェースを実装する例(上:C#、下:VB)
既定の比較(名前順)はIComparable<Person>インタフェースの実装であるCompareToメソッドにあり、オプションの比較(年齢順)はAgeComparerInner内部クラスにあり、どちらもPersonクラスの中に実装されている。AgeComparerInner内部クラスは、publicにしてそのまま公開することも可能だが、ここではそのインスタンスだけをIComparer<T>インタフェースとして公開している。オプションの比較(年齢順)の実装そのもの(=AgeComparerInner内部クラス)は隠蔽できているので、例えば、年齢順の比較ロジックを共通ライブラリに移したとしても、このPersonクラスを使っている箇所には影響が及ばない。

 ちなみに、上のPersonクラスは次のコードのようにしてソートする(コンソールアプリ)。

using System;
using System.Collections.Generic;
using System.Linq;
using static System.Console;

……省略……

List<Person> persons = new List<Person>
{
  new Person("しょうこ", new DateTime(2001,1,1)),
  new Person("あつし", new DateTime(1989,1,8)),
  new Person("こうた", new DateTime(2000,12,31)),
};

WriteLine("既定の並べ替え(名前順):");
persons.Sort();
WriteLine(string.Join(", ",
  persons.Select(p => $"{p.Name}({p.Birthday:yyyy/MM/dd})")));
// 出力:あつし(1989/01/08), こうた(2000/12/31), しょうこ(2001/01/01)

WriteLine("年齢で並べ替え:");
persons.Sort(Person.AgeComparer);
WriteLine(string.Join(", ",
  persons.Select(p => $"{p.Name}({p.Birthday:yyyy/MM/dd})")));
// 出力:しょうこ(2001/01/01), こうた(2000/12/31), あつし(1989/01/08)

Imports System.Console

……省略……

Dim persons As List(Of Person) = New List(Of Person) From
{
  New Person("しょうこ", New DateTime(2001, 1, 1)),
  New Person("あつし", New DateTime(1989, 1, 8)),
  New Person("こうた", New DateTime(2000, 12, 31))
}

WriteLine("既定の並べ替え(名前順):")
persons.Sort()
WriteLine(String.Join(", ",
  persons.Select(Function(p) $"{p.Name}({p.Birthday:yyyy/MM/dd})")))
' 出力:あつし(1989/01/08), こうた(2000/12/31), しょうこ(2001/01/01)

WriteLine("年齢で並べ替え:")
persons.Sort(Person.AgeComparer)
WriteLine(String.Join(", ",
  persons.Select(Function(p) $"{p.Name}({p.Birthday:yyyy/MM/dd})")))
' 出力:しょうこ(2001/01/01), こうた(2000/12/31), あつし(1989/01/08)

Personクラスの使用例(上:C#、下:VB)
例えばList<Person>コレクションを作った場合、そのSortメソッドを引数なしで使えば既定の並べ替えになる(名前順)。Sortメソッドの引数にPerson.AgeComparerを指定すれば、年齢順の並べ替えになる。PersonクラスをソートするときにPersonクラスの静的プロパティを指定するというのは自然ではなかろうか。

内部定数を分類する例

 クラスの一部を切り出して別のクラスにまとめたいが内部に隠蔽したままにしておきたい例として、クラス内だけで使っている定数を考えてみよう。

 クラス内で使う定数が数個とかなら何も問題はないだろうが、もしも数十個も定数定義があったらとしたらどうだろう? 定数を分類して、分類ごとにまとめておきたくなるだろう。しかし、そのクラスの中だけでしか使わない定数なので、隠蔽したままにしておきたい。そんなときに内部クラスが利用できる(次のコード)。

using System;

public class SampleClass1
{
  // 「量」に関する定数(内部クラス)
  static class AmountConst
  {
    public const int Min = 10;
    public const int Max = 100;
  }
  // 「回数」に関する定数(内部クラス)
  static class CountConst
  {
    public const int Min = 1;
    public const int Max = 10;
  }

  public static void SampleMethod(int amount, int count)
  {
    if (amount < AmountConst.Min || AmountConst.Max < amount)
      throw new ArgumentOutOfRangeException();
    if (count < CountConst.Min || CountConst.Max < count)
      throw new ArgumentOutOfRangeException();

    // ……処理本体(省略)……
  }
}

Public Class SampleClass1
  ' 「量」に関する定数(内部クラス)
  Class AmountConst
    Public Const Min As Integer = 10
    Public Const Max As Integer = 100
  End Class

  ' 「回数」に関する定数(内部クラス)
  Class CountConst
    Public Const Min As Integer = 1
    Public Const Max As Integer = 10
  End Class

  Public Shared Sub SampleMethod(amount As Integer, count As Integer)
    If (amount < AmountConst.Min OrElse AmountConst.Max < amount) Then
      Throw New ArgumentOutOfRangeException()
    End If
    If (count < CountConst.Min OrElse CountConst.Max < count) Then
      Throw New ArgumentOutOfRangeException()
    End If

    ' ……処理本体(省略)……
  End Sub
End Class

内部クラスを使って定数を分類する例(上:C#、下:VB)
サンプルということで定数は4つしか書いていないが、これが数十個にもなる場合を想像してもらいたい。そうなると何らかの分類ルールを導入しない限り、混乱してしまうだろう。内部クラスは、定数を分類するための一案になる。
なお、VBでは内部クラスを静的にできない(クラスにShared修飾子を付けられない)。

画面の制御をまとめる例

 ついつい大きくなってしまうクラスに、画面がある。WindowsフォームにせよWPFにせよ、画面のクラスは肥大しがちだ。そこでリファクタリングして、別のクラスに切り出していくことになる。他の画面でも使えるものは、独立した普通のクラスにすればよい。しかし、画面のUIにがっちりと結び付いたものは、隠蔽したままにしておきたいだろう。内部クラスの出番である。

 この例はWindowsフォームで、フォームの縦横比に応じてレイアウトを変えるものとする(次の画像)。赤色のパネル部分(変数名「Panel1」)を、フォームが横長のときは右端に、縦長なら下端に移動させるのだ。

フォームの縦横比に応じてレイアウトを変える例 フォームの縦横比に応じてレイアウトを変える例
赤色のパネル部分には、例えば操作系のUIが並んでいると思っていただきたい。

 そのコードは、フォームそのもの(「Form1」クラス)のイベントハンドラなどにどんどん書いていっても実現できる。だが、そうしてしまうと、「Form1」クラスの見通しが悪くなってしまうだろう。そこで、内部クラス「LayoutController」を作って、レイアウト調整のコードはそこに分離する(次のコード)。

 この内部クラスでは、包含クラス(外側のクラス)のインスタンスメンバ(つまり、「Form1」クラスに配置したUIコントロール)にアクセスしたい。そのためには、内部クラスのコンストラクタの引数として「Form1」クラスのインスタンスを渡すようにする。

using System;
using System.Windows.Forms;

……省略……

partial class Form1
{
  // 画面のレイアウト調整は、この入れ子クラスに行わせる
  class LayoutController
  {
    // 包含クラスのインスタンスを保持するメンバ変数
    private Form1 _thisForm;

    // 包含クラスのメンバにアクセスするには、コンストラクタでインスタンスを渡す
    public LayoutController(Form1 thisForm)
    {
      _thisForm = thisForm;

      InitializeLayout();
      _thisForm.SizeChanged += Form_SizeChanged;
    }

    private void InitializeLayout()
    {
      _thisForm.Panel1.Height = 100;
      _thisForm.Panel1.Width = 100;
    }

    private void Form_SizeChanged(object sender, EventArgs e)
    {
      if (_thisForm.Height > _thisForm.Width)
        _thisForm.Panel1.Dock = DockStyle.Bottom;
      else
        _thisForm.Panel1.Dock = DockStyle.Right;
    }
  }
}

Partial Class Form1
  ' 画面のレイアウト調整は、この入れ子クラスに行わせる
  Class LayoutController

    ' 包含クラスのインスタンスを保持するメンバ変数
    Private _thisForm As Form1

    ' 包含クラスのメンバにアクセスするには、コンストラクタでインスタンスを渡す
    Public Sub New(thisForm As Form1)
      _thisForm = thisForm

      InitializeLayout()
      AddHandler _thisForm.SizeChanged, AddressOf Form_SizeChanged
    End Sub

    Private Sub InitializeLayout()
      _thisForm.Panel1.Height = 100
      _thisForm.Panel1.Width = 100
    End Sub

    Private Sub Form_SizeChanged(sender As Object, e As EventArgs)
      If (_thisForm.Height > _thisForm.Width) Then
        _thisForm.Panel1.Dock = DockStyle.Bottom
      Else
        _thisForm.Panel1.Dock = DockStyle.Right
      End If
    End Sub
  End Class
End Class

画面レイアウトを調整する内部クラスの例(上:C#、下:VB)
実際には、パネル内部に配置したUIコントロールの位置やサイズも調整することになるので、もっと長くて複雑なクラスになるだろう。
なお、部分クラスを利用して、このLayoutController内部クラスは、Form1.cs/.vbファイルとは別のソースファイルとしている。部分クラスと併用することで、内部クラスとして論理的に分離するだけでなく、ソースファイルも分離できる(個々のソースファイルは小さくなる)というメリットも得られるのだ。

 外側のクラス(=「Form1」クラス)では、フォームの初期化後に上記の内部クラスのインスタンスを作ってメンバ変数に保持しておく(次のコード)。これでレイアウト調整は内部クラスに任せて、「Form1」クラスはUIコントロールのイベント処理などに集中できる。

using System.Windows.Forms;

……省略……

public partial class Form1 : Form
{
  // 入れ子クラスのインスタンスを保持するメンバ変数
  private LayoutController _layoutController;

  public Form1()
  {
    InitializeComponent();

    // 入れ子クラスのインスタンスを作り、メンバ変数に保持
    _layoutController = new LayoutController(this);
  }
}

Public Class Form1
  ' 入れ子クラスのインスタンスを保持するメンバ変数
  Private _layoutController As LayoutController

  Public Sub New()
    InitializeComponent()

    ' 入れ子クラスのインスタンスを作り、メンバ変数に保持
    _layoutController = New LayoutController(Me)
  End Sub
End Class

画面レイアウトを調整する内部クラスを使う例(上:C#、下:VB)

まとめ

 クラスにまとめたいのだが、あるいは、クラスにしなければならないのだが、しかし他からは隠蔽しておきたいときに内部クラスが利用できる。本稿ではその使いどころとして、IComparableインタフェースの実装/内部定数の分類/画面の制御をまとめるという3つの例を紹介した。

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

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

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

メールマガジン登録

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