C# では、クラスの内部に別のクラスを定義できます。これを 入れ子クラス(Nested Class) と呼びます。外側のクラスの private メンバーにアクセスできる、入れ子クラス自体を private にして外部から完全に隠蔽できる、といった特徴を持つこの機能は、カプセル化を強化するための実践的な設計テクニック です。
本記事では入れ子クラスの言語仕様から、歴史的な背景、そして「どんな場面で使うべきか」を具体的なコード例とともに掘り下げます。
入れ子クラスの基本
構文
入れ子クラスは、外側のクラス(enclosing class)の本体の中で定義します。
public class OuterClass
{
public class PublicInnerClass { }
private class PrivateInnerClass { }
}
PublicInnerClass は外部から OuterClass.PublicInnerClass としてアクセスできます。一方、PrivateInnerClass は OuterClass の内部からのみアクセスでき、外部のコードからは型名すら見えません。
入れ子にできる型
クラスだけでなく、以下の型も入れ子にできます。
class(通常のクラス、abstract、sealedを含む)structinterfaceenumdelegaterecord/record struct(C# 9.0 以降 / C# 10 以降)
public class Container
{
public enum Status { Active, Inactive }
private struct InternalData { public int Id; }
public interface IHandler { void Handle(); }
private delegate void Callback(string message);
}
C# 初版(1.0)からサポートされている
入れ子型は C# 1.0(2002 年) から言語仕様に含まれています。C# 1.0 の ECMA-334 仕様書の §10.3(Class Members)において、クラスのメンバーとして型宣言が含まれることが明記されています。
これは C# 固有の発明ではなく、基盤となる CLI(Common Language Infrastructure)の型システム が入れ子型をサポートしているためです。CLI の ECMA-335 仕様では、型定義テーブル(TypeDef)において EnclosingClass トークンで入れ子関係が表現されます。C# はこの基盤を言語レベルで素直に公開しているに過ぎません。
Java にも内部クラス(Inner Class)がありますが、C# の入れ子クラスは Java のそれとは異なり、外側のクラスのインスタンスへの暗黙的な参照を持ちません。C# の入れ子クラスは、Java でいう static な内部クラス(Static Nested Class)に近い振る舞いをします。
アクセス修飾子の振る舞い
入れ子クラスのアクセス修飾子を理解することは、この機能を正しく使うための前提条件です。
入れ子クラスに付けられるアクセス修飾子
トップレベルのクラスに付けられるアクセス修飾子は public と internal(デフォルト)の 2 つだけですが、入れ子クラスは クラスのメンバー として扱われるため、メンバーと同じ 6 種類のアクセス修飾子を使えます。
| アクセス修飾子 | 入れ子クラスの可視性 |
|---|---|
public |
外側のクラスにアクセスできるすべてのコードから参照可能 |
private(デフォルト) |
外側のクラスの内部からのみ参照可能 |
protected |
外側のクラスとその派生クラスから参照可能 |
internal |
同じアセンブリ内から参照可能 |
protected internal |
同じアセンブリ内、または外側のクラスの派生クラスから参照可能 |
private protected |
同じアセンブリ内の外側のクラスの派生クラスから参照可能 |
注意: アクセス修飾子を省略した場合、入れ子クラスのデフォルトは private です。トップレベルのクラスが internal をデフォルトとするのとは異なります。
入れ子クラスから外側クラスの private メンバーにアクセスできる
これは入れ子クラスの 最も重要な特性 です。
public class BankAccount
{
private decimal _balance;
private readonly List<Transaction> _transactions = [];
public IReadOnlyList<Transaction> GetHistory() =>
_transactions.AsReadOnly();
// 外部からは Transaction のコンストラクタにアクセスできない
public void Deposit(decimal amount)
{
_balance += amount;
_transactions.Add(new Transaction(amount, TransactionType.Deposit));
}
// 入れ子クラスは外側の private メンバーにアクセス可能
public class Transaction
{
public decimal Amount { get; }
public TransactionType Type { get; }
public DateTime Timestamp { get; }
// internal コンストラクタで外部アセンブリからの生成を制限
internal Transaction(decimal amount, TransactionType type)
{
Amount = amount;
Type = type;
Timestamp = DateTime.UtcNow;
}
}
public enum TransactionType { Deposit, Withdrawal }
}
逆に、外側のクラスから入れ子クラスの private メンバーにはアクセスできません。入れ子クラスの private はあくまでその入れ子クラス自身のスコープに閉じています。
public class Outer
{
private int _outerField = 42;
public void Test()
{
var inner = new Inner();
// コンパイルエラー: 'Inner._innerField' is inaccessible due to its protection level
// Console.WriteLine(inner._innerField);
}
private class Inner
{
private int _innerField = 10;
public void AccessOuter(Outer outer)
{
// OK: 入れ子クラスから外側クラスの private メンバーにアクセスできる
Console.WriteLine(outer._outerField);
}
}
}
入れ子クラスのインスタンスと外側クラスの関係
C# の入れ子クラスは、Java の非 static 内部クラスとは異なり、外側のクラスのインスタンスへの暗黙的な参照を保持しません。入れ子クラスのインスタンスが外側のクラスのインスタンスメンバーにアクセスするには、明示的に参照を渡す必要があります。
public class Outer
{
private int _value = 100;
private class Inner
{
// 外側のインスタンスを明示的に受け取る
private readonly Outer _outer;
public Inner(Outer outer) => _outer = outer;
public int GetOuterValue() => _outer._value;
}
public int CreateInnerAndGetValue()
{
var inner = new Inner(this);
return inner.GetOuterValue();
}
}
この設計には利点があります。暗黙的な参照を持たないため、入れ子クラスのインスタンスのライフタイムが外側のインスタンスのライフタイムと独立 しています。Java の内部クラスでよく問題になる、外側のインスタンスが GC に回収されないメモリリークが、C# では構造的に発生しません。
実装の肥大化と partial class による分割
入れ子クラスの最大の懸念は、外側のクラスのソースファイルが肥大化する ことです。入れ子クラスの定義がすべて同じファイルに書かれると、可読性が著しく低下します。
この問題は partial class で解決できます。外側のクラスを partial にし、入れ子クラスの定義を別ファイルに分離します。
// OrderProcessor.cs — 外側クラスの主要ロジック
public partial class OrderProcessor
{
private readonly IOrderRepository _repository;
public OrderProcessor(IOrderRepository repository)
{
_repository = repository;
}
public async Task<OrderResult> ProcessAsync(Order order)
{
var validator = new OrderValidator();
if (!validator.Validate(order))
return OrderResult.Invalid;
// 処理ロジック...
return OrderResult.Success;
}
}
// OrderProcessor.OrderValidator.cs — 入れ子クラスを別ファイルに分離
public partial class OrderProcessor
{
// private なのでこのクラスの存在を外部に知らせない
private class OrderValidator
{
public bool Validate(Order order)
{
if (order.Items.Count == 0) return false;
if (order.TotalAmount <= 0) return false;
return true;
}
}
}
ファイル名の命名規則として OuterClass.InnerClass.cs とすることで、Solution Explorer 上で関連性が一目でわかります。.NET のランタイムライブラリでもこの命名パターンが広く使われています。
使いどころ — private 入れ子クラスが輝く場面
1. 実装詳細の隠蔽(Builder パターン)
外側のクラスだけが知っていればよいヘルパーオブジェクトを private 入れ子クラスとして定義すると、名前空間を汚さずに済みます。
public class HtmlDocument
{
private readonly List<Node> _nodes = [];
public HtmlDocument Add(string tag, string content)
{
_nodes.Add(new ElementNode(tag, content));
return this;
}
public HtmlDocument AddText(string text)
{
_nodes.Add(new TextNode(text));
return this;
}
public string Render() =>
string.Concat(_nodes.Select(n => n.Render()));
// --- 以下、外部に公開する必要のない実装詳細 ---
private abstract class Node
{
public abstract string Render();
}
private class ElementNode : Node
{
private readonly string _tag;
private readonly string _content;
public ElementNode(string tag, string content)
{
_tag = tag;
_content = content;
}
public override string Render() => $"<{_tag}>{_content}</{_tag}>";
}
private class TextNode : Node
{
private readonly string _text;
public TextNode(string text) => _text = text;
public override string Render() => _text;
}
}
Node、ElementNode、TextNode は HtmlDocument の利用者にとってまったく関係のない型です。private 入れ子クラスにすることで、名前空間の汚染を防ぎ、利用者が気にすべき API を最小限に保てます。
2. インターフェースの具象型を隠す(ファクトリパターン)
公開するのはインターフェースだけにし、具象クラスを private にして外部からの直接依存を防ぎます。
public class NotificationService
{
public INotification CreateEmail(string to, string subject, string body) =>
new EmailNotification(to, subject, body);
public INotification CreateSms(string phoneNumber, string message) =>
new SmsNotification(phoneNumber, message);
// 外部からは INotification としてのみ扱える
public interface INotification
{
Task SendAsync();
}
// 具象型は private — 外部コードが直接参照・継承できない
private class EmailNotification : INotification
{
private readonly string _to;
private readonly string _subject;
private readonly string _body;
public EmailNotification(string to, string subject, string body)
{
_to = to;
_subject = subject;
_body = body;
}
public async Task SendAsync()
{
// SMTP 送信ロジック
}
}
private class SmsNotification : INotification
{
private readonly string _phoneNumber;
private readonly string _message;
public SmsNotification(string phoneNumber, string message)
{
_phoneNumber = phoneNumber;
_message = message;
}
public async Task SendAsync()
{
// SMS 送信ロジック
}
}
}
この設計なら、将来 PushNotification を追加しても外部の利用者に影響を与えません。具象型が隠蔽されているため、利用者は INotification インターフェースにのみ依存します。
3. 状態を持つイテレーター / 非同期列挙の実装
IEnumerable<T> や IAsyncEnumerable<T> を実装する際に、状態管理用のクラスを入れ子にするのは定番のパターンです。.NET のランタイムライブラリでも多用されています。
public class SlidingWindow<T>
{
private readonly IReadOnlyList<T> _source;
private readonly int _windowSize;
public SlidingWindow(IReadOnlyList<T> source, int windowSize)
{
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(windowSize, 0);
_source = source;
_windowSize = windowSize;
}
public IEnumerable<IReadOnlyList<T>> GetWindows()
{
for (int i = 0; i <= _source.Count - _windowSize; i++)
{
yield return new WindowSegment(_source, i, _windowSize);
}
}
// ウィンドウの各セグメントを遅延評価で返す — 外部に公開する必要なし
private class WindowSegment : IReadOnlyList<T>
{
private readonly IReadOnlyList<T> _source;
private readonly int _offset;
private readonly int _count;
public WindowSegment(IReadOnlyList<T> source, int offset, int count)
{
_source = source;
_offset = offset;
_count = count;
}
public T this[int index] => _source[_offset + index];
public int Count => _count;
public IEnumerator<T> GetEnumerator()
{
for (int i = 0; i < _count; i++)
yield return _source[_offset + i];
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() =>
GetEnumerator();
}
}
4. テスト用のスタブ / フェイクをテストクラス内に閉じ込める
テストコードでも入れ子クラスは有用です。テスト対象のクラスに注入するフェイク実装を、テストクラスの private 入れ子クラスとして定義すると、他のテストクラスから見えず、テストの関心事が局所化されます。
public class OrderServiceTests
{
[Fact]
public async Task ProcessOrder_ValidOrder_ReturnsSuccess()
{
var service = new OrderService(new FakeRepository());
var result = await service.ProcessAsync(CreateValidOrder());
Assert.Equal(OrderResult.Success, result);
}
// このテストクラスでしか使わないフェイク
private class FakeRepository : IOrderRepository
{
private readonly List<Order> _orders = [];
public Task SaveAsync(Order order)
{
_orders.Add(order);
return Task.CompletedTask;
}
public Task<Order?> FindAsync(int id) =>
Task.FromResult(_orders.FirstOrDefault(o => o.Id == id));
}
private static Order CreateValidOrder() =>
new() { Id = 1, Items = [new OrderItem("Widget", 1, 9.99m)] };
}
.NET ランタイムライブラリでの使用例
.NET のランタイムライブラリ自体が入れ子クラスを多用しています。いくつかの代表例を挙げます。
| 外側のクラス | 入れ子クラス | 役割 |
|---|---|---|
Dictionary<TKey, TValue> |
Entry(private struct) |
ハッシュテーブルのエントリを格納する内部構造体 |
List<T> |
Enumerator(public struct) |
foreach で使われるイテレーター |
Task |
ContingentProperties |
遅延初期化される付加情報の保持 |
ConcurrentDictionary<TKey, TValue> |
Node |
ハッシュバケットのリンクリストノード |
Dictionary<TKey, TValue>.Entry が private struct として定義されているのは、まさに「外部に公開する必要のない実装詳細を隠蔽する」典型例です。
入れ子クラスを使うべきでない場面
万能な機能ではないため、使うべきでない場面も明確にしておきます。
1. 入れ子クラスが独立した責務を持つ場合
入れ子クラスが外側のクラスとは独立した意味を持つなら、トップレベルのクラスにすべきです。入れ子にするのは 「外側のクラスの実装詳細である」 場合に限ります。
2. 入れ子の深さが 2 段以上になる場合
入れ子の入れ子は可読性を著しく損ないます。外側のクラスの責務が大きすぎる兆候なので、設計を見直すべきです。
// アンチパターン: 入れ子が深すぎる
public class A
{
private class B
{
private class C // ここまで来たら設計を見直す
{
}
}
}
3. 公開 API として広く使われる型
入れ子クラスを public にすると、利用者は常に OuterClass.InnerClass という修飾名で参照することになります。広く使われる型をこの形で公開するのは煩雑です。
まとめ
C# の入れ子クラスは 1.0 から存在する基本機能ですが、その真価は private 入れ子クラスによる実装の隠蔽 にあります。
- 外部に公開する必要のない型 を
private入れ子クラスにすることで、名前空間の汚染を防ぎ、利用者が気にすべき API を最小限に保てる - 入れ子クラスは 外側のクラスの
privateメンバーにアクセスできる ため、密結合な実装を自然に表現できる - ソースファイルの肥大化は
partial classで解決できる - .NET のランタイムライブラリでも
Dictionary<TKey, TValue>.Entryをはじめとして広く使われている実績のある設計手法
前回の記事で取り上げた sealed と組み合わせると、「private sealed class で定義した入れ子クラスは外部から参照も継承もできない」という最も堅牢なカプセル化が実現できます。実装詳細をクラスの中に閉じ込めるこのテクニックは、設計のツールボックスに入れておいて損はないでしょう。