C#を用いたアプリケーション開発において、クラスの設計はプログラムの保守性や可読性を左右する極めて重要な要素です。

その中で、あるクラスの内部に別のクラスを定義する「内部クラス(入れ子クラス、Nested Types)」という機能があります。

内部クラスを適切に活用することで、関連性の強い型を論理的にグループ化し、外部からの不要なアクセスを遮断する高度なカプセル化を実現できます。

本記事では、C#における内部クラスの基本的な書き方から、そのメリット、実務で役立つ具体的な活用シーン、そして使用上の注意点までをプロの視点で詳しく解説します。

内部クラス(入れ子クラス)とは

C#における内部クラスとは、あるクラス(外側のクラス:Outer Class)の宣言内に直接記述されたクラスのことを指します。

公式のドキュメントでは「入れ子になった型(Nested Types)」と表現されることもあります。

通常、C#のクラスは名前空間(namespace)の直下に定義されますが、内部クラスは特定のクラスの「メンバ」として扱われる点が最大の特徴です。

これにより、メソッドやプロパティと同様に、クラスのスコープに基づいたアクセス制御が可能になります。

内部クラスは主に、「そのクラス内でしか使用されない補助的なデータ構造」や、「外側のクラスと密接に関係し、外部に公開する必要のないロジック」を定義するために利用されます。

内部クラスの基本的な書き方と構文

内部クラスの定義は非常にシンプルです。

外側のクラスのブロック内に、通常のクラス定義を記述するだけです。

基本的な構文

以下のコードは、OuterClass というクラスの中に InnerClass という内部クラスを定義する例です。

C#
using System;

namespace NestedClassExample
{
    // 外側のクラス
    public class OuterClass
    {
        private string _outerMessage = "外側のクラスの非公開フィールドです。";

        // 内部クラス(入れ子クラス)
        public class InnerClass
        {
            public void DisplayMessage(OuterClass outer)
            {
                // 内部クラスから外側のクラスの private メンバにアクセス可能
                Console.WriteLine("InnerClassからアクセス: " + outer._outerMessage);
            }
        }

        public void CreateInner()
        {
            // 外側のクラス内からは直接インスタンス化できる
            InnerClass inner = new InnerClass();
            inner.DisplayMessage(this);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // 外部から内部クラスを利用する場合、「外側のクラス名.内部クラス名」で指定する
            OuterClass.InnerClass nestedInstance = new OuterClass.InnerClass();
            
            OuterClass outer = new OuterClass();
            nestedInstance.DisplayMessage(outer);
        }
    }
}
実行結果
InnerClassからアクセス: 外側のクラスの非公開フィールドです。

この例から分かる通り、外部から内部クラスを参照する際には OuterClass.InnerClass のように、ドット記法を用いて階層構造を明示する必要があります。

内部クラスのアクセス制御

内部クラスの大きな特徴の一つは、アクセス修飾子の自由度にあります。

内部クラスで使用可能なアクセス修飾子

通常の(名前空間直下の)クラスでは、指定できるアクセス修飾子は主に publicinternal のいずれかに限定されます。

しかし、内部クラスはクラスの「メンバ」であるため、以下のすべての修飾子を使用できます。

修飾子説明
publicどこからでもアクセス可能。
internal同じアセンブリ内からアクセス可能。
protected外側のクラス、またはその派生クラス内からアクセス可能。
private外側のクラス内からのみアクセス可能(デフォルト)。
private protected同じアセンブリ内の外側のクラスの派生クラスからアクセス可能。
protected internal同じアセンブリ内、または他アセンブリの派生クラスからアクセス可能。

特に private を指定できる点は重要です。

これにより、「特定のクラスの内部実装としてのみ機能し、外部にはその存在すら知られたくないクラス」を安全に定義できます。

外側のクラスとの相互アクセス

内部クラスと外側のクラスの間には、特殊なアクセス権限が存在します。

内部から外側へのアクセス

内部クラスは外側のクラスが持つ private メンバ(フィールド、メソッドなど) に直接アクセスできます。

ただし、外側のインスタンスメンバにアクセスするには外側のクラスのインスタンス参照が必要です(例: OuterClass.this を使う)。

外側から内部へのアクセス

外側のクラスは内部クラスのインスタンスを生成してそのメンバにアクセスできます。

内部クラスのコンストラクタが private であっても、外側のクラス内からであればインスタンス化可能です(例: 外側クラス内で new Inner() を呼び出す)。

内部クラスを活用するメリット

なぜ内部クラスを利用するのでしょうか。

その主なメリットは以下の4点に集約されます。

1. カプセル化の強化

内部クラスを使用する最大の理由は、カプセル化(情報の隠蔽)の徹底です。

あるクラス A を助けるためだけに存在するクラス B がある場合、B を独立したファイルとして定義すると、他のクラス C からも B が見えてしまいます。

B を A の内部クラスとして private 定義すれば、B の存在を A の中に完全に閉じ込めることができます。

2. 論理的なグループ化

関連性の高い型を一つにまとめることで、プロジェクトの構造が整理されます。

名前空間がクラスで溢れかえるのを防ぎ、「このクラスはこの文脈でしか使われない」という意図をエンジニアに対して明確に示すことができます。

3. 名前空間の汚染防止

例えば、複数のクラスでそれぞれ異なる「Node」という補助クラスが必要な場合、内部クラスを使えば LinkedList.NodeBinaryTree.Node のように定義でき、名前の衝突を避けることができます。

4. 外部に公開しない実装の分離

インターフェースの実装を隠蔽したい場合にも役立ちます。

外側のクラスがあるインターフェースを実装するのではなく、内部クラスにその役割を持たせ、外側からはそのインスタンスをインターフェース型として公開する手法があります。

実践的な活用シーン

内部クラスが具体的にどのような場面で役立つのか、いくつかのパターンを見ていきましょう。

シーン1:コレクション内部の列挙子(Enumerator)の実装

.NET の標準ライブラリでも多用されているパターンです。

例えば、独自のリスト型を作成する場合、その要素を走査するための IEnumerator を内部クラスとして実装します。

C#
using System;
using System.Collections;
using System.Collections.Generic;

public class MyCollection<T> : IEnumerable<T>
{
    private T[] _items = new T[100];
    private int _count = 0;

    public void Add(T item) => _items[_count++] = item;

    // 内部クラスとして列挙子を定義
    private class MyEnumerator : IEnumerator<T>
    {
        private readonly MyCollection<T> _collection;
        private int _currentIndex = -1;

        public MyEnumerator(MyCollection<T> collection) => _collection = collection;

        public T Current => _collection._items[_currentIndex];
        object IEnumerator.Current => Current;

        public bool MoveNext() => ++_currentIndex < _collection._count;
        public void Reset() => _currentIndex = -1;
        public void Dispose() { }
    }

    public IEnumerator<T> GetEnumerator() => new MyEnumerator(this);
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

この実装では、MyEnumeratorprivate なため、外部のユーザーは MyEnumerator という型を直接知る必要はなく、IEnumerator<T> インターフェースを通じてのみ操作します。

シーン2:Factory パターンによるインスタンス生成の制御

特定のクラスのインスタンス生成を制限し、必ず特定の Factory を通して生成させたい場合に、内部クラスと private コンストラクタを組み合わせます。

C#
public class Document
{
    private Document(string title) { Title = title; }
    public string Title { get; }

    // Factory クラスを内部に配置
    public static class Factory
    {
        public static Document CreateDraft(string title)
        {
            // 内部クラス(または外側のクラスの静的メソッド)なので
            // private なコンストラクタを呼べる
            return new Document(title + " (下書き)");
        }
    }
}

// 利用側
// Document doc = new Document("Test"); // コンパイルエラー
Document doc = Document.Factory.CreateDraft("月報"); // OK

シーン3:定数や設定値の整理

クラス内で使用する複数の定数を、意味のある単位でグループ化するために内部クラス(または構造体)を使うことがあります。

C#
public class DatabaseConnector
{
    // 定数をグループ化
    private static class Defaults
    {
        public const int Timeout = 30;
        public const string Port = "1433";
    }

    public void Connect()
    {
        Console.WriteLine($"Connecting with timeout: {Defaults.Timeout}");
    }
}

内部クラスを使用する際の注意点

内部クラスは強力な機能ですが、乱用するとコードの品質を低下させる恐れがあります。

1. コードの肥大化と可読性の低下

一つのファイル内に外側のクラスと複数の内部クラスを記述すると、ファイル全体の行数が膨大になり、見通しが悪くなります。

ロジックが複雑になる場合は、partial キーワードを使用してファイルを分割することを検討してください。

C#
// File: MyLargeClass.cs
public partial class MyLargeClass { }

// File: MyLargeClass.Inner.cs
public partial class MyLargeClass
{
    private class InnerHelper { }
}

2. 公開(public)内部クラスの是非

内部クラスを public にすることは可能ですが、設計上は慎重になるべきです。

外部から Outer.Inner と参照させることは、クラス間の結合度を高める原因になります。

基本的には、内部クラスは private または internal に留め、外部に公開する必要がある場合は、独立したトップレベルのクラスとして定義し直すのが一般的なベストプラクティスです。

3. シリアライズの制約

XMLシリアライズや一部のライブラリでは、内部クラスのシリアライズに制限がある場合があります。

例えば、XmlSerializer はデフォルトでは public な内部クラスを扱えますが、構成によっては問題が発生することがあります。

シリアライズ対象のデータモデルを設計する場合は、内部クラスを避けたほうが無難なケースが多いです。

4. 継承の複雑さ

内部クラスを継承する場合、そのスコープやアクセス権のルールが複雑になりがちです。

特に「内部クラスを継承したクラスを外部で定義する」といったトリッキーな構造は、後のメンテナンスを困難にします。

内部クラスと static

内部クラスそのものに static 修飾子を付与するかどうかで、振る舞いが変わることはありません(C#において、内部クラスは常に外側のクラスのインスタンスとは独立してインスタンス化できます。

これはJavaの非static内部クラスとは異なる挙動です)。

ただし、内部クラスの中に static メンバを持つことは可能です。

前述の Factory パターンの例のように、補助的なユーティリティをまとめる際には有効です。

内部クラスの高度な利用:ジェネリッククラス

ジェネリッククラスの中に内部クラスを定義する場合、内部クラスは外側のクラスの型パラメータを自動的に引き継ぎます。

C#
public class Container<T>
{
    private T _data;

    public class Inspector
    {
        public void Inspect(Container<T> container)
        {
            // 外側の型パラメータ T をそのまま使用できる
            Console.WriteLine($"Type: {typeof(T)}, Value: {container._data}");
        }
    }
}

このように記述することで、型安全性を保ったまま補助的なクラスを実装できます。

まとめ

C#の内部クラス(入れ子クラス)は、単なる機能の一つというだけでなく、オブジェクト指向における「カプセル化」を極限まで高めるための設計ツールです。

本記事のポイントを振り返ります。

  • 隠蔽性: private クラスとして定義することで、特定のクラス専用のヘルパーを完全に隠すことができる。
  • アクセス権: 外側のクラスのプライベートメンバにアクセスできるため、密接な連携が可能。
  • 整理: 関連する型を論理的にグループ化し、名前空間の汚染を防ぐ。
  • 注意点: 過度な使用は可読性を損なうため、partial 分割の利用や、公開範囲の適切な設定が必要。

内部クラスを使いこなすことで、複雑なロジックをシンプルに整理し、変更に強い堅牢なコードを記述できるようになります。

まずは、特定のクラス内でしか使わない小さなデータ構造を private class として定義することから始めてみてはいかがでしょうか。