C#のinternalとは?アクセス修飾子の使い方・public/privateとの違い・実務での注意点をわかりやすく解説
はじめに
C#でクラスやメソッドを定義していると、publicやprivateと並んでinternalというアクセス修飾子を見かけることがあります。
publicは外部から使える、privateはクラス内だけで使える、というイメージを持っている人は多いでしょう。一方で、internalは「どこからアクセスできるのか」が少しわかりにくいアクセス修飾子です。
結論からいうと、C#のinternalは同じアセンブリ内からだけアクセスできるようにするためのアクセス修飾子です。ライブラリやアプリケーションの内部実装を外部に公開せず、同じプロジェクト内では共有したい場合によく使われます。
この記事では、C#のinternalについて、publicやprivateとの違い、基本的な使い方、コード例、単体テストとの関係、実務での注意点までわかりやすく解説します。
1. C#のinternalとは?まず結論から理解する
1-1. internalは「同じアセンブリ内だけでアクセスできる」アクセス修飾子
C#のinternalとは、同じアセンブリ内でのみアクセスできるようにするアクセス修飾子です。
たとえば、あるプロジェクト内に次のようなクラスがあるとします。
C#internal class UserValidator
{
public bool IsValid(string name)
{
return !string.IsNullOrWhiteSpace(name);
}
}
このUserValidatorクラスはinternalなので、同じアセンブリ内のコードからは利用できます。しかし、別のアセンブリ、つまり別プロジェクトから参照しても直接使うことはできません。
C#var validator = new UserValidator(); // 同じアセンブリ内ならOK
internalは、外部に公開したくないけれど、プロジェクト内部では複数のクラスから使いたい処理に向いています。
1-2. アセンブリとは何か:プロジェクト・DLL・EXEとの関係
internalを理解するうえで重要なのが「アセンブリ」です。
アセンブリとは、C#のコードをビルドした結果として作られる単位です。一般的には、次のようなファイルがアセンブリにあたります。
MyApp.exe
MyLibrary.dll
Visual Studioや.NET CLIでC#プロジェクトをビルドすると、多くの場合、1つのプロジェクトから1つのアセンブリが生成されます。
たとえば、次のような構成を考えてみます。
Solution
├── MyApp → MyApp.exe
└── MyLibrary → MyLibrary.dll
この場合、MyAppとMyLibraryは別々のアセンブリです。
MyLibrary内でinternalとして定義されたクラスは、MyLibraryの中からは使えます。しかし、MyAppからMyLibraryを参照していても、internalなクラスには通常アクセスできません。
つまり、internalのアクセス範囲は「同じnamespace」や「同じフォルダ」ではなく、同じアセンブリかどうかで決まります。
1-3. internalを使うと何が制限されるのか
internalを使うと、外部アセンブリからのアクセスが制限されます。
たとえば、ライブラリ側に次のクラスがあるとします。
C#namespace MyLibrary;
internal class FilePathBuilder
{
public string Build(string fileName)
{
return $"data/{fileName}";
}
}
同じMyLibraryプロジェクト内であれば、次のように使えます。
C#var builder = new FilePathBuilder();
var path = builder.Build("users.json");
しかし、別プロジェクトであるMyAppからは、次のように使えません。
C#using MyLibrary;
var builder = new FilePathBuilder(); // コンパイルエラー
このように、internalは「そのアセンブリの内部用」として扱いたい型やメンバーを外部から隠すために使います。
1-4. internalが必要になる典型的な場面
internalがよく使われるのは、次のような場面です。
・ライブラリの内部実装を隠したい
・同じプロジェクト内の複数クラスからは使いたい
・外部に公開するAPIを最小限にしたい
・将来変更される可能性があるクラスを外部に使わせたくない
・テスト用に内部クラスを限定的に扱いたい
たとえば、NuGetパッケージを作る場合、利用者に使ってほしいクラスだけをpublicにし、それ以外の補助クラスはinternalにしておくと、公開APIを整理しやすくなります。
C#public class CsvReader
{
private readonly CsvLineParser _parser = new();
public IEnumerable<string[]> Read(string text)
{
foreach (var line in text.Split('\n'))
{
yield return _parser.Parse(line);
}
}
}
internal class CsvLineParser
{
public string[] Parse(string line)
{
return line.Split(',');
}
}
この例では、外部の利用者にはCsvReaderだけを使ってもらい、CsvLineParserはライブラリ内部の実装詳細として隠しています。
2. C#のアクセス修飾子の基本
2-1. アクセス修飾子とは何か
アクセス修飾子とは、クラス、メソッド、プロパティ、フィールドなどに対して、どこからアクセスできるかを制御するためのキーワードです。
C#では、アクセス修飾子を使うことで、コードの利用範囲を明確にできます。
C#public class User
{
private string _password;
public string Name { get; set; }
internal void ChangePassword(string password)
{
_password = password;
}
}
この例では、Userクラス自体はpublicなので外部から使えます。Nameプロパティもpublicなので外部からアクセスできます。一方、_passwordフィールドはprivateなのでUserクラスの中からしか触れません。ChangePasswordメソッドはinternalなので、同じアセンブリ内からのみ呼び出せます。
アクセス修飾子を適切に使うことで、次のようなメリットがあります。
・意図しない使われ方を防げる
・外部に公開する機能を明確にできる
・変更してよい内部実装と変更しにくい公開APIを分けられる
・カプセル化を実現できる
2-2. public・private・protected・internalの一覧
C#でよく使うアクセス修飾子には、次のようなものがあります。
| アクセス修飾子 | アクセスできる範囲 |
|---|---|
public | どこからでもアクセス可能 |
private | 同じ型の中からのみアクセス可能 |
protected | 同じ型、または派生クラスからアクセス可能 |
internal | 同じアセンブリ内からアクセス可能 |
protected internal | 同じアセンブリ内、または派生クラスからアクセス可能 |
private protected | 同じアセンブリ内にある派生クラスからアクセス可能 |
最初に押さえるべきなのは、public、private、protected、internalの4つです。
internalは、publicほど広く公開したくないけれど、privateのように1つのクラス内だけに閉じるには狭すぎる、という場合に使います。
2-3. 何も指定しない場合のデフォルトアクセス修飾子
C#では、アクセス修飾子を省略した場合、対象によってデフォルトのアクセス範囲が変わります。
トップレベルのクラスは、何も指定しないとinternalになります。
C#class UserService
{
}
これは、次のように書いた場合とほぼ同じ意味です。
C#internal class UserService
{
}
一方、クラスの中に定義したフィールドやメソッドは、何も指定しないとprivateになります。
C#public class User
{
string _name;
void Rename(string name)
{
_name = name;
}
}
上のコードは、次のように書いた場合と同じです。
C#public class User
{
private string _name;
private void Rename(string name)
{
_name = name;
}
}
つまり、C#では何も書かないと、基本的には外部に公開されにくい安全なアクセス範囲になります。
ただし、読みやすさを考えると、チーム開発ではpublic、private、internalなどを明示することが多いです。
2-4. クラス・メソッド・プロパティ・フィールドに指定できる範囲
internalは、クラスだけでなく、メソッド、プロパティ、フィールド、インターフェースなどにも指定できます。
C#internal class OrderService
{
internal void CreateOrder()
{
}
internal string Status { get; set; }
internal int Count;
private void Validate()
{
}
}
ただし、メンバーのアクセス範囲は、そのメンバーを含む型のアクセス範囲にも影響されます。
たとえば、internal classの中にpublicメソッドを書いた場合、そのメソッドは「どこからでも使える」という意味にはなりません。
C#internal class ReportGenerator
{
public void Generate()
{
}
}
この場合、Generateメソッドはpublicですが、そもそもReportGeneratorクラスがinternalなので、外部アセンブリからReportGeneratorを使うことはできません。
つまり、メンバーのアクセス範囲は、外側の型のアクセス範囲を超えることはできません。
3. internalとpublic/private/protectedの違い
3-1. internalとpublicの違い:外部公開するかどうか
internalとpublicの一番大きな違いは、外部アセンブリからアクセスできるかどうかです。
publicは、他のプロジェクトからもアクセスできます。
C#public class UserService
{
public void Create()
{
}
}
このクラスは、別プロジェクトから参照して使うことができます。
C#var service = new UserService();
service.Create();
一方、internalは同じアセンブリ内でしか使えません。
C#internal class UserRepository
{
public void Save()
{
}
}
このUserRepositoryは、同じプロジェクト内では使えますが、別プロジェクトからは直接使えません。
publicは「外部の利用者に使ってもらうもの」、internalは「アセンブリ内部だけで使うもの」と考えるとわかりやすいです。
3-2. internalとprivateの違い:クラス内限定かアセンブリ内限定か
privateは、同じクラス内からのみアクセスできます。
C#public class UserService
{
private void Validate()
{
}
public void Create()
{
Validate(); // OK
}
}
このValidateメソッドは、UserServiceクラスの中からは呼び出せますが、他のクラスからは呼び出せません。
C#var service = new UserService();
service.Validate(); // コンパイルエラー
一方、internalは同じアセンブリ内の別クラスからもアクセスできます。
C#internal class UserValidator
{
internal bool IsValid(string name)
{
return !string.IsNullOrWhiteSpace(name);
}
}
internal class UserService
{
public void Create(string name)
{
var validator = new UserValidator();
if (!validator.IsValid(name))
{
throw new ArgumentException("Invalid name");
}
}
}
privateは「そのクラスだけで使うもの」、internalは「同じアセンブリ内で共有するもの」です。
3-3. internalとprotectedの違い:継承関係かアセンブリか
protectedは、同じクラスまたは派生クラスからアクセスできるアクセス修飾子です。
C#public class BaseService
{
protected void WriteLog(string message)
{
Console.WriteLine(message);
}
}
public class UserService : BaseService
{
public void Create()
{
WriteLog("ユーザーを作成しました"); // OK
}
}
protectedは、継承関係にあるクラスから使えます。
一方、internalは継承関係ではなく、同じアセンブリ内かどうかでアクセス範囲が決まります。
C#internal class LogWriter
{
internal void Write(string message)
{
Console.WriteLine(message);
}
}
このLogWriterは、同じアセンブリ内であれば継承していないクラスからも使えます。逆に、別アセンブリであれば、派生クラスであっても通常のinternalメンバーにはアクセスできません。
protectedは「継承先に見せる」、internalは「同じアセンブリ内に見せる」と考えましょう。
3-4. アクセス範囲を比較表で整理
public、private、protected、internalの違いを整理すると、次のようになります。
| アクセス修飾子 | 同じクラス | 同じアセンブリの別クラス | 別アセンブリ | 派生クラス |
|---|---|---|---|---|
public | ○ | ○ | ○ | ○ |
private | ○ | × | × | × |
protected | ○ | × | × | ○ |
internal | ○ | ○ | × | 同じアセンブリなら○ |
protected internal | ○ | ○ | 派生クラスなら○ | ○ |
private protected | ○ | 同じアセンブリ内の派生クラスなら○ | × | 同じアセンブリ内なら○ |
internalのポイントは、アクセス可否の基準が「継承」でも「namespace」でもなく、アセンブリであることです。
3-5. どのアクセス修飾子を選ぶべきかの判断基準
アクセス修飾子を選ぶときは、まず「誰に使わせたいのか」を考えるのが基本です。
外部のプロジェクトやライブラリ利用者に使ってもらう必要があるならpublicを選びます。
C#public class PaymentService
{
public void Pay()
{
}
}
クラスの内部だけで使う処理ならprivateを選びます。
C#public class PaymentService
{
public void Pay()
{
Validate();
}
private void Validate()
{
}
}
同じアセンブリ内の複数クラスで共有したいけれど、外部には見せたくないならinternalを選びます。
C#internal class PaymentRequestBuilder
{
}
派生クラスにだけ見せたいならprotectedを選びます。
C#public abstract class BaseController
{
protected void WriteLog()
{
}
}
基本的な考え方は、必要最小限のアクセス範囲にすることです。迷ったときは、まず狭いアクセス範囲から考えると安全です。
4. internalの基本的な使い方とコード例
4-1. internal classの書き方
internal classは、次のように定義します。
C#internal class UserFormatter
{
public string Format(string firstName, string lastName)
{
return $"{lastName} {firstName}";
}
}
このクラスは、同じアセンブリ内からは使えます。
C#var formatter = new UserFormatter();
var name = formatter.Format("太郎", "山田");
Console.WriteLine(name);
別アセンブリからは、UserFormatterを直接使えません。
internal classは、外部公開する必要のない補助クラスに向いています。
C#public class UserService
{
private readonly UserFormatter _formatter = new();
public string GetDisplayName(string firstName, string lastName)
{
return _formatter.Format(firstName, lastName);
}
}
internal class UserFormatter
{
public string Format(string firstName, string lastName)
{
return $"{lastName} {firstName}";
}
}
この例では、外部にはUserServiceだけを公開し、UserFormatterは内部実装として隠しています。
4-2. internalメソッド・プロパティ・フィールドの書き方
internalはクラスだけでなく、メソッド、プロパティ、フィールドにも指定できます。
C#public class Order
{
public int Id { get; set; }
internal string InternalStatus { get; set; } = "Created";
internal DateTime CreatedAt { get; set; } = DateTime.Now;
internal void MarkAsProcessed()
{
InternalStatus = "Processed";
}
}
この場合、Orderクラス自体はpublicなので外部アセンブリからも使えます。しかし、InternalStatus、CreatedAt、MarkAsProcessedはinternalなので、別アセンブリからはアクセスできません。
同じアセンブリ内では次のように使えます。
C#var order = new Order();
order.MarkAsProcessed();
Console.WriteLine(order.InternalStatus);
別アセンブリからは、次のようなコードはコンパイルエラーになります。
C#var order = new Order();
order.MarkAsProcessed(); // コンパイルエラー
Console.WriteLine(order.InternalStatus); // コンパイルエラー
4-3. 同じプロジェクト内からアクセスできる例
多くの場合、1つのC#プロジェクトは1つのアセンブリとしてビルドされます。そのため、同じプロジェクト内のクラスからはinternalにアクセスできます。
たとえば、同じプロジェクト内に次の2つのファイルがあるとします。
MyApp
├── Services/UserService.cs
└── Helpers/UserNameNormalizer.cs
UserNameNormalizer.csは次のように定義します。
C#namespace MyApp.Helpers;
internal class UserNameNormalizer
{
public string Normalize(string name)
{
return name.Trim().ToLower();
}
}
UserService.csでは、このinternalクラスを使えます。
C#using MyApp.Helpers;
namespace MyApp.Services;
public class UserService
{
public void Register(string name)
{
var normalizer = new UserNameNormalizer();
var normalizedName = normalizer.Normalize(name);
Console.WriteLine(normalizedName);
}
}
同じプロジェクト内であれば、フォルダやnamespaceが違っていてもアクセスできます。重要なのは、同じアセンブリに含まれているかどうかです。
4-4. 別プロジェクトからアクセスできない例
次のようなソリューション構成を考えます。
Solution
├── MyLibrary
│ └── InternalCalculator.cs
└── MyApp
└── Program.cs
MyLibraryプロジェクトに、次のinternalクラスを定義します。
C#namespace MyLibrary;
internal class InternalCalculator
{
public int Add(int x, int y)
{
return x + y;
}
}
MyAppプロジェクトからMyLibraryを参照していても、次のコードはコンパイルエラーになります。
C#using MyLibrary;
var calculator = new InternalCalculator(); // エラー
var result = calculator.Add(1, 2);
InternalCalculatorはMyLibraryアセンブリの内部でしか使えないためです。
外部プロジェクトから使わせたい場合は、クラスをpublicにする必要があります。
C#namespace MyLibrary;
public class Calculator
{
public int Add(int x, int y)
{
return x + y;
}
}
4-5. コンパイルエラーになるケースと原因
internalに関する代表的なコンパイルエラーは、別アセンブリからアクセスしようとした場合に発生します。
C#// MyLibrary側
namespace MyLibrary;
internal class TokenGenerator
{
public string Generate()
{
return Guid.NewGuid().ToString();
}
}
C#// MyApp側
using MyLibrary;
var generator = new TokenGenerator(); // アクセスできない
この場合、TokenGeneratorがinternalなので、MyAppからは見えません。
また、publicメソッドの戻り値や引数にinternal型を使うと、アクセシビリティの不整合が起きることがあります。
C#internal class InternalResult
{
}
public class PublicService
{
public InternalResult GetResult()
{
return new InternalResult();
}
}
このコードは問題になります。PublicService.GetResultは外部に公開される可能性があるのに、戻り値のInternalResultが外部から見えないためです。
このような場合は、戻り値の型もpublicにするか、メソッドのアクセス範囲をinternalにします。
C#internal class InternalResult
{
}
public class PublicService
{
internal InternalResult GetResult()
{
return new InternalResult();
}
}
公開するものと内部に隠すもののアクセス範囲が矛盾しないように注意しましょう。
5. internalを使うメリット
5-1. ライブラリの内部実装を外部に見せずに済む
internalを使う大きなメリットは、ライブラリの内部実装を外部に見せずに済むことです。
たとえば、外部の利用者に使ってほしいのはJsonConfigLoaderだけで、その内部で使うパーサーや検証クラスは公開したくない場合があります。
C#public class JsonConfigLoader
{
private readonly JsonConfigParser _parser = new();
public Config Load(string json)
{
return _parser.Parse(json);
}
}
internal class JsonConfigParser
{
public Config Parse(string json)
{
// JSONを解析する処理
return new Config();
}
}
public class Config
{
}
このようにしておくと、外部利用者はJsonConfigLoaderだけを使えばよく、JsonConfigParserの存在を意識する必要がありません。
内部クラスをpublicにしてしまうと、利用者が直接使い始める可能性があります。一度publicにしたAPIは、後から変更や削除がしにくくなります。
internalにしておけば、外部に対する互換性を気にせず、内部実装を比較的自由に変更できます。
5-2. 同じプロジェクト内では柔軟に共有できる
privateにすると、そのクラス内でしか使えません。一方、internalにすると、同じアセンブリ内の複数クラスから共有できます。
たとえば、複数のサービスで共通して使う変換処理があるとします。
C#internal class DateTimeProvider
{
public DateTime Now()
{
return DateTime.Now;
}
}
同じアセンブリ内であれば、複数のクラスから使えます。
C#internal class OrderService
{
private readonly DateTimeProvider _dateTimeProvider = new();
public void CreateOrder()
{
var now = _dateTimeProvider.Now();
}
}
internal class InvoiceService
{
private readonly DateTimeProvider _dateTimeProvider = new();
public void CreateInvoice()
{
var now = _dateTimeProvider.Now();
}
}
このように、外部には公開しないけれど、プロジェクト内部では共有したいクラスにinternalは便利です。
5-3. public APIを増やしすぎない設計にできる
publicなクラスやメソッドは、外部利用者との契約になります。
ライブラリやNuGetパッケージでは、public APIを増やしすぎると、次のような問題が起きます。
・どれを使えばよいかわかりにくくなる
・内部実装まで利用されてしまう
・将来のリファクタリングで破壊的変更が起きやすくなる
・ドキュメント化すべき範囲が広がる
たとえば、次のようにすべてpublicにしてしまうと、外部から何でも使えてしまいます。
C#public class ReportService
{
}
public class ReportBuilder
{
}
public class ReportLineFormatter
{
}
public class ReportTemporaryBuffer
{
}
しかし、本当に外部に使ってほしいのがReportServiceだけなら、他のクラスはinternalにできます。
C#public class ReportService
{
}
internal class ReportBuilder
{
}
internal class ReportLineFormatter
{
}
internal class ReportTemporaryBuffer
{
}
このようにすると、公開APIが整理され、利用者にとっても使いやすい設計になります。
5-4. 保守性・安全性・カプセル化を高められる
internalを適切に使うと、保守性やカプセル化を高められます。
カプセル化とは、外部に見せる必要のない情報や処理を隠し、必要な操作だけを公開する考え方です。
C#public class UserRegistrationService
{
private readonly PasswordHasher _passwordHasher = new();
public void Register(string email, string password)
{
var hashedPassword = _passwordHasher.Hash(password);
// ユーザー登録処理
}
}
internal class PasswordHasher
{
public string Hash(string password)
{
return $"hashed:{password}";
}
}
この例では、外部の利用者はUserRegistrationService.Registerだけを使えばよく、内部でどのようにパスワードをハッシュ化しているかを知る必要はありません。
PasswordHasherをinternalにしておけば、外部から直接使われることを防げます。そのため、将来ハッシュ化の実装を変更しても、外部コードへの影響を抑えやすくなります。
5-5. チーム開発で意図しない利用を防ぎやすい
チーム開発では、クラスやメソッドが増えてくると「これは外部から使ってよいものなのか」「内部実装なのか」がわかりにくくなります。
internalを使うと、そのクラスが外部公開用ではないことをコード上で示せます。
C#internal class TemporaryUserImportContext
{
}
このような名前のクラスをpublicにしてしまうと、別プロジェクトからも使えてしまいます。しかし、internalにしておけば、少なくともアセンブリ外からの利用は防げます。
アクセス修飾子は、単なるコンパイル上の制限だけでなく、開発者に対する設計意図のメッセージにもなります。
6. internalを使うときの注意点
6-1. internalは完全なセキュリティ機能ではない
internalはアクセス制御の仕組みですが、完全なセキュリティ機能として考えるべきではありません。
internalにしたからといって、機密情報を安全に隠せるわけではありません。リフレクションなどを使えば、通常のコードから直接アクセスできないメンバーにアクセスされる可能性もあります。
そのため、次のような目的でinternalに頼るのは避けるべきです。
・秘密鍵を隠す
・パスワードを保護する
・ライセンスチェックの重要処理を完全に隠す
・攻撃者からロジックを守る
internalは、あくまで設計上のアクセス範囲を制御するための機能です。セキュリティ対策は、認証、認可、暗号化、適切な秘密情報管理など、別の仕組みで行う必要があります。
6-2. 同じアセンブリ内ではどこからでも使えてしまう
internalは外部アセンブリからのアクセスを防ぎますが、同じアセンブリ内であれば基本的にどこからでもアクセスできます。
C#internal class DatabaseConnectionFactory
{
public object Create()
{
return new object();
}
}
このクラスは、同じアセンブリ内のどのクラスからでも使えます。
そのため、何でもinternalにしてしまうと、アセンブリ内部で依存関係が広がりすぎることがあります。
本当にそのクラスだけで使うメソッドなら、internalではなくprivateにするべきです。
C#public class UserService
{
public void Register()
{
Validate();
}
private void Validate()
{
// UserServiceの内部だけで使う処理
}
}
internalはprivateより広いアクセス範囲を持つため、安易に使うと内部構造が複雑になりやすい点に注意しましょう。
6-3. internalを多用すると依存関係が複雑になる
internalなクラスは同じアセンブリ内から自由に使えるため、多用するとクラス同士の依存関係が増えやすくなります。
たとえば、次のようにさまざまなクラスが互いにinternalクラスを直接参照していると、変更の影響範囲が広くなります。
C#internal class UserCache
{
}
internal class UserService
{
private readonly UserCache _cache = new();
}
internal class OrderService
{
private readonly UserCache _cache = new();
}
internal class ReportService
{
private readonly UserCache _cache = new();
}
この状態でUserCacheの仕様を変更すると、複数のサービスに影響する可能性があります。
internalは便利ですが、共有範囲が広いということは、それだけ依存されやすいということでもあります。
必要に応じて、インターフェースを使ったり、責務を分けたりして、依存関係が複雑になりすぎないようにしましょう。
6-4. publicにすべきものまでinternalにしない
外部プロジェクトから使うことが前提のクラスやメソッドは、internalではなくpublicにする必要があります。
たとえば、ライブラリ利用者に使ってもらうメイン機能をinternalにしてしまうと、外部からアクセスできません。
C#internal class CsvReader
{
public string[] Read(string line)
{
return line.Split(',');
}
}
この場合、別プロジェクトからCsvReaderを使えないため、ライブラリとして機能しません。
外部に公開する入り口は、明確にpublicにしましょう。
C#public class CsvReader
{
public string[] Read(string line)
{
return line.Split(',');
}
}
ただし、その内部で使う補助クラスはinternalにできます。
C#internal class CsvLineValidator
{
}
公開すべきものと隠すべきものを分けることが重要です。
6-5. privateで十分なものをinternalにしない
privateで十分なものをinternalにしてしまうのも避けるべきです。
たとえば、あるクラスの内部でしか使わない処理はprivateにします。
C#public class MailService
{
public void Send(string address)
{
ValidateAddress(address);
// メール送信処理
}
private void ValidateAddress(string address)
{
if (string.IsNullOrWhiteSpace(address))
{
throw new ArgumentException("address is required");
}
}
}
このValidateAddressメソッドをinternalにすると、同じアセンブリ内の別クラスからも呼び出せるようになります。
C#internal void ValidateAddress(string address)
{
}
すると、本来MailServiceの内部処理だったものが、他のクラスからも依存される可能性があります。
アクセス範囲は、できるだけ狭くするのが基本です。
クラス内だけで使う → private
同じアセンブリで使う → internal
外部にも公開する → public
この基準を意識すると、internalの使いすぎを防げます。
7. protected internal・private protectedとの違い
7-1. protected internalとは
protected internalは、protectedとinternalを組み合わせたアクセス修飾子です。
意味は、同じアセンブリ内からアクセスできる、または派生クラスからアクセスできるです。
C#public class BaseService
{
protected internal void WriteLog(string message)
{
Console.WriteLine(message);
}
}
このWriteLogメソッドは、次のどちらかに該当すればアクセスできます。
・同じアセンブリ内にあるクラス
・別アセンブリであってもBaseServiceを継承したクラス
つまり、protected internalは「protectedまたはinternal」と考えると理解しやすいです。
C#public class UserService : BaseService
{
public void Create()
{
WriteLog("作成しました"); // 派生クラスなのでOK
}
}
同じアセンブリ内であれば、継承していないクラスからもアクセスできます。
C#public class LogTest
{
public void Test()
{
var service = new BaseService();
service.WriteLog("test"); // 同じアセンブリならOK
}
}
7-2. private protectedとは
private protectedは、同じアセンブリ内にある派生クラスからのみアクセスできるアクセス修飾子です。
C#public class BaseRepository
{
private protected void OpenConnection()
{
Console.WriteLine("接続を開きます");
}
}
このOpenConnectionメソッドは、同じアセンブリ内にある派生クラスからはアクセスできます。
C#public class UserRepository : BaseRepository
{
public void Find()
{
OpenConnection(); // 同じアセンブリ内の派生クラスなのでOK
}
}
しかし、同じアセンブリ内であっても、派生クラスでなければアクセスできません。
C#public class OtherClass
{
public void Test()
{
var repository = new BaseRepository();
repository.OpenConnection(); // コンパイルエラー
}
}
また、別アセンブリにある派生クラスからもアクセスできません。
private protectedは、「private寄りのprotected」と考えるとわかりやすいです。
7-3. internalとのアクセス範囲の違い
internal、protected internal、private protectedの違いを整理すると、次のようになります。
| アクセス修飾子 | 同じアセンブリの別クラス | 同じアセンブリの派生クラス | 別アセンブリの派生クラス |
|---|---|---|---|
internal | ○ | ○ | × |
protected internal | ○ | ○ | ○ |
private protected | × | ○ | × |
internalは、同じアセンブリ内であれば派生クラスでなくてもアクセスできます。
protected internalは、同じアセンブリ内からも、別アセンブリの派生クラスからもアクセスできます。
private protectedは、同じアセンブリ内の派生クラスだけに限定されます。
この3つは名前が似ていますが、アクセス範囲はかなり違います。
7-4. 継承とアセンブリが絡む場合の考え方
継承とアセンブリが絡む場合は、次の2つの軸で考えると整理しやすくなります。
・同じアセンブリかどうか
・派生クラスかどうか
internalは、同じアセンブリかどうかだけを見ます。
C#internal void Execute()
{
}
protectedは、派生クラスかどうかを見ます。
C#protected void Execute()
{
}
protected internalは、同じアセンブリまたは派生クラスかどうかを見ます。
C#protected internal void Execute()
{
}
private protectedは、同じアセンブリかつ派生クラスかどうかを見ます。
C#private protected void Execute()
{
}
つまり、protected internalは範囲が広く、private protectedは範囲が狭いです。
7-5. 実務で使い分ける際の注意点
実務では、protected internalやprivate protectedは、public、private、internal、protectedほど頻繁には使いません。
特にprotected internalはアクセス範囲が広いため、意図せず外部の派生クラスから使われる可能性があります。
ライブラリを作る場合、基底クラスの拡張ポイントとして外部の派生クラスに使わせたいならprotectedを検討します。
C#public abstract class BaseHandler
{
protected abstract void HandleCore();
}
同じアセンブリ内部だけで使いたいならinternalを検討します。
C#internal class HandlerContext
{
}
同じアセンブリ内の派生クラスにだけ見せたい場合はprivate protectedが候補になります。
C#public class BaseHandler
{
private protected void Initialize()
{
}
}
ただし、複雑なアクセス修飾子を使うと、チームメンバーが意図を理解しにくくなる場合があります。必要性が明確でない限り、まずはシンプルなprivate、internal、protected、publicで設計できないかを考えるとよいでしょう。
8. internalと単体テストの関係
8-1. internalなクラスやメソッドは別テストプロジェクトから直接呼べない
C#では、単体テスト用のプロジェクトをアプリケーション本体とは別に作ることがよくあります。
Solution
├── MyApp
└── MyApp.Tests
この場合、MyAppとMyApp.Testsは別アセンブリです。
そのため、MyApp側にinternalなクラスがあると、通常はMyApp.Testsから直接アクセスできません。
C#// MyApp側
namespace MyApp;
internal class UserNameNormalizer
{
public string Normalize(string name)
{
return name.Trim().ToLower();
}
}
C#// MyApp.Tests側
using MyApp;
public class UserNameNormalizerTests
{
[Fact]
public void Normalize_ReturnsLowerCase()
{
var normalizer = new UserNameNormalizer(); // コンパイルエラー
}
}
これは、テストプロジェクトが別アセンブリだからです。
8-2. InternalsVisibleTo属性とは
internalなクラスやメンバーを、特定の別アセンブリから見えるようにする仕組みとして、InternalsVisibleTo属性があります。
InternalsVisibleToを使うと、指定したアセンブリに対してinternalメンバーへのアクセスを許可できます。
C#using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MyApp.Tests")]
この設定をMyApp側に追加すると、MyApp.TestsからMyAppのinternalなクラスやメソッドにアクセスできるようになります。
単体テストで内部クラスを直接テストしたい場合によく使われます。
8-3. テストプロジェクトにinternalメンバーを公開する方法
InternalsVisibleToを使うには、アプリケーション本体側のプロジェクトに属性を追加します。
たとえば、AssemblyInfo.csに次のように書きます。
C#using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MyApp.Tests")]
または、任意の.csファイルに同じ内容を記述しても構いません。
C#using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MyApp.Tests")]
このとき、指定するのはプロジェクト名ではなく、テストアセンブリ名です。多くの場合はテストプロジェクト名と一致しますが、設定によって異なる場合もあります。
設定後、テストプロジェクトからinternalクラスを使えるようになります。
C#public class UserNameNormalizerTests
{
[Fact]
public void Normalize_TrimsAndLowerCasesName()
{
var normalizer = new UserNameNormalizer();
var result = normalizer.Normalize(" TARO ");
Assert.Equal("taro", result);
}
}
8-4. internalをテストするべきか設計を見直すべきか
InternalsVisibleToを使えばinternalなクラスをテストできますが、すべてのinternalを直接テストすべきとは限りません。
まず考えるべきなのは、その内部クラスを直接テストする必要があるかどうかです。
たとえば、次のような構成があるとします。
C#public class UserService
{
private readonly UserNameNormalizer _normalizer = new();
public string Register(string name)
{
return _normalizer.Normalize(name);
}
}
internal class UserNameNormalizer
{
public string Normalize(string name)
{
return name.Trim().ToLower();
}
}
この場合、UserNameNormalizerを直接テストしなくても、UserService.Registerのテストを通じて振る舞いを確認できる場合があります。
一方で、internalクラスに複雑なロジックがある場合は、直接テストしたほうがわかりやすいこともあります。
・単純な補助処理ならpublic API経由でテストする
・複雑な内部ロジックならInternalsVisibleToを使ってテストする
・そもそも複雑すぎる場合は設計を見直す
internalをテストしたくなる場面では、そのクラスの責務が大きくなりすぎていないかも確認しましょう。
8-5. テストしやすい設計にするためのポイント
テストしやすい設計にするには、アクセス修飾子だけでなく、責務の分割や依存関係の扱いも重要です。
たとえば、内部ロジックを小さなクラスに分けると、internalでもテストしやすくなります。
C#internal class PriceCalculator
{
public decimal Calculate(decimal price, decimal taxRate)
{
return price + price * taxRate;
}
}
また、外部に公開する必要がある抽象化だけをpublicにし、実装クラスをinternalにする設計もあります。
C#public interface IClock
{
DateTime Now { get; }
}
internal class SystemClock : IClock
{
public DateTime Now => DateTime.Now;
}
このようにすると、外部からはIClockを使い、具体的なSystemClockは内部実装として隠せます。
テストしやすさを考えるときは、単にpublicにして解決するのではなく、公開すべき抽象と隠すべき実装を分けることが大切です。
9. 実務でのinternalの使いどころ
9-1. ライブラリやNuGetパッケージの内部クラス
internalが特に役立つのは、ライブラリやNuGetパッケージを作る場面です。
ライブラリでは、利用者に使ってほしいAPIと、ライブラリ内部で使う実装クラスを明確に分ける必要があります。
C#public class ExcelExporter
{
private readonly ExcelCellFormatter _formatter = new();
public void Export()
{
// エクスポート処理
}
}
internal class ExcelCellFormatter
{
public string Format(object value)
{
return value?.ToString() ?? string.Empty;
}
}
この例では、利用者に公開するのはExcelExporterだけです。ExcelCellFormatterは内部処理なのでinternalにしています。
このようにしておくと、ライブラリの利用者が内部クラスに依存することを防げます。
9-2. アプリケーション内部だけで使うサービスクラス
アプリケーション開発でも、外部に公開する必要のないサービスクラスにinternalを使うことがあります。
C#internal class UserImportService
{
public void Import(string path)
{
// ユーザー取り込み処理
}
}
特に、Webアプリケーションやバッチアプリケーションでは、アプリケーション内部でしか使わない処理が多く存在します。
すべてをpublicにするのではなく、アプリケーション内部で閉じるものはinternalにしておくと、コードの意図が明確になります。
C#public class UserController
{
private readonly UserImportService _importService;
public UserController(UserImportService importService)
{
_importService = importService;
}
}
このように、コントローラーなど外部から呼ばれる入り口はpublicにし、内部サービスはinternalにする設計が考えられます。
9-3. DTO・ヘルパークラス・ユーティリティクラス
DTO、ヘルパークラス、ユーティリティクラスにもinternalはよく使われます。
C#internal class UserCsvRow
{
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
このUserCsvRowは、CSV読み込み処理の内部でだけ使うDTOです。外部に公開する必要がないならinternalが適しています。
ヘルパークラスも同様です。
C#internal static class StringHelper
{
public static bool IsNullOrWhiteSpace(string value)
{
return string.IsNullOrWhiteSpace(value);
}
}
ただし、HelperやUtilityという名前のクラスは責務が曖昧になりやすいため、何でも詰め込まないように注意しましょう。
9-4. ドメイン層やインフラ層での使い分け
レイヤードアーキテクチャやクリーンアーキテクチャでは、ドメイン層、アプリケーション層、インフラ層などにプロジェクトを分けることがあります。
MyApp.Domain
MyApp.Application
MyApp.Infrastructure
MyApp.Web
このような構成では、各プロジェクトが別アセンブリになるため、internalの境界が設計上の境界として機能します。
たとえば、インフラ層の内部実装をinternalにできます。
C#namespace MyApp.Infrastructure;
internal class SqlConnectionFactory
{
}
一方、アプリケーション層から使う必要があるインターフェースやサービスはpublicにする必要があります。
C#public interface IUserRepository
{
User FindById(int id);
}
実装クラスをinternalにし、抽象だけをpublicにする設計もよく使われます。
C#public interface IUserRepository
{
User FindById(int id);
}
internal class SqlUserRepository : IUserRepository
{
public User FindById(int id)
{
return new User();
}
}
このようにすると、外部には抽象だけを公開し、具体的な実装を隠せます。
9-5. APIとして公開したくない実装詳細の隠蔽
internalは、APIとして公開したくない実装詳細を隠すために使います。
たとえば、外部に公開したいのは次のようなシンプルなAPIだとします。
C#public class NotificationClient
{
public void Send(string message)
{
var payload = NotificationPayloadBuilder.Build(message);
// 送信処理
}
}
内部で使うペイロード生成処理はinternalにできます。
C#internal static class NotificationPayloadBuilder
{
public static string Build(string message)
{
return $"{{ \"message\": \"{message}\" }}";
}
}
この設計では、外部利用者はNotificationClient.Sendだけを使えばよく、内部でどのようなJSONを組み立てているかを意識する必要がありません。
内部実装を隠すことで、利用者にとってわかりやすく、開発者にとって変更しやすいコードになります。
10. internalを使うべきか迷ったときの判断基準
10-1. 外部プロジェクトから使わせたいならpublic
外部プロジェクトから使わせたいクラスやメソッドは、publicにします。
C#public class UserApiClient
{
public Task GetUsersAsync()
{
return Task.CompletedTask;
}
}
ライブラリの利用者、別プロジェクト、別アセンブリから直接使う必要があるなら、internalではなくpublicです。
ただし、何でもpublicにするのは避けましょう。publicは外部との契約になるため、将来の変更が難しくなります。
公開する前に、次の点を確認するとよいです。
・外部から本当に使う必要があるか
・使い方を説明できるAPIになっているか
・将来変更しても問題ないか
・内部実装を漏らしていないか
外部に使わせる明確な理由があるものだけをpublicにしましょう。
10-2. クラス内だけで使うならprivate
そのクラスの内部だけで使うメソッドやフィールドは、privateにします。
C#public class OrderService
{
public void CreateOrder()
{
Validate();
Save();
}
private void Validate()
{
}
private void Save()
{
}
}
ValidateやSaveがOrderServiceの内部処理でしかないなら、privateが適切です。
internalにすると、同じアセンブリ内の他のクラスから呼び出せるようになってしまいます。
C#internal void Validate()
{
}
将来、他のクラスがこのメソッドに依存し始めると、リファクタリングが難しくなります。
迷ったときは、まずprivateで十分かを考えましょう。
10-3. 同じアセンブリ内で共有したいならinternal
同じアセンブリ内の複数クラスから使いたいけれど、外部プロジェクトには見せたくない場合はinternalが適しています。
C#internal class RetryPolicy
{
public void Execute(Action action)
{
for (var i = 0; i < 3; i++)
{
try
{
action();
return;
}
catch
{
if (i == 2)
{
throw;
}
}
}
}
}
このRetryPolicyは、同じアプリケーション内部の複数サービスで使う補助クラスとしては便利です。
C#internal class PaymentService
{
private readonly RetryPolicy _retryPolicy = new();
public void Pay()
{
_retryPolicy.Execute(() =>
{
// 決済処理
});
}
}
外部には公開せず、内部で共有する。この目的にinternalは向いています。
10-4. 継承先だけに見せたいならprotected
派生クラスにだけ見せたいメソッドやプロパティは、protectedにします。
C#public abstract class BaseValidator
{
public bool Validate(string value)
{
return ValidateCore(value);
}
protected abstract bool ValidateCore(string value);
}
この場合、外部のコードはValidateCoreを直接呼び出せません。しかし、派生クラスはValidateCoreを実装できます。
C#public class EmailValidator : BaseValidator
{
protected override bool ValidateCore(string value)
{
return value.Contains("@");
}
}
継承を前提にした拡張ポイントを作るならprotectedが適しています。
ただし、継承はクラス設計を複雑にすることもあります。単に同じアセンブリ内で共有したいだけなら、protectedではなくinternalを検討しましょう。
10-5. 将来の変更に強いアクセス範囲の決め方
将来の変更に強い設計にするには、アクセス範囲を必要最小限にすることが大切です。
最初から広く公開してしまうと、後から狭くするのが難しくなります。
たとえば、publicにしたクラスを後からinternalに変えると、そのクラスを使っていた外部コードが壊れます。
C#public class OldService
{
}
これを次のように変更すると、外部利用者にとっては破壊的変更になります。
C#internal class OldService
{
}
一方、最初にinternalにしておいたクラスを、必要になってからpublicに広げることは比較的行いやすいです。
C#internal class NewService
{
}
必要になった時点で、公開APIとして設計を見直してからpublicにします。
C#public class NewService
{
}
アクセス修飾子は「後から広げるのは比較的簡単、後から狭めるのは難しい」と考えると判断しやすくなります。
11. internalに関するよくある疑問
11-1. internalは同じnamespaceならアクセスできる?
いいえ。internalは同じnamespaceかどうかではなく、同じアセンブリかどうかでアクセス可否が決まります。
たとえば、同じnamespaceでも別アセンブリならアクセスできません。
C#namespace MyApp.Services;
internal class UserService
{
}
別プロジェクトで同じnamespaceを使っても、アセンブリが違えばinternalクラスにはアクセスできません。
C#namespace MyApp.Services;
// 別アセンブリならUserServiceにはアクセスできない
namespaceは名前の整理のための仕組みであり、アクセス範囲を決める境界ではありません。
11-2. internalは同じフォルダならアクセスできる?
いいえ。同じフォルダかどうかもinternalのアクセス範囲には関係ありません。
C#では、フォルダ構成はコードを整理するためのものです。アクセス制御はフォルダではなく、アクセス修飾子、型、アセンブリなどによって決まります。
MyApp
├── Services
│ └── UserService.cs
└── Helpers
└── UserHelper.cs
UserHelperがinternalなら、同じアセンブリ内のUserServiceからアクセスできます。フォルダが違っていても問題ありません。
逆に、同じようなフォルダ構成でも、別プロジェクト、別アセンブリであればアクセスできません。
11-3. internal classとpublic classのinternalメンバーは何が違う?
internal classは、クラスそのものが同じアセンブリ内でしか使えません。
C#internal class InternalUser
{
public string Name { get; set; } = string.Empty;
}
この場合、Nameプロパティはpublicですが、InternalUserクラス自体がinternalなので、外部アセンブリからはクラス自体にアクセスできません。
一方、public classの中にinternalメンバーを持たせることもできます。
C#public class PublicUser
{
public string Name { get; set; } = string.Empty;
internal string InternalCode { get; set; } = string.Empty;
}
この場合、外部アセンブリからPublicUserクラスやNameプロパティにはアクセスできます。しかし、InternalCodeにはアクセスできません。
つまり、違いは次のとおりです。
internal class
→ 型そのものを外部に見せない
public class の internal メンバー
→ 型は外部に見せるが、一部のメンバーは内部に隠す
どちらを使うべきかは、型そのものを公開したいかどうかで判断します。
11-4. internalはインターフェースにも使える?
はい。internalはインターフェースにも使えます。
C#internal interface IFileReader
{
string Read(string path);
}
このIFileReaderは、同じアセンブリ内でのみ使えるインターフェースです。
実装クラスもinternalにできます。
C#internal class LocalFileReader : IFileReader
{
public string Read(string path)
{
return File.ReadAllText(path);
}
}
一方、外部に公開する抽象として使いたいインターフェースはpublicにします。
C#public interface IUserRepository
{
User FindById(int id);
}
そして、実装クラスだけをinternalにすることもあります。
C#internal class SqlUserRepository : IUserRepository
{
public User FindById(int id)
{
return new User();
}
}
このように、インターフェースをpublicにして実装をinternalにすると、外部には契約だけを公開し、実装詳細を隠せます。
11-5. internalはUnityやASP.NETでも使える?
はい。internalはC#の言語機能なので、UnityやASP.NETでも使えます。
ASP.NETでは、コントローラーや公開するモデルはpublicにすることが多いですが、アプリケーション内部で使うサービス、ヘルパー、実装クラスをinternalにすることがあります。
C#internal class EmailTemplateRenderer
{
public string Render(string template, object model)
{
return template;
}
}
Unityでも、同じアセンブリ内でだけ使うクラスにinternalを指定できます。
C#internal class EnemySpawnCalculator
{
public int Calculate(int level)
{
return level * 3;
}
}
ただし、Unityではアセンブリ定義ファイルを使っているかどうかによって、アセンブリの分かれ方が変わる場合があります。internalの範囲はプロジェクト全体ではなく、あくまで同じアセンブリ内です。
ASP.NETでもUnityでも、考え方は同じです。
外部やフレームワークから使われる必要があるもの → public
内部実装として隠したいもの → internal
クラス内だけで使うもの → private
まとめ
C#のinternalは、同じアセンブリ内からだけアクセスできるアクセス修飾子です。
publicのように外部プロジェクトへ公開するわけではなく、privateのように1つのクラス内だけに閉じるわけでもありません。internalは、同じプロジェクトや同じアセンブリ内部で共有したいクラスやメンバーに向いています。
internalのポイントを整理すると、次のようになります。
・internalは同じアセンブリ内だけでアクセスできる
・同じnamespaceや同じフォルダかどうかは関係ない
・トップレベルのクラスは省略するとinternalになる
・ライブラリの内部実装を隠すのに便利
・public APIを増やしすぎない設計にできる
・privateで十分なものをinternalにしないことが重要
・テストプロジェクトから使う場合はInternalsVisibleToを検討する
アクセス修飾子を選ぶときは、「誰に使わせたいのか」を基準にすると判断しやすくなります。
外部から使わせたい → public
クラス内だけで使う → private
同じアセンブリで使う → internal
派生クラスに見せたい → protected
internalを適切に使うと、外部に公開するAPIを整理し、内部実装を隠しながら、同じアセンブリ内では柔軟にコードを共有できます。
C#で保守しやすい設計を目指すなら、publicにする前に「これは本当に外部公開すべきか」を考え、内部実装には積極的にinternalを活用するとよいでしょう。

