C#を用いたソフトウェア開発において、コードの保守性と再利用性を高めるためには、適切なカプセル化が欠かせません。

オブジェクト指向プログラミングの原則に基づき、クラスやメソッドの公開範囲を最小限に抑えることは、予期せぬ依存関係を防ぎ、システムの堅牢性を確保する上で極めて重要です。

その中心的な役割を担うのが「アクセス修飾子」であり、中でもinternal修飾子は、同一アセンブリ内でのみアクセスを許可するという、コンポーネント開発において非常に強力な機能を提供します。

大規模なプロジェクトやライブラリ開発において、すべての型をpublicで定義してしまうと、外部の利用者が内部実装の詳細に依存してしまい、将来的なコードの変更が困難になる「硬直化」を招きます。

本記事では、C#のinternal修飾子の基本的な使い方から、アセンブリの概念、ユニットテストでの活用方法、そして実務における設計指針まで、プロフェッショナルな視点で詳しく解説します。

internal修飾子とは:アセンブリ単位のアクセス制御

C#におけるinternalは、「同じアセンブリ(プログラムの実行単位)内に限ってアクセスを許可する」ことを示すアクセス修飾子です。

通常、クラスや構造体、インターフェースを定義する際に何も修飾子を指定しない場合、その型はデフォルトでinternalとして扱われます(名前空間直下の型の場合)。

これは、不用意に外部へ機能を公開しないという「最小権限の原則」に基づいた仕様です。

アセンブリの定義

ここで言う「アセンブリ」とは、.NETにおけるビルドの結果生成されるファイル(.exe または .dll)を指します。

1つのソリューション内に複数のプロジェクトが存在する場合、それぞれのプロジェクトが別々のアセンブリとしてビルドされます。

同一プロジェクト内のファイル間であれば、ファイルが分かれていても自由にアクセス可能ですが、参照設定を追加して別のプロジェクトから利用しようとしても、internalで定義された要素は見えません。

この境界線が、ライブラリの内部実装を隠蔽し、クリーンな公開APIを維持するための鍵となります。

他のアクセス修飾子との比較

C#には複数のアクセス修飾子が存在します。

それぞれの公開範囲を整理したのが以下の表です。

修飾子公開範囲の説明適用範囲の広さ
public制限なし。どこからでもアクセス可能。最も広い
protected internal同一アセンブリ内、または別アセンブリの派生クラスからアクセス可能。広い
internal同一アセンブリ内からのみアクセス可能。中間
protected同一クラス内、または派生クラスからアクセス可能。狭い
private protected同一アセンブリ内の派生クラスからのみアクセス可能。より狭い
private同一クラス(または構造体)内からのみアクセス可能。最も狭い

このように、internalは「プロジェクト内部では共有したいが、利用者には見せたくない」という絶妙な公開範囲をカバーしています。

internal修飾子の具体的な使い方

それでは、コード例を用いてinternalの使い方を確認していきましょう。

まずは、クラスそのものをinternalにする場合です。

クラス定義での使用例

以下の例では、ライブラリ内部でのみ使用する計算ロジックをinternalクラスとして定義しています。

C#
using System;

namespace MyLibrary
{
    // 同一アセンブリ内でのみ参照可能なクラス
    internal class InternalCalculator
    {
        // internalクラス内のメソッド。アクセス修飾子を省略(またはinternal)にすると
        // 同一アセンブリ内から呼び出せる
        internal int Add(int a, int b)
        {
            return a + b;
        }

        // internalクラスの中にpublicメソッドを定義することも可能だが、
        // クラス自体が非公開なので、実質的に外部からは見えない
        public int Subtract(int a, int b)
        {
            return a - b;
        }
    }

    public class PublicService
    {
        public void Execute()
        {
            // 同じアセンブリ内なので、InternalCalculatorを利用できる
            var calc = new InternalCalculator();
            int result = calc.Add(10, 5);
            Console.WriteLine($"Result: {result}");
        }
    }
}

このコードにおいて、PublicServicepublicであるため、外部のプロジェクトから参照・利用が可能です。

しかし、InternalCalculatorinternalであるため、外部プロジェクトのコードで new InternalCalculator() と記述するとコンパイルエラーになります。

メンバに対する使用例

型(クラス等)はpublicであっても、その中の特定のプロパティやメソッドだけをinternalに制限することも可能です。

C#
namespace MyLibrary
{
    public class UserProfile
    {
        // 外部から自由に読み書きできる
        public string Name { get; set; }

        // 外部からは読み取れるが、書き込みは同一アセンブリ内限定
        public int Id { get; internal set; }

        // 同一アセンブリ内でのみ使用する初期化メソッド
        internal void InitializeInternalState()
        {
            // 内部的なセットアップ処理
        }
    }
}

上記の Id プロパティのように、「取得はどこからでもできるが、値の更新はライブラリ内部からしか行わせたくない」というケースは実務で非常に頻出します。

これにより、外部ユーザーによる不適切なデータ操作を防ぎ、オブジェクトの状態を安全に保つことができます。

実務での活用シーン:なぜinternalが必要なのか

internalを使いこなすことは、ソフトウェアアーキテクチャの品質を一段階引き上げることに繋がります。

ここでは、具体的な3つの活用シーンを紹介します。

1. 公開APIの肥大化を防ぐ

優れたライブラリやモジュールは、利用者に対して最小限の「窓口(インターフェース)」のみを提供します。

内部的な補助クラスや、データ変換のためのヘルパー関数をすべてpublicにしてしまうと、利用者は「どのクラスを使えばよいのか」と迷ってしまいます。

「外部に見せるべき機能」と「内部で完結する機能」を明確に分離し、内部実装をinternalに閉じ込めることで、メンテナンス性の高いクリーンなAPIを設計できます。

2. レイヤードアーキテクチャにおける依存性制御

大規模開発で採用されるレイヤードアーキテクチャにおいて、各レイヤーをプロジェクト(アセンブリ)ごとに分割することがあります。

例えば、「Infrastructure(インフラ)層」で定義したDBアクセスのための具象クラスをinternalとし、DI(依存性の注入)を介して「Domain(ドメイン)層」のインターフェースとして公開する場合です。

これにより、上位レイヤーのプログラマが誤って具象クラスを直接インスタンス化して使用することを物理的に禁止でき、疎結合な設計を強制することができます。

3. バージョンアップ時の破壊的変更の回避

一度publicとして公開した型やメソッドは、それを参照している外部ユーザーがいる限り、シグネチャの変更や削除が困難になります。

これを変更すると「破壊的変更」となり、利用者のコードを壊してしまうからです。

一方で、internalな要素であれば、その変更の影響範囲は自アセンブリ内に限定されます。

リファクタリングの際、外部を気にせず自由に内部構造を作り変えることができるのは、長期的なプロジェクト運営において計り知れないメリットとなります。

特殊なケース:InternalsVisibleTo によるテストの容易化

internalを利用する際の最大の懸念点は、「ユニットテストが書きにくくなる」ことです。

テストプロジェクトは通常、対象のプロジェクトとは別のアセンブリになるため、そのままではinternalなメンバにアクセスできません。

この問題をスマートに解決するのが InternalsVisibleToAttribute です。

属性の設定方法

プロジェクトファイル(.csproj)またはソースコード内で以下のように設定することで、特定の別アセンブリ(テスト用アセンブリなど)に対してのみ、internalなメンバを公開することができます。

.csproj での設定例(推奨)

最近の .NET Core / .NET 5 以降では、プロジェクトファイルに記述するのが一般的です。

XML
<ItemGroup>
  <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
    <_Parameter1>MyProject.Tests</_Parameter1>
  </AssemblyAttribute>
</ItemGroup>

C#コード(AssemblyInfo.cs 等)での設定例

C#
using System.Runtime.CompilerServices;

// "MyProject.Tests" という名前のアセンブリに対して internal メンバを公開する
[assembly: InternalsVisibleTo("MyProject.Tests")]

この設定を行うことで、製品コードの隠蔽性は保ったまま、テストコードからは全てのロジックを直接検証できるようになります。

リフレクションを使って無理やりアクセスするようなトリッキーな手法を避けることができるため、非常に安全です。

発展的なアクセス修飾:組み合わせによる制御

C#では、internalを他の修飾子と組み合わせることで、より細かい制御が可能です。

特に重要な2つの組み合わせについて解説します。

protected internal

これは「protected または internal」という意味です。

つまり、同一アセンブリ内であれば誰でもアクセスでき、別アセンブリであってもそのクラスを継承した派生クラスからであればアクセスを許可します。

フレームワーク開発において、「基本的にはライブラリ内部で使うが、ユーザーが機能を拡張(継承)する際にも使わせてあげたい」というプロパティなどに利用されます。

private protected

これは「protected かつ internal」という意味です(C# 7.2から導入)。

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

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

「継承による再利用を認めつつ、その継承関係を同一プロジェクト内に閉じ込めたい」という、非常に厳密なカプセル化を行いたい場合に非常に有用です。

実装時の注意点とアンチパターン

internalは便利ですが、誤った使い方をすると設計の混乱を招きます。

1. publicな型からinternalな型を公開できない

C#のコンパイラは、アクセス可能性の不一致を厳しくチェックします。

例えば、publicなメソッドの戻り値として、internalなクラスを指定することはできません。

C#
internal class InternalData { }

public class PublicApi
{
    // コンパイルエラー!
    // 'PublicApi.GetData()' の戻り値の型 'InternalData' は
    // 'PublicApi.GetData()' よりもアクセスしにくいです。
    public InternalData GetData() => new InternalData();
}

外部に公開される窓口(public)のシグネチャには、必ず同じかそれ以上の公開範囲を持つ型を使用する必要があります。

2. インターフェース実装における注意

インターフェースを明示的に実装する場合、そのメソッドのアクセス修飾子は指定できませんが、インターフェース自体がinternalであれば、その実装はアセンブリ外からは利用できなくなります。

抽象化のレベルと公開範囲のバランスを常に意識することが重要です。

まとめ

C#のinternal修飾子は、単なる「隠蔽」のための道具ではなく、アセンブリという明確な境界線を用いてソフトウェアの構造を整理するための戦略的ツールです。

本記事で解説した内容を振り返ります。

  • 定義:同一アセンブリ内(プロジェクト内)でのみアクセス可能な範囲を定義する。
  • メリット:公開APIの複雑化を防ぎ、ライブラリ内部のリファクタリングを容易にする。
  • テストInternalsVisibleTo を活用することで、隠蔽とテスト容易性を両立できる。
  • 応用private protected などの複合修飾子でさらに緻密な設計が可能。

モダンなC#開発において、何でもpublicにする習慣を捨て、まずはinternalから検討を始めるというスタンスは、堅牢なシステムを構築する第一歩となります。

アセンブリの境界を意識したコード設計を心がけ、変更に強く、利用者にとって使いやすいプログラムを目指しましょう。