C#のアクセス修飾子6種類とfile、this:2つのアセンブリで検証

C#のアクセスレベル6種類とfile(C# 11)を2つのアセンブリで実際にコンパイルし、エラーコードまで確認しました。既定のアクセスレベル、protected internalの誤解、thisの3つの使い道。

2026年11月改訂: この記事は2021年に韓国語で書いた講座を、日本語向けに書き直したものです。サンプルを .NET 10.0.14(C# 14)でコンパイルし直し、3点を直しました。①当時の表の protected internal の説明が誤りでした(「同じアセンブリ内だけ protected」ではなく「同じアセンブリ または 派生クラス」)。②アクセスレベルは5つではなく、C# 7.2 の private protected を加えた6つで、C# 11 では型専用の file も入りました。③当時載せたエラー出力には個人PCのユーザーフォルダーのパスが含まれていたため削除し、新しくコンパイルした出力に差し替えました。

public と private は分かるけれど protected internal と private protected の違いがあいまいな人、this の使い道を一度整理しておきたい人向けの記事です。先に結論を書くと、メンバーは何も書かなければ private、トップレベルのクラスは何も書かなければ internal で、広げるのは外から本当に必要になったときだけです。アクセスレベルの表は覚え間違えやすいので、今回はクラスライブラリと、それを参照するコンソールアプリの2つのプロジェクトを作り、すべての組み合わせをコンパイラーに聞いてみました。

2021年のサンプルをコンパイルし直す

元の記事は、修飾子を付けていないフィールドにクラスの外からアクセスする例から始まります。

class A {
    int B;
    int C;
}
// Main の中で
A a = new A();
a.B = 1;
Program.cs(12,15): error CS0122: 'A.B' はアクセスできない保護レベルになっています
Program.cs(3,13): warning CS0169: フィールド 'A.B' は使用されていません

修飾子のないメンバーは private なので、クラスの外からは見えません。当時の説明は正しいです。public int B; にすればエラーは消えます。

MSDN(現在の Microsoft Learn)から引用した protected の例も、そのまま再現しました。

class X { protected int x = 123; }
class Y : X {
    static void Main() {
        X xa = new X();
        Y yb = new Y();
        xa.x = 10;   // エラー
        yb.x = 10;   // OK
    }
}
Program.cs(15,16): error CS1540: 'X' 型の修飾子をとおしてプロテクト メンバー 'X.x' にアクセスすることはできません。修飾子は 'Y' 型、またはそれから派生したものでなければなりません

見落としやすいのは、Y は X を継承しているのに xa.x は通らない点です。protected が許すのは「派生クラスが自分自身(または自分の派生型)を通して継承したメンバーに触る」ことだけで、ほかの X オブジェクトの中身をのぞくことは許しません。

6つのアクセスレベルと file:2つのアセンブリでの結果

クラスライブラリ(AccLib)にアクセスレベルごとにフィールドを1つずつ持つ Base を作り、同じアセンブリの派生クラス・非派生クラス、そしてこのライブラリを参照するコンソールアプリの派生クラス・トップレベルのコードから、それぞれアクセスしてみました。

namespace AccLib;
public class Base
{
    public int Pub = 1;
    protected internal int ProtInt = 2;
    internal int Int = 3;
    protected int Prot = 4;
    private protected int PrivProt = 5;
    private int Priv = 6;
    int NoModifier = 7;   // 既定:private
}

結果です。✓ はコンパイル成功、それ以外のマスは実際に出たエラーコードです。

修飾子 同じクラス 同じアセンブリ・派生 同じアセンブリ・非派生 別アセンブリ・派生 別アセンブリ・非派生
public ✓ ✓ ✓ ✓ ✓
protected internal ✓ ✓ ✓ ✓ CS0122
internal ✓ ✓ ✓ CS0103 CS1061
protected ✓ ✓ CS0122 ✓ CS0122
private protected(C# 7.2) ✓ ✓ CS0122 CS0103 CS1061
private ✓ CS0122 CS0122 CS0103 CS1061

2021年版で直すべきは protected internal の行です。当時は「同じアセンブリでのみ protected としてアクセス可能」と書いていましたが、4列目のとおり別アセンブリの派生クラスからも使えます。名前どおり protected または internal です。「自分のアセンブリ内の派生クラスだけ」という AND の条件は private protected の役目です。

もう1つ面白いのは、別アセンブリから internal・private protected・private のメンバーを触ったとき、CS0122(保護レベル)ではなく CS0103/CS1061(名前が存在しない)が出ることです。別アセンブリからは、これらのメンバーは最初から存在しないものとして扱われます。エラーだけ見るとタイプミスを疑ってしまいがちなポイントです。

Program.cs(16,13): error CS0103: 現在のコンテキストに 'Int' という名前は存在しません
Program.cs(5,7): error CS0122: 'Base.ProtInt' はアクセスできない保護レベルになっています

単体テストのプロジェクトから internal メンバーを見たいなら、メンバーを広げるのではなく、ライブラリに [assembly: InternalsVisibleTo("テストプロジェクト名")] を付けます。コンソールアプリの名前を指定すると b.Int が読めるようになることを確認しました。

file ローカル型(C# 11)

file はメンバーではなく型に付ける修飾子で、その型を宣言したソースファイルの中だけで使えるようにします。主にソースジェネレーターが出力するヘルパークラス同士の名前衝突を防ぐために入りました。

// Helper.cs
file static class Helper { public static int Twice(int x) => x * 2; }
static class UsesHelper { public static int Run() => Helper.Twice(21); }   // 同じファイル:OK、42

Program.cs から Helper.Twice(21) を呼ぶと error CS0103(現在のコンテキストに 'Helper' という名前は存在しません)です。メンバーに file int z; と付けると error CS0106(修飾子 'file' がこの項目に対して有効ではありません)、public メソッドのシグネチャーに file 型を使うと error CS9051 になります。

バージョンも確かめました。LangVersion 7.1 では private protected が CS8302(… 7.2 or greater)、LangVersion 10 では file class が CS8936(Feature 'file types' … 11.0 or greater)でした。

中央に「誰がアクセスするか」という問いがあり、周りに7つの答えがある図。private はこのクラスだけ、file はこのソースファイルだけ(型のみ)、private protected は同じアセンブリの派生クラス、protected はどのアセンブリでも派生クラス、internal は同じアセンブリのコード、protected internal は同じアセンブリか、どこかの派生クラス、public は誰でも。
C# の7つのアクセス修飾子を狭い順に。まず private にして、外から本当に必要になったときだけ広げる。file は型にだけ付けられる。

何も書かないとき:既定のアクセスレベル

リフレクションで既定値を読み出しました。

class Top           : internal
Top.field           : private
Top.Method()        : private
Top.Nested          : private
Top() ctor          : public
IShape.Area()       : public
struct S.field      : private
abstract Abs() ctor : protected
  • 名前空間の直下の型:internal
  • クラス・構造体のメンバーと入れ子の型:private
  • インターフェイスのメンバー:public
  • コンパイラーが自動で作る既定のコンストラクター:通常は public、abstract クラスなら protected

この既定値のせいでよく出会うのが CS0050 です。修飾子なし(=internal)のクラスを public メソッドが返すと、ほかのアセンブリからは戻り値の型が見えないので、コンパイラーが止めます。

error CS0050: Inconsistent accessibility: return type 'Hidden' is less accessible than method 'Api.Get()'

this:自分自身を指す3つの使い道

1. フィールドと引数の名前が同じとき

元の num = num の例を動かし直しました。2021年は出力だけを載せていましたが、いまのコンパイラーは実行前にミスを教えてくれます。

class A {
    private int num;
    public A(int num) { num = num; }
    public void Show() => Console.WriteLine("num: " + num);
}
warning CS0649: フィールド 'A.num' は割り当てられません。常に既定値 0 を使用します
warning CS1717: 同じ変数に代入られました。他の変数に代入しますか?
num: 0

this.num = num; にすると num: 50 と出ます。this.num は「このオブジェクトのフィールド num」、ただの num は一番近いスコープ、つまり引数を指します。この2つの警告を抑止していなければ、このバグは実行前に見つかります。

2. コンストラクターの連鎖 : this(...)

同じクラスの別のコンストラクターを先に呼び、初期化の処理を1か所にまとめます。

class Point {
    public int X { get; } public int Y { get; }
    public Point() : this(0, 0) { Console.WriteLine("Point()"); }
    public Point(int xy) : this(xy, xy) { Console.WriteLine("Point(int)"); }
    public Point(int x, int y) { X = x; Y = y; Console.WriteLine($"Point(int, int) {x},{y}"); }
}
Point(int, int) 0,0
Point()
Point(int, int) 3,3
Point(int)

: this(...) で指定したコンストラクターの本体が先に動き、そのあと自分の本体が動きます。基底クラスやフィールド初期化子、静的コンストラクターまで含めた全体の順序は、コンストラクターとファイナライザーで実際の出力とともに扱います。

3. 拡張メソッドの this 引数と C# 14 の拡張ブロック

静的クラスの静的メソッドの第1引数に this を付けると、その型のインスタンスメソッドのように呼べます。.NET 10 と一緒に出た C# 14 では extension ブロックが入り、拡張プロパティも書けるようになりました。

static class StringExt {
    public static int WordCount(this string s) =>
        s.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
    extension(string s) {
        public bool IsBlank => string.IsNullOrWhiteSpace(s);   // C# 14 の拡張プロパティ
    }
}
// "hello world".WordCount() → 2、"hello world".IsBlank → False

LangVersion を 13 に下げると、extension ブロックは「機能が使えません」という案内すら出ず、構文エラー(CS1022、CS1513)になります。C# 14 専用の文法です。

ほかにも this は、インデクサーの宣言(public string this[int i] { get; set; })や、自分自身を別のオブジェクトに渡すとき(new Node(childName) { Parent = this })に使います。どちらも今回コンパイルして動作を確認しました。

まとめ:実務での選び方

状況 修飾子
まずはここから メンバーは private、型は internal
ライブラリの外に公開する API public
プロジェクト内だけのヘルパー internal
派生クラスに使わせたい・オーバーライドさせたいメンバー protected
自分のアセンブリの派生クラスだけ private protected
テストプロジェクトにも見せたい internal internal + InternalsVisibleTo(protected internal に広げない)
1ファイルの中だけで使う型 file class

クラスそのものの基本(フィールド、プロパティ、required)はC#のクラスにまとめています。

検証環境:.NET SDK 10.0.403/ランタイム 10.0.14、C# 14(net10.0 の既定)、Apple Silicon Mac 上の macOS。

よくある質問

protected internal は「同じアセンブリの中だけで protected」という意味?

違います。protected internal は「同じアセンブリ、または(OR)派生クラス」で、別アセンブリの派生クラスからも使えます。「同じアセンブリの派生クラスだけ(AND)」は C# 7.2 で追加された private protected です。2つのアセンブリで実際にコンパイルして確かめました。

アクセス修飾子を書かないと既定は何になる?

クラスや構造体のメンバーと入れ子の型は private、名前空間の直下に書いたトップレベルのクラスは internal、インターフェイスのメンバーは public です。そのため、修飾子なしのクラスを public メソッドが返すと error CS0050(一貫性のないアクセシビリティ)になります。

this.num = num ではなく num = num と書くとどうなる?

引数に自分自身を代入するだけになり、フィールドは変わりません。いまのコンパイラーは warning CS1717(同じ変数に代入された)と CS0649 を出し、実行すると num: 0 と表示されます。

  • #C#
  • #アクセス修飾子
  • #private protected
  • #file
  • #this

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑