カプセル化は「必要最小限だけを外部に公開し、内部実装を隠す」というオブジェクト指向の根幹です。C# には public・private・protected の3種だけでなく、internal・protected internal・private protected を合わせた全6種のアクセス修飾子があります。
本記事では全アクセス修飾子の詳細・デフォルト値・プロパティへの非対称設定・テスト向けの InternalsVisibleTo・設計原則まで体系的に解説します。
カプセル化の本質
カプセル化の目的は「クラスの使い方を正しく制限すること」です。外部から直接データを書き換えられると、クラスが想定しない状態になりバグの原因になります。アクセス修飾子で公開範囲を絞ることで、クラスの不変条件(invariant)を保護します。
// ── カプセル化なし(フィールドが public)──
class BadAccount
{
public decimal Balance; // 誰でも直接書き換えられる
}
var bad = new BadAccount();
bad.Balance = -99999; // 残高がマイナスになっても誰も止められない
// ── カプセル化あり(プロパティで守る)──
class GoodAccount
{
private decimal _balance;
public decimal Balance
{
get => _balance;
private set // 外部からは設定不可。内部のメソッドだけが変更する
{
if (value < 0) throw new InvalidOperationException("残高不足");
_balance = value;
}
}
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("金額は正の値にしてください");
Balance += amount;
}
public void Withdraw(decimal amount)
{
if (amount <= 0) throw new ArgumentException("金額は正の値にしてください");
Balance -= amount; // Balance の setter でマイナスチェック
}
}
var account = new GoodAccount();
account.Deposit(10_000);
account.Withdraw(3_000);
Console.WriteLine(account.Balance); // 7000
// account.Balance = -1; // コンパイルエラー: private set のため変更不可
全6種のアクセス修飾子
public ― どこからでもアクセス可能
制限なし。どのアセンブリのどのクラスからもアクセスできます。外部ライブラリとして公開する API・インターフェースの実装メソッドに使います。
public class Calculator
{
// 外部から自由に使える計算メソッド
public double Add(double a, double b) => a + b;
public double Sqrt(double x) => Math.Sqrt(x);
}
// どこからでも呼べる
var calc = new Calculator();
Console.WriteLine(calc.Add(3, 4)); // 7
private ― 同じクラス内のみ
最も制限が強く、同じクラス(または同じ型)の内部からのみアクセスできます。クラスメンバーを何も書かなかった場合のデフォルトが private です。
class PasswordManager
{
// 外部に公開しない内部データ
private string _hashedPassword = "";
// 外部に公開しない内部ヘルパー
private string Hash(string raw)
=> Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(
System.Text.Encoding.UTF8.GetBytes(raw)));
// 外部に公開する操作(private メンバーを内部で使う)
public void SetPassword(string raw)
=> _hashedPassword = Hash(raw);
public bool Verify(string raw)
=> _hashedPassword == Hash(raw);
}
var pm = new PasswordManager();
pm.SetPassword("MySecret123");
Console.WriteLine(pm.Verify("MySecret123")); // True
Console.WriteLine(pm.Verify("wrong")); // False
// pm._hashedPassword = "hack"; // コンパイルエラー
protected ― 同クラスと派生クラス
クラス外部からはアクセスできませんが、継承した子クラスからは使えます。基底クラスが持つ共通データや補助メソッドを子クラスだけに公開したいときに使います。
abstract class HttpClientBase
{
// 子クラスだけが使える HTTP クライアント
protected readonly HttpClient _http = new();
// 子クラスだけが使えるログメソッド
protected void LogRequest(string url)
=> Console.WriteLine($"[Request] {url}");
public abstract Task<string> FetchAsync(string url);
}
class ApiClient : HttpClientBase
{
public override async Task<string> FetchAsync(string url)
{
LogRequest(url); // protected メソッドを使える
return await _http.GetStringAsync(url); // protected フィールドを使える
}
}
// 外部からは見えない
// new ApiClient()._http; // コンパイルエラー
// new ApiClient().LogRequest("x"); // コンパイルエラー
protected フィールドを多用すると、子クラスが親クラスの内部状態に直接依存するため密結合になりがちです。フィールドは private にして、protected なプロパティやメソッドで安全に公開する設計を推奨します。詳しくは継承とオーバーライド完全ガイドを参照してください。internal ― 同一アセンブリ内のみ
同じアセンブリ(プロジェクト / .dll / .exe)の中であればどのクラスからもアクセスできますが、別のアセンブリからは見えません。クラス宣言に何も付けなかった場合のデフォルトが internal です。
// ── MyLibrary プロジェクト ──
// public: 外部ライブラリの利用者に公開する API
public class UserService
{
private readonly UserRepository _repo = new();
public User? FindUser(int id) => _repo.FindById(id);
}
// internal: ライブラリ内部だけで使う実装クラス(外部に見せない)
internal class UserRepository
{
private readonly List<User> _users = new();
internal User? FindById(int id)
=> _users.FirstOrDefault(u => u.Id == id);
}
public class User
{
public int Id { get; init; }
public string Name { get; init; } = "";
}
// ── ConsoleApp プロジェクト(別アセンブリ)──
// var repo = new UserRepository(); // コンパイルエラー: internal のため見えない
// var svc = new UserService(); // OK: public なのでアクセス可能
public、内部実装は internal」という分離が重要です。公開する必要のないクラスを internal にしておくと、後からリファクタリングしやすくなります(利用者に影響なく内部を変更できる)。protected internal ― 派生クラス OR 同一アセンブリ
protected と internal の和集合(OR)です。同一アセンブリ内のどのクラスからでも、または別アセンブリでも継承した派生クラスからアクセスできます。
// ── Base ライブラリ ──
public class BaseLogger
{
// 同一アセンブリ内 OR 派生クラスならアクセス可能
protected internal void WriteLog(string message)
=> Console.WriteLine($"[LOG] {message}");
}
// 同一アセンブリ内: 継承しなくてもアクセスできる
class LogHelper
{
void Test()
{
var logger = new BaseLogger();
logger.WriteLog("アセンブリ内からアクセス"); // OK
}
}
// 別アセンブリの派生クラス: 継承しているのでアクセスできる
public class CustomLogger : BaseLogger
{
public void Custom()
=> WriteLog("派生クラスからアクセス"); // OK
}
// 別アセンブリの非派生クラス: アクセス不可
// class External { void X() { new BaseLogger().WriteLog("…"); } } // エラー
private protected ― 派生クラス AND 同一アセンブリ
C# 7.2 で追加された最も制限の強い複合修飾子です。protected と internal の積集合(AND)で、「同一アセンブリ内の派生クラスのみ」アクセスできます。
// ── MyFramework ライブラリ ──
public class FrameworkBase
{
// 同一アセンブリの派生クラスだけに公開する内部フック
private protected virtual void OnInitialize()
=> Console.WriteLine("基本の初期化");
}
// 同一アセンブリの派生クラス: アクセス可能
internal class InternalExtension : FrameworkBase
{
private protected override void OnInitialize()
{
base.OnInitialize();
Console.WriteLine("拡張初期化");
}
}
// 別アセンブリの派生クラス: アクセス不可(external はフレームワーク外)
// public class ExternalExtension : FrameworkBase
// {
// private protected override void OnInitialize() { } // コンパイルエラー
// }
private protected はフレームワーク内部でのみオーバーライドを許可したいフックポイントに有用です。外部ライブラリの利用者には触れさせたくないが、ライブラリ内の派生クラスには公開したいメンバーに使います。修飾子を省略したときのデフォルト
アクセス修飾子を省略した場合、宣言する場所によってデフォルト値が決まります。明示しておく方がコードの意図が伝わりやすいですが、デフォルト値を知っておくことで既存コードを読む際に役立ちます。
| 宣言する場所 | デフォルト | 備考 |
|---|---|---|
| トップレベルクラス・構造体 | internal |
同一アセンブリ内のみ公開 |
| クラスのメンバー(フィールド・メソッド等) | private |
クラス内部のみ |
| インターフェースのメンバー | public |
すべてに公開(修飾子を書くとエラー) |
| 列挙型(enum)のメンバー | public |
enum 内のメンバーは常に public |
| 名前空間 | 適用なし | 名前空間にアクセス修飾子は付けられない |
プロパティの非対称アクセス設定
プロパティの get と set(または init)に異なるアクセス修飾子を付けることができます。「外から読めるが外から書けない」設計を簡潔に表現できます。
class Order
{
// public get / private set: 外部から読み取り専用
public int Id { get; private set; }
public string Status { get; private set; } = "Pending";
// public get / protected set: 子クラスから変更可能
public decimal TotalAmount { get; protected set; }
// public get / internal set: 同一アセンブリからのみ変更可能
public DateTime CreatedAt { get; internal set; } = DateTime.UtcNow;
// public get / init: オブジェクト初期化子でのみ設定可能(C# 9)
public string CustomerName { get; init; } = "";
public Order(int id) => Id = id;
// ビジネスロジックのメソッドだけが Status を変更できる
public void Ship()
{
if (Status != "Pending")
throw new InvalidOperationException("出荷済みまたはキャンセル済みです");
Status = "Shipped"; // private set なのでクラス内からは変更可能
}
}
var order = new Order(1001) { CustomerName = "Taro" };
Console.WriteLine(order.Status); // Pending
order.Ship();
Console.WriteLine(order.Status); // Shipped
// order.Status = "Pending"; // コンパイルエラー: private set
| 書き方 | set できる範囲 | 主な用途 |
|---|---|---|
{ get; private set; } |
クラス内部のみ set | ビジネスロジックだけが状態を変える |
{ get; protected set; } |
派生クラスまで set | テンプレートメソッドパターンで状態を設定 |
{ get; internal set; } |
同一アセンブリ内で set | フレームワーク内部で管理する値 |
{ get; init; } |
オブジェクト初期化子のみで set(C# 9) | イミュータブルオブジェクト設計 |
{ get; } |
コンストラクタのみで設定可 | 完全に読み取り専用の値 |
readonly と private の組み合わせ
private readonly フィールドはコンストラクタで設定後は変更できない、最も安全なフィールド定義です。不変オブジェクト(イミュータブル)設計の基本です。
class Money
{
// コンストラクタで設定後は変更不可
private readonly decimal _amount;
private readonly string _currency;
public Money(decimal amount, string currency)
{
if (amount < 0) throw new ArgumentException("金額は0以上にしてください");
_amount = amount;
_currency = currency;
}
public decimal Amount => _amount;
public string Currency => _currency;
// 新しい Money を返すことで元のオブジェクトは変わらない
public Money Add(Money other)
{
if (_currency != other._currency)
throw new InvalidOperationException("通貨が異なります");
return new Money(_amount + other._amount, _currency);
}
public override string ToString() => $"{_amount:N0} {_currency}";
}
var price = new Money(1_000, "JPY");
var tax = new Money(100, "JPY");
var total = price.Add(tax);
Console.WriteLine(total); // 1,100 JPY
// price._amount = 999; // コンパイルエラー: private readonly
設計原則:最小公開の原則
「必要最小限のアクセスレベルを使う」のがアクセス修飾子設計の鉄則です。最初から広いアクセスを与えると、後から狭めるときに利用側のコードが壊れます。
| 原則 | 理由 |
|---|---|
まず private で始める |
クラスのメンバーはすべて private から始め、必要になったら範囲を広げる |
クラスは internal から始める |
外部に公開する必要がないクラスは internal に。意図せず public にしない |
protected は慎重に |
protected メンバーは「継承インターフェース」になる。後から変更しにくい |
public は意図を持って |
一度 public にしたメンバーは外部コードが依存する。変更コストが高い |
| フィールドを直接 public にしない | フィールドは private・内部状態の公開はプロパティで行う |
InternalsVisibleTo:テストプロジェクトから internal にアクセスする
本番コードを internal にしておきながら、テストプロジェクトからだけアクセスできるようにする仕組みが [assembly: InternalsVisibleTo] 属性です。
// ── MyLibrary プロジェクトの AssemblyInfo.cs または Program.cs ──
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("MyLibrary.Tests")]
// ── MyLibrary プロジェクト ──
internal class OrderValidator
{
internal bool Validate(string status)
=> status is "Pending" or "Shipped" or "Cancelled";
}
// ── MyLibrary.Tests プロジェクト(別アセンブリ)──
// InternalsVisibleTo により internal クラスにアクセスできる
// [Fact]
// public void Validate_ValidStatus_ReturnsTrue()
// {
// var v = new OrderValidator(); // OK: InternalsVisibleTo のおかげでアクセス可能
// Assert.True(v.Validate("Pending"));
// }
InternalsVisibleTo を使うと「外部 API は public だけど、テストは内部実装も検証できる」設計が実現できます。ただし、ユニットテストでは内部実装より外部から見えるふるまい(public API)を検証する方が、リファクタリングに強いテストになります。よくある落とし穴と注意点
クラスを public にしすぎる
何も考えずにクラスを public にしてしまうと、ライブラリの内部実装が外部に漏れます。外部に公開しないクラスは internal にしておくことで、後から自由にリファクタリングできます(外部の利用者に影響しないため)。
protected フィールドで子クラスが親の内部状態を壊す
protected フィールドを直接子クラスから書き換えられると、親クラスの不変条件が守られなくなります。フィールドは private にして、子クラスには protected なプロパティ(get のみか protected set)や protected メソッドを通じてアクセスさせましょう。
private set と init の違いを混同する
private set はクラス内部のどのメソッドからでも変更できます。一方 init はオブジェクト初期化子(new X { Prop = val })でのみ設定でき、コンストラクタ完了後は変更不可です。「オブジェクト生成後は絶対に変えたくない」なら init、「クラス内のメソッドは変えてよい」なら private set を選びます。
internal と public を混在させてアセンブリ境界が曖昧になる
public クラスのメソッドが internal 型を引数や戻り値に使うと、コンパイルエラーまたは実行時に問題が起きます(外部からそのメソッドを呼んでも引数の型を解決できない)。public なメンバーは引数・戻り値もすべて public にする必要があります。
よくある質問
protected internal は「protected または internal」(どちらか一方でもOK)。private protected は「protected かつ internal」(両方満たす必要あり)。結果として private protected の方が制限が強く、アクセスできる範囲が狭くなります。まとめ
| 修飾子 | アクセス可能範囲 | 主な用途 |
|---|---|---|
public |
どこからでも | 外部公開 API・インターフェース実装 |
private |
同じクラス内のみ | 内部フィールド・補助メソッド(デフォルト) |
protected |
同クラス+派生クラス | 継承クラスに公開する共通処理・フック |
internal |
同一アセンブリ内 | ライブラリ内部実装・非公開クラス(クラスのデフォルト) |
protected internal |
派生クラス OR 同一アセンブリ | フレームワーク拡張ポイント(OR 条件) |
private protected |
派生クラス AND 同一アセンブリ | ライブラリ内部の継承専用フック(AND 条件) |
プロパティとフィールドの詳細はプロパティとフィールドの違いを、継承での protected 活用は継承とオーバーライド完全ガイドを参照してください。