【C#】カプセル化とアクセス修飾子完全ガイド|public・private・protected・internal・非対称プロパティ・設計原則まで

カプセル化は「必要最小限だけを外部に公開し、内部実装を隠す」というオブジェクト指向の根幹です。C# には publicprivateprotected の3種だけでなく、internalprotected internalprivate 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 の使用例
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 です。

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 ― 同クラスと派生クラス

クラス外部からはアクセスできませんが、継承した子クラスからは使えます。基底クラスが持つ共通データや補助メソッドを子クラスだけに公開したいときに使います。

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 です。

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 なのでアクセス可能
ライブラリ設計では「外部 API は public、内部実装は internal」という分離が重要です。公開する必要のないクラスを internal にしておくと、後からリファクタリングしやすくなります(利用者に影響なく内部を変更できる)。

protected internal ― 派生クラス OR 同一アセンブリ

protectedinternal和集合(OR)です。同一アセンブリ内のどのクラスからでも、または別アセンブリでも継承した派生クラスからアクセスできます。

protected internal の使用例
// ── 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 で追加された最も制限の強い複合修飾子です。protectedinternal積集合(AND)で、「同一アセンブリ内の派生クラスのみ」アクセスできます。

private protected の使用例
// ── 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
名前空間 適用なし 名前空間にアクセス修飾子は付けられない

プロパティの非対称アクセス設定

プロパティの getset(または 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 フィールドはコンストラクタで設定後は変更できない、最も安全なフィールド定義です。不変オブジェクト(イミュータブル)設計の基本です。

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] 属性です。

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 にする必要があります。

よくある質問

Q全部 public にすれば便利じゃないですか?
A短期的には楽ですが、長期的に問題が起きます。① 使う側が内部状態を誤った方法で操作できてしまう ② 後から制限を厳しくすると利用者のコードが壊れる ③ テストで「想定外の操作」を防げない。最初から「使ってほしい部分だけ public」にしておく方が、後の変更コストが大幅に下がります。
Qinternal と public の使い分け基準は?
Aそのクラスや型を「プロジェクト(アセンブリ)の外側から使うことがあるか」で判断します。単一プロジェクトのアプリなら実質 public と internal の差はあまりありませんが、ライブラリを作る場合は「外部 API = public」「内部実装 = internal」を徹底します。判断に迷ったらまず internal にしておき、必要になったときに public に格上げします。
Qprotected internal と private protected の違いが覚えられません
A「OR か AND か」で覚えましょう。protected internal は「protected または internal」(どちらか一方でもOK)。private protected は「protected かつ internal」(両方満たす必要あり)。結果として private protected の方が制限が強く、アクセスできる範囲が狭くなります。
Qインターフェースのメンバーに private は使えますか?
AC# 8 以降、インターフェースに private メンバーを宣言できます(ただしデフォルト実装を持つメンバーを内部で補助する private メソッドに限ります)。インターフェースの通常のメンバー宣言は public が基本で、アクセス修飾子を書くとコンパイルエラーになります(暗黙的に public)。
Qフィールドに public を付けてはいけないのはなぜですか?
A① バリデーション(値のチェック)ができない ② 後からプロパティに変えると、フィールドに直接代入していた利用者のコードがコンパイルエラーになる ③ データバインディングやシリアライザがプロパティを前提にしていることが多い。フィールドは常に private(または private readonly)にして、外部公開は必ずプロパティ経由にするのが C# のベストプラクティスです。詳しくはプロパティとフィールドの違いを参照してください。

まとめ

修飾子 アクセス可能範囲 主な用途
public どこからでも 外部公開 API・インターフェース実装
private 同じクラス内のみ 内部フィールド・補助メソッド(デフォルト)
protected 同クラス+派生クラス 継承クラスに公開する共通処理・フック
internal 同一アセンブリ内 ライブラリ内部実装・非公開クラス(クラスのデフォルト)
protected internal 派生クラス OR 同一アセンブリ フレームワーク拡張ポイント(OR 条件)
private protected 派生クラス AND 同一アセンブリ ライブラリ内部の継承専用フック(AND 条件)

プロパティとフィールドの詳細はプロパティとフィールドの違いを、継承での protected 活用は継承とオーバーライド完全ガイドを参照してください。