C#でクラスの中にクラスを書く方法とは?入れ子クラスの使い方・メリット・注意点を初心者向けに解説

はじめに

C#では、クラスの定義の中に別のクラスを書くことができます。このようなクラスは「入れ子クラス」や「ネストしたクラス」と呼ばれます。

入れ子クラスは、特定のクラスだけで使用する補助的な処理をまとめたい場合や、外部に公開する必要のない実装を隠したい場合に便利です。

基本的な書き方は、次のとおりです。

C#
public class OuterClass
{
private class InnerClass
{
}
}

この例では、OuterClassが外側のクラス、InnerClassがクラスの中に定義された入れ子クラスです。

本記事では、C#でクラスの中にクラスを書く方法を、アクセス修飾子やインスタンスの生成方法、外側のクラスとの関係も含めて初心者向けに解説します。

1. C#でクラスの中にクラスを書く「入れ子クラス」とは

1-1. 入れ子クラスの基本的な意味

C#における入れ子クラスとは、別のクラスの内部に定義されたクラスのことです。

C#
public class Company
{
public class Employee
{
}
}

このコードでは、EmployeeクラスがCompanyクラスの中に定義されています。

外側にあるCompanyは「外側のクラス」や「包含型」、内側にあるEmployeeは「入れ子クラス」や「入れ子型」と呼ばれます。

入れ子クラスを使用すると、次のような関係をコード上で表現できます。

Company
└── Employee

ただし、クラスの中に書かれているからといって、必ずしも継承関係や親子関係が成立するわけではありません。

EmployeeCompanyを継承しているわけではなく、単にCompanyの内部に型として定義されているだけです。

1-2. 通常のクラスとの違い

通常のクラスは、名前空間の直下などに定義します。

C#
public class Company
{
}

public class Employee
{
}

一方、入れ子クラスは、別のクラスの内部に定義します。

C#
public class Company
{
public class Employee
{
}
}

大きな違いは、クラス名を指定するときの書き方です。

通常のクラスは、そのままクラス名を指定できます。

C#
Employee employee = new Employee();

入れ子クラスを外部から利用する場合は、外側のクラス名も含めて指定します。

C#
Company.Employee employee = new Company.Employee();

また、入れ子クラスではprivateprotectedなどのアクセス修飾子を指定できます。これにより、特定のクラス内部だけで使う型を作成できます。

通常の名前空間直下にあるクラスにはprivateprotectedを指定できませんが、入れ子クラスには指定可能です。

1-3. 内部クラス・ネストしたクラス・入れ子型の呼び方

クラスの中に定義されたクラスには、いくつかの呼び方があります。

代表的な呼び方は、次のとおりです。

  • 入れ子クラス

  • ネストしたクラス

  • ネストクラス

  • 内部クラス

  • 入れ子型

C#の仕様やMicrosoftのドキュメントでは、クラスだけでなく構造体やインターフェイスなども含めて「入れ子にされた型」「nested type」と表現されることがあります。

「内部クラス」という言葉も一般的に使われますが、Javaなど他の言語では、外側のインスタンスを自動的に参照する特別な意味を持つ場合があります。

C#の入れ子クラスは、外側のクラスのインスタンスを自動的に保持しません。この点は、他の言語からC#を学ぶ場合に注意が必要です。

1-4. クラス以外の構造体やインターフェイスも入れ子にできる

C#では、クラスだけでなく、構造体やインターフェイス、列挙型なども別の型の内部に定義できます。

C#
public class Application
{
public class Service
{
}

public struct Position
{
public int X { get; set; }

public int Y { get; set; }
}

public interface ILogger
{
void Log(string message);
}

public enum Status
{
Stopped,
Running
}
}

外部から利用するときは、外側のクラス名を付けます。

C#
Application.Position position = new Application.Position
{
X = 10,
Y = 20
};

Application.Status status = Application.Status.Running;

クラス以外も含めた一般的な名称が「入れ子型」です。

2. C#でクラスの中にクラスを書く基本構文

2-1. 入れ子クラスを定義する最小コード例

入れ子クラスは、外側のクラスの波かっこの中に通常のクラスと同じ形式で定義します。

C#
public class OuterClass
{
public class InnerClass
{
}
}

外側のクラスがOuterClass、内側のクラスがInnerClassです。

入れ子クラスには、フィールド、プロパティ、コンストラクター、メソッドなどを定義できます。

C#
public class OuterClass
{
public class InnerClass
{
public string Name { get; set; }

public InnerClass(string name)
{
Name = name;
}

public void ShowName()
{
Console.WriteLine(Name);
}
}
}

入れ子クラスであっても、通常のクラスと同じように機能を実装できます。

2-2. 外側のクラスから入れ子クラスを利用する方法

外側のクラスの内部では、入れ子クラスをクラス名だけで指定できます。

C#
public class OuterClass
{
private class InnerClass
{
public void Execute()
{
Console.WriteLine("処理を実行しました");
}
}

public void Run()
{
InnerClass inner = new InnerClass();
inner.Execute();
}
}

利用例は、次のとおりです。

C#
OuterClass outer = new OuterClass();
outer.Run();

実行結果は次のようになります。

処理を実行しました

InnerClassprivateですが、外側のOuterClass内部では利用できます。

このように、外部に公開する必要のない補助クラスをprivateな入れ子クラスとして定義できます。

2-3. 外部から入れ子クラスのインスタンスを生成する方法

入れ子クラスを外部から利用するには、入れ子クラスにpublicなどのアクセス可能な修飾子を付けます。

C#
public class OuterClass
{
public class InnerClass
{
}
}

インスタンスを生成するときは、外側のクラス名.入れ子クラス名と記述します。

C#
OuterClass.InnerClass inner = new OuterClass.InnerClass();

usingディレクティブで名前空間を省略できても、通常は外側のクラス名まで自動的に省略されるわけではありません。

たとえば、次のクラスがSampleApp名前空間にあるとします。

C#
namespace SampleApp
{
public class OuterClass
{
public class InnerClass
{
}
}
}

完全修飾名は、次のようになります。

C#
SampleApp.OuterClass.InnerClass inner =
new SampleApp.OuterClass.InnerClass();

ファイルの先頭にusing SampleApp;があれば、名前空間部分を省略できます。

C#
using SampleApp;

OuterClass.InnerClass inner = new OuterClass.InnerClass();

2-4. 入れ子クラスのメソッドやプロパティを呼び出す方法

入れ子クラスのメソッドやプロパティも、通常のクラスと同じ方法で利用できます。

C#
public class User
{
public class Profile
{
public string DisplayName { get; set; } = "";

public void Show()
{
Console.WriteLine($"表示名: {DisplayName}");
}
}
}

外部からインスタンスを生成し、プロパティを設定してメソッドを呼び出します。

C#
User.Profile profile = new User.Profile();

profile.DisplayName = "山田太郎";
profile.Show();

実行結果は次のとおりです。

表示名: 山田太郎

コンストラクターを定義して初期値を渡すこともできます。

C#
public class User
{
public class Profile
{
public string DisplayName { get; }

public Profile(string displayName)
{
DisplayName = displayName;
}

public void Show()
{
Console.WriteLine($"表示名: {DisplayName}");
}
}
}

利用方法は次のとおりです。

C#
User.Profile profile = new User.Profile("山田太郎");
profile.Show();

2-5. 入れ子クラスを複数定義する方法

1つのクラスの中には、複数の入れ子クラスを定義できます。

C#
public class ShoppingSystem
{
public class Customer
{
public string Name { get; set; } = "";
}

public class Product
{
public string Name { get; set; } = "";

public decimal Price { get; set; }
}

public class Order
{
public int OrderNumber { get; set; }
}
}

それぞれ外部から利用できます。

C#
ShoppingSystem.Customer customer =
new ShoppingSystem.Customer
{
Name = "田中"
};

ShoppingSystem.Product product =
new ShoppingSystem.Product
{
Name = "キーボード",
Price = 5000m
};

ShoppingSystem.Order order =
new ShoppingSystem.Order
{
OrderNumber = 1001
};

入れ子クラスを複数使用すると、関連する型を1つの外側のクラスにまとめられます。

ただし、すべての関連クラスを安易に入れ子にすると、コードが深くなり、再利用しにくくなることがあります。本当に外側のクラスに強く依存する型だけを入れ子にすることが重要です。

3. 入れ子クラスのアクセス修飾子と公開範囲

3-1. アクセス修飾子を省略した場合の扱い

入れ子クラスのアクセス修飾子を省略すると、初期状態ではprivateとして扱われます。

C#
public class OuterClass
{
class InnerClass
{
}
}

このコードは、次のコードとほぼ同じ意味です。

C#
public class OuterClass
{
private class InnerClass
{
}
}

そのため、次のように外部から利用することはできません。

C#
OuterClass.InnerClass inner = new OuterClass.InnerClass();

コンパイル時に、保護レベルが原因でアクセスできないことを示すエラーが発生します。

外部から利用させたい場合は、publicなどのアクセス修飾子を明示的に指定しましょう。

3-2. privateで外側のクラス内部だけに限定する方法

入れ子クラスにprivateを指定すると、基本的に外側のクラスの実装内部だけで利用する型にできます。

C#
public class ReportService
{
private class ReportFormatter
{
public string Format(string text)
{
return $"【レポート】{text}";
}
}

public string CreateReport(string text)
{
ReportFormatter formatter = new ReportFormatter();
return formatter.Format(text);
}
}

外部から利用するのは、公開されているCreateReportメソッドだけです。

C#
ReportService service = new ReportService();

string report = service.CreateReport("売上は100万円です");

Console.WriteLine(report);

実行結果は次のようになります。

【レポート】売上は100万円です

ReportFormatterReportServiceの内部処理でしか必要ないため、privateにすることで余計な型を外部に公開せずに済みます。

3-3. publicで外部からアクセスできるようにする方法

入れ子クラスを外部から使用させたい場合は、publicを指定します。

C#
public class ApiResponse
{
public class Error
{
public int Code { get; set; }

public string Message { get; set; } = "";
}
}

外部からは、次のように利用できます。

C#
ApiResponse.Error error = new ApiResponse.Error
{
Code = 404,
Message = "データが見つかりません"
};

ただし、入れ子クラスをpublicにしても、外側のクラスが外部からアクセスできなければ入れ子クラスにもアクセスできません。

C#
internal class ApiResponse
{
public class Error
{
}
}

この場合、Errorにはpublicが付いていますが、外側のApiResponseinternalであるため、別のアセンブリからは利用できません。

入れ子クラスの実際の公開範囲は、外側のクラスの公開範囲にも制限されます。

3-4. protected・internalなどを指定する場合

入れ子クラスには、次のようなアクセス修飾子を指定できます。

C#
public class OuterClass
{
public class PublicClass
{
}

private class PrivateClass
{
}

protected class ProtectedClass
{
}

internal class InternalClass
{
}

protected internal class ProtectedInternalClass
{
}

private protected class PrivateProtectedClass
{
}
}

それぞれの基本的な意味は、次のとおりです。

アクセス修飾子主なアクセス範囲
publicアクセス可能な場所であればどこからでも利用できる
private外側のクラスの内部に限定される
protected外側のクラスと、その派生クラスから利用できる
internal同じアセンブリ内から利用できる
protected internal同じアセンブリ内、または派生クラスから利用できる
private protected同じアセンブリ内にある派生クラスから利用できる

protectedな入れ子クラスは、外側のクラスを継承したクラスから利用できます。

C#
public class BaseService
{
protected class Helper
{
public void Execute()
{
Console.WriteLine("補助処理");
}
}
}

public class CustomService : BaseService
{
public void Run()
{
Helper helper = new Helper();
helper.Execute();
}
}

internalは、プロジェクトやライブラリの構成によって意味を持ちます。厳密には「同じプロジェクト」ではなく「同じアセンブリ」がアクセス範囲です。

3-5. 外側のクラスと入れ子クラスのアクセス関係

入れ子クラスからは、外側のクラスに定義されたprivateメンバーへアクセスできます。

C#
public class OuterClass
{
private static string message = "外側のprivateフィールドです";

private class InnerClass
{
public void ShowMessage()
{
Console.WriteLine(message);
}
}

public void Run()
{
InnerClass inner = new InnerClass();
inner.ShowMessage();
}
}

入れ子クラスは、外側のクラスの実装の一部として扱われるため、外側のクラスのprivateメンバーも参照できます。

ただし、外側のクラスのインスタンスメンバーを使用するには、外側のクラスのインスタンスが必要です。

C#
public class OuterClass
{
private string message = "インスタンスごとのメッセージ";

private class InnerClass
{
public void ShowMessage(OuterClass outer)
{
Console.WriteLine(outer.message);
}
}
}

入れ子クラスだからといって、外側のインスタンスが自動的に用意されるわけではありません。

また、外側のクラスから入れ子クラスを使えることと、入れ子クラスのすべての非公開メンバーへ無条件にアクセスできることは同じではありません。

外側と内側のクラスを強く結び付けすぎると、役割の分離が難しくなるため、必要な範囲だけを公開する設計が大切です。

3-6. 「アクセスできない」と表示される原因と対処法

入れ子クラスを利用したときに「アクセスできない」というエラーが発生する主な原因は、アクセス修飾子です。

次のコードでは、InnerClassのアクセス修飾子が省略されています。

C#
public class OuterClass
{
class InnerClass
{
}
}

省略時はprivateになるため、外部から次のようには利用できません。

C#
OuterClass.InnerClass inner = new OuterClass.InnerClass();

外部から利用する必要がある場合は、publicに変更します。

C#
public class OuterClass
{
public class InnerClass
{
}
}

また、入れ子クラスがpublicでも、コンストラクターがprivateであれば外部からインスタンスを生成できません。

C#
public class OuterClass
{
public class InnerClass
{
private InnerClass()
{
}
}
}

外部でインスタンスを生成するなら、コンストラクターもアクセス可能にします。

C#
public class OuterClass
{
public class InnerClass
{
public InnerClass()
{
}
}
}

ほかにも、次の点を確認しましょう。

  • 外側のクラスがアクセス可能か

  • 入れ子クラスに適切なアクセス修飾子が付いているか

  • コンストラクターがアクセス可能か

  • 外側のクラス名を付けて型を指定しているか

  • 名前空間が正しいか

  • 利用側のプロジェクトからアセンブリを参照しているか

たとえば、次の書き方では入れ子クラスを通常のクラスとして参照しているため、型が見つからない可能性があります。

C#
InnerClass inner = new InnerClass();

外部からは、外側のクラス名を付けます。

C#
OuterClass.InnerClass inner = new OuterClass.InnerClass();

4. 入れ子クラスから外側のクラスのメンバーを使う方法

4-1. 外側のクラスのstaticメンバーへアクセスする

外側のクラスのstaticメンバーは、外側のインスタンスを作成しなくても入れ子クラスからアクセスできます。

C#
public class OuterClass
{
private static string applicationName = "SampleApp";

public class InnerClass
{
public void ShowApplicationName()
{
Console.WriteLine(applicationName);
}
}
}

利用例は次のとおりです。

C#
OuterClass.InnerClass inner = new OuterClass.InnerClass();
inner.ShowApplicationName();

実行結果は次のようになります。

SampleApp

外側のクラス名を明示してアクセスすることもできます。

C#
public class OuterClass
{
private static string applicationName = "SampleApp";

public class InnerClass
{
public void ShowApplicationName()
{
Console.WriteLine(OuterClass.applicationName);
}
}
}

クラス名を付けると、どの型のstaticメンバーを利用しているのかが分かりやすくなります。

4-2. 外側のクラスのインスタンスメンバーへアクセスする

外側のクラスのインスタンスメンバーには、外側のクラスのインスタンスを通じてアクセスします。

C#
public class OuterClass
{
private string message = "外側のインスタンスメンバーです";

public class InnerClass
{
public void ShowMessage(OuterClass outer)
{
Console.WriteLine(outer.message);
}
}
}

利用例は次のとおりです。

C#
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = new OuterClass.InnerClass();

inner.ShowMessage(outer);

messagestaticではないため、次のように直接参照することはできません。

C#
public class OuterClass
{
private string message = "メッセージ";

public class InnerClass
{
public void ShowMessage()
{
// コンパイルエラー
// Console.WriteLine(message);
}
}
}

入れ子クラスには、どのOuterClassインスタンスを使えばよいか分からないためです。

4-3. 外側のクラスのインスタンスをコンストラクターで渡す

入れ子クラスで外側のインスタンスを継続して使用する場合は、コンストラクターで受け取ってフィールドに保存する方法が分かりやすいでしょう。

C#
public class Counter
{
private int value;

public Counter(int initialValue)
{
value = initialValue;
}

public class Viewer
{
private readonly Counter counter;

public Viewer(Counter counter)
{
this.counter = counter;
}

public void ShowValue()
{
Console.WriteLine(counter.value);
}
}
}

利用例は次のとおりです。

C#
Counter counter = new Counter(10);
Counter.Viewer viewer = new Counter.Viewer(counter);

viewer.ShowValue();

実行結果は次のようになります。

10

外側のクラス自身が入れ子クラスを生成する設計にもできます。

C#
public class Counter
{
private int value;

public Counter(int initialValue)
{
value = initialValue;
}

public Viewer CreateViewer()
{
return new Viewer(this);
}

public class Viewer
{
private readonly Counter counter;

private Viewer(Counter counter)
{
this.counter = counter;
}

public void ShowValue()
{
Console.WriteLine(counter.value);
}
}
}

利用側では、CreateViewerメソッドを通じてインスタンスを取得します。

C#
Counter counter = new Counter(10);
Counter.Viewer viewer = counter.CreateViewer();

viewer.ShowValue();

この方法なら、Viewerの生成方法を外側のクラス側で管理できます。

4-4. privateメンバーへアクセスするときの考え方

入れ子クラスは、外側のクラスのprivateメンバーへアクセスできます。

C#
public class BankAccount
{
private decimal balance;

public BankAccount(decimal initialBalance)
{
balance = initialBalance;
}

private class BalanceChecker
{
public bool HasEnoughBalance(
BankAccount account,
decimal amount)
{
return account.balance >= amount;
}
}

public bool CanWithdraw(decimal amount)
{
BalanceChecker checker = new BalanceChecker();
return checker.HasEnoughBalance(this, amount);
}
}

利用例は次のとおりです。

C#
BankAccount account = new BankAccount(10000m);

Console.WriteLine(account.CanWithdraw(3000m));
Console.WriteLine(account.CanWithdraw(15000m));

実行結果は次のようになります。

True
False

技術的にアクセスできるからといって、何でも入れ子クラスから操作してよいわけではありません。

外側のクラスの状態を入れ子クラスが自由に変更すると、どこで値が変化したのか分かりにくくなります。更新処理は外側のクラスのメソッドに集約し、入れ子クラスには必要最小限の責務だけを持たせる設計が安全です。

4-5. 入れ子クラスが外側のインスタンスを自動参照しない点に注意する

C#の入れ子クラスは、外側のクラスのインスタンスを自動的には参照しません。

次のコードでは、外側のクラスを作成せずに入れ子クラスだけを生成できます。

C#
public class OuterClass
{
public class InnerClass
{
}
}
C#
OuterClass.InnerClass inner = new OuterClass.InnerClass();

次のように外側のインスタンスを先に作る必要はありません。

C#
OuterClass outer = new OuterClass();

// C#では、このouterとinnerが自動的に関連付けられるわけではない
OuterClass.InnerClass inner = new OuterClass.InnerClass();

また、次のような書き方もできません。

C#
OuterClass outer = new OuterClass();

// C#では使用できない書き方
// OuterClass.InnerClass inner = outer.new InnerClass();

外側のインスタンスメンバーを使う必要がある場合は、メソッドの引数やコンストラクターを通じて明示的に渡します。

C#
public class OuterClass
{
private readonly string name;

public OuterClass(string name)
{
this.name = name;
}

public class InnerClass
{
private readonly OuterClass outer;

public InnerClass(OuterClass outer)
{
this.outer = outer;
}

public void ShowName()
{
Console.WriteLine(outer.name);
}
}
}
C#
OuterClass outer = new OuterClass("外側のオブジェクト");
OuterClass.InnerClass inner = new OuterClass.InnerClass(outer);

inner.ShowName();

この違いを理解していないと、「クラスの中にクラスを書いたのに、外側のフィールドへ直接アクセスできない」という疑問につながります。

入れ子になっているのは型の定義上の位置であり、各インスタンスが自動的に結び付くわけではないと覚えておきましょう。

5. C#で入れ子クラスを使うメリット

C#で入れ子クラスを使う主なメリットは、関連する型をまとめられることです。

たとえば、あるクラスの処理にしか使わない補助クラスを名前空間直下に定義すると、ほかの場所からも利用できる独立したクラスに見えてしまいます。

C#
public class FileParser
{
}

public class ParseResultBuilder
{
}

ParseResultBuilderFileParserの内部だけで使われるなら、入れ子クラスにすると関係が明確になります。

C#
public class FileParser
{
private class ParseResultBuilder
{
}
}

privateな入れ子クラスにすることで、外部に公開する型を減らせる点もメリットです。

ライブラリを作る場合、外部から使用してほしいクラスだけをpublicにし、実装専用の補助クラスを非公開にできます。

C#
public class CsvReader
{
private class CsvTokenizer
{
public string[] Split(string line)
{
return line.Split(',');
}
}

public string[] ReadLine(string line)
{
CsvTokenizer tokenizer = new CsvTokenizer();
return tokenizer.Split(line);
}
}

また、型名の衝突を避けやすい点も特徴です。

C#
public class SuccessResponse
{
public class Data
{
}
}

public class ErrorResponse
{
public class Data
{
}
}

両方にDataというクラスがありますが、完全な型名は異なります。

C#
SuccessResponse.Data successData = new SuccessResponse.Data();
ErrorResponse.Data errorData = new ErrorResponse.Data();

入れ子クラスは、次のような場面に向いています。

  • 外側のクラスだけで使用する補助クラス

  • 外部に公開したくない実装用クラス

  • 外側のクラスと強い関係を持つ型

  • ビルダーや列挙子など、外側のクラスに従属する型

  • 同じ役割名を外側のクラスごとに分けたい場合

一方で、入れ子クラスには注意点もあります。

入れ子が深くなると、型名が長くなります。

C#
Application.Module.Service.Result result =
new Application.Module.Service.Result();

このような多重の入れ子は、コードを読みづらくする原因になります。

また、入れ子クラスが外側のクラスのprivateメンバーを大量に操作すると、クラス間の依存関係が強くなります。入れ子クラスを別の場所で再利用することも難しくなります。

次のような場合は、独立したクラスにしたほうが適切です。

  • 複数のクラスから利用する

  • 単独で明確な役割を持っている

  • 単体テストを独立して行いたい

  • 将来的に再利用する可能性がある

  • 外側のクラスと直接関係しない処理が多い

  • 入れ子構造が深くなりすぎる

入れ子クラスを使うかどうかは、「この型は外側のクラスが存在しなくても独立した意味を持つか」という視点で考えると判断しやすくなります。

独立した意味を持つなら通常のクラス、外側のクラスの実装詳細としてのみ意味を持つなら入れ子クラスが適しています。

まとめ

C#では、クラスの波かっこの中に別のクラスを定義することで、クラスの中にクラスを書くことができます。

C#
public class OuterClass
{
public class InnerClass
{
}
}

外部から入れ子クラスを利用するときは、外側のクラス名を付けます。

C#
OuterClass.InnerClass inner = new OuterClass.InnerClass();

入れ子クラスのアクセス修飾子を省略すると、基本的にはprivateとして扱われます。外部から利用する必要がある場合は、publicなどを明示的に指定しましょう。

C#
public class OuterClass
{
public class InnerClass
{
}
}

入れ子クラスから外側のクラスのstaticメンバーには直接アクセスできますが、インスタンスメンバーへアクセスするには外側のインスタンスが必要です。

C#
public class OuterClass
{
private string message = "こんにちは";

public class InnerClass
{
private readonly OuterClass outer;

public InnerClass(OuterClass outer)
{
this.outer = outer;
}

public void ShowMessage()
{
Console.WriteLine(outer.message);
}
}
}

C#の入れ子クラスは、外側のクラスのインスタンスを自動的に参照しません。必要な場合は、コンストラクターやメソッドの引数で明示的に渡す必要があります。

入れ子クラスは、関連する型をまとめたり、実装専用の補助クラスを外部から隠したりする場合に便利です。ただし、入れ子を深くしすぎたり、外側のクラスに強く依存させすぎたりすると、可読性や再利用性が低下します。

外側のクラスの内部だけで意味を持つ型には入れ子クラスを使い、独立した役割を持つ型は通常のクラスとして定義することが、分かりやすいC#コードを書くためのポイントです。