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)でした。
何も書かないとき:既定のアクセスレベル
リフレクションで既定値を読み出しました。
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 と表示されます。