C#を用いたシステム開発において、クラスやそのメンバーの「公開範囲」を適切に制御することは、堅牢なアプリケーションを構築する上で最も重要な要素の一つです。

オブジェクト指向プログラミングの四大要素の一つである「カプセル化」を実現するためには、アクセス修飾子の正しい理解が欠かせません。

本記事では、C#で利用可能なアクセス修飾子の全パターンを網羅し、それぞれのスコープ(有効範囲)や具体的な活用シーンを詳しく解説します。

C# 11で導入されたfile修飾子を含む最新の仕様に基づき、設計時に迷いやすい「どの修飾子を選ぶべきか」という判断基準についても深く掘り下げていきます。

C#のアクセス修飾子とは

アクセス修飾子とは、クラス、構造体、インターフェース、およびそれらのメンバー(フィールド、プロパティ、メソッドなど)に対するアクセス許可の範囲を定義するキーワードです。

ソフトウェアの開発規模が大きくなるにつれ、すべてのデータがどこからでも書き換えられる状態(グローバルな状態)は、バグの温床となります。

アクセス修飾子を適切に設定することで、内部的な実装詳細を隠蔽し、外部には必要なインターフェースのみを公開する「情報の隠蔽」が可能になります。

これにより、コードの修正が他の部分に与える影響を最小限に抑え、メンテナンス性の高いプログラムを実現できます。

アクセス修飾子の全8パターンとスコープの定義

C#には、基本となる修飾子とそれらを組み合わせたもの、そして特殊な用途で使用されるものを合わせて、主に8つのアクセスパターンが存在します。

これらを正確に使い分けるためには、「アセンブリ(プロジェクト)」「継承関係」という2つの軸で範囲を理解する必要があります。

1. public(すべての場所からアクセス可能)

publicは最も制限が緩い修飾子です。

同一アセンブリ内はもちろん、そのアセンブリを参照している外部のアセンブリからもアクセスが可能です。

ライブラリとして提供するAPIや、システムの構成要素として広く利用されるクラスに使用します。

2. private(同一クラス内のみ)

privateは最も制限が厳しい修飾子です。

定義されたクラスまたは構造体の内部からのみアクセスできます。

クラスの内部状態を保持するフィールドなどは、原則としてprivateに設定し、必要に応じてプロパティ経由で公開するのが基本原則です。

3. protected(同一クラスおよび派生クラス内)

protectedは、そのクラス自身と、そのクラスを継承した「派生クラス」からアクセスを許可します。

アセンブリが異なっていても、継承関係があればアクセス可能です。

フレームワークの基底クラスなどで、子クラスにのみカスタマイズを許容したい場合に利用します。

4. internal(同一アセンブリ内のみ)

internalは、同じプロジェクト(コンパイル単位であるアセンブリ)内であればどこからでもアクセスできますが、外部のプロジェクトからは参照できません。

コンポーネント内部でのみ共有したいユーティリティクラスなどに適しています。

5. protected internal(同一アセンブリ または 派生クラス)

protected internalは、protectedinternalの「和集合(OR)」です。

同じアセンブリ内のすべてのコード、および別アセンブリであっても派生クラスであればアクセス可能です。

6. private protected(同一アセンブリ かつ 派生クラス)

private protectedは、C# 7.2で追加された修飾子で、privateprotectedの「積集合(AND)」のような動作をします。

同一アセンブリ内の派生クラスからのみアクセスを許可します。

別アセンブリの派生クラスからはアクセスできません。

7. file(同一ファイル内のみ)

C# 11から導入された比較的新しい修飾子です。

file修飾子を付けた型は、定義されたソースファイル内からのみ可視となります。

主にソースジェネレーター(コード自動生成機能)において、名前の衝突を避けるために利用されます。

8. 省略時のデフォルト動作(暗黙の修飾子)

明示的に修飾子を記述しなかった場合、コンテキストに応じてデフォルトのアクセスレベルが適用されます。

  • クラスや構造体のメンバー(メソッドやフィールド):private
  • クラスや構造体(トップレベル):internal
  • インターフェースのメンバー:public (C# 8.0以降は明示的な指定も可能)

アクセス範囲の比較一覧表

各修飾子のアクセス可否を整理すると、以下の表のようになります。

アクセス修飾子同一クラス同一アセンブリの派生クラス同一アセンブリの非派生クラス別アセンブリの派生クラス別アセンブリの非派生クラス
public
protected internal×
internal××
protected××
private protected×××
private××××
file(※1)(※1)(※1)××

(※1) 同一ファイル内であればアクセス可能。

実践的なコード例と解説

ここでは、各アクセス修飾子がどのように動作するのか、具体的なコードを用いて解説します。

基本的なアクセス制限の挙動

以下のプログラムでは、異なるアクセスレベルを持つメンバーを持つクラスを定義し、外部からのアクセス可否を確認します。

C#
using System;

namespace AccessModifierDemo
{
    public class BaseClass
    {
        public string PublicField = "公開フィールド";
        private string _privateField = "非公開フィールド";
        protected string ProtectedField = "保護フィールド";
        internal string InternalField = "アセンブリ内公開フィールド";

        public void ShowPrivate()
        {
            // 同一クラス内なのでprivateにもアクセス可能
            Console.WriteLine($"内部アクセス: {_privateField}");
        }
    }

    public class DerivedClass : BaseClass
    {
        public void TestAccess()
        {
            // 継承先なのでpublic, protected, internalにアクセス可能
            Console.WriteLine(PublicField);
            Console.WriteLine(ProtectedField);
            Console.WriteLine(InternalField);

            // privateはエラーになるためコメントアウト
            // Console.WriteLine(_privateField); 
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var baseObj = new BaseClass();
            
            // 外部からのアクセス
            Console.WriteLine(baseObj.PublicField);   // OK
            Console.WriteLine(baseObj.InternalField); // 同一アセンブリならOK
            
            // baseObj.ProtectedField; // コンパイルエラー(継承関係がない)
            // baseObj._privateField;  // コンパイルエラー
            
            baseObj.ShowPrivate(); // メソッド経由なら間接的に利用可能
        }
    }
}
実行結果
公開フィールド
アセンブリ内公開フィールド
内部アクセス: 非公開フィールド

複雑な組み合わせ:private protected と protected internal

アセンブリを跨ぐ場合の挙動の違いは、大規模開発において非常に重要です。

以下の例は、Library.dllApp.exe という2つのプロジェクトがある想定のコードです。

C#
// Project: Library (AssemblyA)
namespace Library
{
    public class AccessCore
    {
        // 同一アセンブリ内、または別アセンブリの派生クラス
        protected internal string ProtectedInternalMsg = "Protected Internal";

        // 同一アセンブリ内の派生クラスのみ
        private protected string PrivateProtectedMsg = "Private Protected";
    }

    public class InternalDerived : AccessCore
    {
        public void Check()
        {
            // 同一アセンブリ内なので、両方アクセス可能
            Console.WriteLine(ProtectedInternalMsg);
            Console.WriteLine(PrivateProtectedMsg);
        }
    }
}

// Project: App (AssemblyB) - Libraryを参照
namespace App
{
    using Library;

    public class RemoteDerived : AccessCore
    {
        public void Check()
        {
            // 別アセンブリの派生クラスなので、protected internalはOK
            Console.WriteLine(ProtectedInternalMsg);

            // private protectedは「同一アセンブリ内」という制約があるためNG
            // Console.WriteLine(PrivateProtectedMsg); // コンパイルエラー
        }
    }
}

private protectedは、ライブラリの作者が「このクラスの内部実装を助けるために子クラスを作ってほしいが、それは自分たちのプロジェクト内に限定したい(外部のユーザーに継承して内部を触られたくない)」という場合に非常に有効な手段となります。

最新機能:file 修飾子の活用

C# 11で導入されたfileアクセス修飾子は、型のスコープを「ファイル単位」に限定します。

これは従来のprivate class(入れ子クラス)とは異なり、トップレベルの型として定義できるのが特徴です。

C#
// FileA.cs
namespace MyNamespace;

// このファイル内だけで見えるクラス
file class InternalHelper
{
    public void Log(string message) => Console.WriteLine($"[Log] {message}");
}

public class MainService
{
    public void Execute()
    {
        var helper = new InternalHelper();
        helper.Log("メインサービス実行中");
    }
}

// FileB.cs
namespace MyNamespace;

public class AnotherService
{
    public void Run()
    {
        // InternalHelper helper = new InternalHelper(); // コンパイルエラー
        // FileA.csで定義されたfileクラスはここでは見えない
    }
}

この機能は、特にSource Generatorsで威力を発揮します。

自動生成されたコードが、ユーザーが作成した既存のクラス名と衝突するリスクを回避しつつ、特定のファイル内だけで完結するロジックを安全に実装できるためです。

アクセス修飾子を選ぶ際の設計指針

「どのアクセス修飾子を使うべきか」迷った際は、以下の優先順位と原則に従うことを推奨します。

1. 最小権限の原則(Principle of Least Privilege)

アクセス範囲は、可能な限り狭く設定するのが鉄則です。

最初はすべてのメンバーをprivateにし、クラス外から呼ぶ必要があるものだけを順次publicinternalに昇格させていくアプローチが最も安全です。

最初から広く公開してしまうと、後から制限をかける際に影響範囲が広がり、リファクタリングが困難になります。

2. internal の積極的な活用

多くの開発者が、ついついすべてのクラスをpublicにしてしまいがちです。

しかし、同一ソリューション内の特定のプロジェクトでしか使わないクラスは、internalにするべきです。

これにより、IntelliSense(コード補完)に不要な候補が出るのを防ぎ、外部アセンブリとの結合度を下げることができます。

テストプロジェクトからinternalメンバーにアクセスしたい場合は、InternalsVisibleToAttributeを使用することで、カプセル化を維持したままユニットテストを行うことが可能です。

3. 継承のための設計(protected)

protectedを付与するということは、「このクラスは継承されることを前提としている」というメッセージになります。

継承はクラス間の結合度が非常に強くなるため、本当に継承が必要な箇所に限定して使用しましょう。

継承を許可したくない場合は、クラス自体にsealed修飾子を付けることも検討してください。

4. インターフェースとアクセス修飾子

C# 8.0以降、インターフェース内でもprivateprotectedなメソッド(デフォルト実装を持つもの)を定義できるようになりました。

これにより、インターフェース内でのみ使用される補助的なロジックを共通化することが可能になっています。

ただし、インターフェースの本来の目的は「公開契約」であるため、複雑なアクセス制御を持ち込む際は注意が必要です。

カプセル化を支えるプロパティのアクセス制御

フィールドをprivateにし、プロパティを通じて公開するのがC#の標準的なスタイルですが、プロパティ自体にも個別のアクセス修飾子を設定できます。

C#
public class UserProfile
{
    // 読み取りはどこからでも可能だが、書き換えはクラス内のみ
    public string UserName { get; private set; }

    // 初期化時(コンストラクタまたは初期化子)のみ設定可能
    public int UserId { get; init; }

    public UserProfile(int id, string name)
    {
        UserId = id;
        UserName = name;
    }
}

このように、getsetで異なるアクセス権を与えることで、「外部からは見えるが変更はさせない」という読み取り専用の性質を簡単に持たせることができます。

これは、バグの少ないクリーンな設計において非常に強力なツールとなります。

まとめ

C#のアクセス修飾子は、単に「見える・見えない」を決めるだけのものではなく、クラス間の依存関係を整理し、システムの「壊れにくさ」を決定づける重要な設計要素です。

本記事で紹介した全8パターンの要点を振り返ります。

  • public / private / protected:基本の3要素。
  • internal:アセンブリ単位の制御に必須。
  • protected internal / private protected:継承とアセンブリを組み合わせた高度な制御。
  • file:最新のファイル単位スコープ。
  • Default:記述を省略した際の暗黙的な挙動。

開発時には常に「この情報は本当に外部に公開する必要があるか?」を自問自答し、最も制限の強い修飾子を選択する習慣をつけましょう。

適切にカプセル化されたコードは、変更に強く、他の開発者にとっても理解しやすい優れた資産となります。

最新のC#の機能を活用し、より洗練されたオブジェクト指向設計を目指してください。