C#の修飾子とは?public・private・staticなど種類と使い方を一覧で解説
はじめに
C#の修飾子は、クラス、メソッド、フィールド、プロパティなどの宣言に付けて、アクセス範囲や継承の可否、値を変更できるかどうかといった性質を指定するキーワードです。
代表的な修飾子には、次のようなものがあります。
アクセス範囲を指定する
public、private、protected、internalインスタンスを作らずに利用できるようにする
static値の変更を制限する
readonly、const継承やオーバーライドを制御する
virtual、override、abstract、sealed非同期処理を表す
asyncクラスの定義を複数ファイルに分割する
partial
修飾子を正しく使うと、意図しない場所からのアクセスや変更を防ぎ、安全で保守しやすいコードを作れます。
この記事では、C#の修飾子の種類と使い方を、一覧表とサンプルコードを交えて解説します。
1. C#の修飾子とは?
1-1. 修飾子はクラス・メソッド・変数などの性質を指定するキーワード
C#の修飾子とは、クラスやメンバーの宣言に追加して、その要素が持つ性質をコンパイラに伝えるキーワードです。
たとえば、次のコードでは複数の修飾子が使われています。
public class User{private readonly int _id;public static int UserCount { get; private set; }public User(int id){_id = id;UserCount++;}
}
それぞれの修飾子には、次の意味があります。
public class User:ほかのコードから利用できるクラスprivate readonly int _id:クラス内部でのみ利用でき、初期化後は変更できないフィールドpublic static int UserCount:インスタンスを作らず、クラス名から利用できるプロパティprivate set:値の変更はクラス内部からだけ許可する
修飾子は単独で使うだけでなく、private static readonlyのように組み合わせて指定することもあります。
1-2. 修飾子を使う目的:アクセス制御・継承制御・共通化・読み取り専用化
C#で修飾子を使う主な目的は、次の4つです。
アクセス制御
publicやprivateを使い、どこから利用できるかを制限します。外部から変更してほしくない処理やデータをprivateにすることで、クラス内部の状態を守れます。
継承制御
virtual、override、abstract、sealedを使い、派生クラスによる処理の上書きや、クラスの継承可否を制御します。
データや処理の共通化
staticを使うと、オブジェクトごとではなく、型全体で共有するフィールドやメソッドを定義できます。
値の変更制限
readonlyやconstを使い、設定後に変更してはいけない値を明示できます。
1-3. 修飾子を理解するとコードの設計意図が読みやすくなる
修飾子は、単にコンパイルを通すための記号ではありません。コードを書いた人の設計意図を表す重要な情報です。
たとえば、次の宣言を見てみましょう。
public sealed class AppSettings{public static readonly string ApplicationName = "Sample App";private AppSettings(){}
}
このコードから、次の設計意図を読み取れます。
public:ほかの場所から利用させたいsealed:派生クラスを作らせたくないstatic readonly:型全体で共有し、初期化後は変更させたくないprivateコンストラクター:外部からインスタンスを作らせたくない
修飾子を理解すると、クラスがどのように使われることを想定しているのかを、宣言だけで判断しやすくなります。
2. C#の修飾子一覧
2-1. アクセス修飾子一覧:public・private・protected・internalなど
C#の主なアクセス修飾子は次のとおりです。
| 修飾子 | アクセスできる範囲 |
|---|---|
public | どこからでもアクセスできる |
private | 宣言した型の内部からのみアクセスできる |
protected | 宣言した型と、その派生型からアクセスできる |
internal | 同じアセンブリ内からアクセスできる |
protected internal | 同じアセンブリ内、または派生型からアクセスできる |
private protected | 同じアセンブリ内にある派生型からアクセスできる |
file | 同じソースファイル内からアクセスできる |
ここでいうアセンブリとは、一般的にはビルドによって作られるDLLや実行ファイルの単位です。
fileはトップレベル型に指定できる修飾子で、その型を同じソースファイルの中だけで利用できるようにします。
file class FileLocalHelper{}2-2. クラスやメンバーの性質を変える修飾子一覧:static・readonly・constなど
| 修飾子 | 主な役割 |
static | インスタンスではなく型に属することを表す |
readonly | フィールドへの代入を宣言時やコンストラクター内などに制限する |
const | コンパイル時に決まる定数を宣言する |
required | オブジェクト作成時に初期化が必要なメンバーを表す |
volatile | フィールドが複数スレッドから変更される可能性をコンパイラに伝える |
ref | 参照による受け渡しや参照戻り値などを表す |
たとえば、requiredを指定したプロパティは、オブジェクト初期化子などで値を設定する必要があります。
public class User{public required string Name { get; init; }}var user = new User{Name = "田中"};
2-3. 継承やオーバーライドに関する修飾子一覧:virtual・override・abstract・sealed
| 修飾子 | 主な役割 |
virtual | 派生クラスで処理を上書きできるようにする |
override | 基底クラスの仮想メンバーや抽象メンバーを上書きする |
abstract | 実装を派生クラスに任せる |
sealed | クラスの継承や、オーバーライドの継続を禁止する |
new | 基底クラスの同名メンバーを隠す |
overrideとnewは似て見えますが、役割が異なります。overrideは基底クラスの処理を多態的に上書きするのに対し、newは同名メンバーを隠します。
2-4. その他の修飾子一覧:async・partial・unsafe・externなど
| 修飾子 | 主な役割 |
async | awaitを使用する非同期メソッドを宣言する |
partial | 型やメンバーの定義を複数の宣言に分割する |
unsafe | ポインターを扱う安全でないコードを許可する |
extern | メソッドの実装が外部にあることを表す |
これらは、特定の目的や開発環境で利用する修飾子です。通常のアプリケーションではasyncやpartialを目にする機会が多く、unsafeやexternはネイティブコードとの連携などで使われます。
3. アクセス修飾子の種類と使い方
3-1. public:どこからでもアクセスできる
publicを指定した型やメンバーは、参照可能な場所であれば、基本的にどこからでもアクセスできます。
public class Calculator{public int Add(int left, int right){return left + right;}}利用側では、次のように呼び出せます。
var calculator = new Calculator();int result = calculator.Add(10, 20);Console.WriteLine(result);
publicは、クラスの利用者に公開する正式な窓口に指定します。ただし、すべてをpublicにすると、外部から自由に状態を変更される可能性があるため注意が必要です。
3-2. private:同じクラス内からのみアクセスできる
privateを指定したメンバーは、そのメンバーを宣言した型の内部からのみアクセスできます。
public class BankAccount{private decimal _balance;public void Deposit(decimal amount){if (amount <= 0){throw new ArgumentOutOfRangeException(nameof(amount));}_balance += amount;}public decimal GetBalance(){return _balance;}
}
_balanceをprivateにすることで、外部から次のように直接変更されることを防げます。
var account = new BankAccount();// コンパイルエラー// account._balance = -10000;
残高の変更をDepositなどのメソッドに限定すれば、不正な値を設定されにくい設計になります。
3-3. protected:同じクラスと派生クラスからアクセスできる
protectedを指定したメンバーは、宣言したクラスと、そのクラスを継承した派生クラスからアクセスできます。
public class Employee{protected decimal Salary { get; set; }public Employee(decimal salary){Salary = salary;}
}
public class Manager : Employee{public Manager(decimal salary) : base(salary){}
public decimal CalculateBonus(){return Salary * 0.2m;}
}
ManagerはEmployeeの派生クラスなので、protectedであるSalaryを利用できます。一方、通常の外部コードからSalaryへ直接アクセスすることはできません。
3-4. internal:同じアセンブリ内からアクセスできる
internalを指定した型やメンバーは、同じアセンブリ内からアクセスできます。
internal class CsvParser{internal string[] Parse(string line){return line.Split(',');}}同じプロジェクトでビルドされるコードからは利用できますが、別のアセンブリからは原則として利用できません。
ライブラリ内部では共通利用したいものの、ライブラリ利用者には公開したくないクラスやメソッドに適しています。
なお、InternalsVisibleTo属性を使うと、指定した別アセンブリにinternalメンバーを公開できます。テストプロジェクトから内部処理を検証するときなどに使われます。
3-5. protected internal:同じアセンブリまたは派生クラスからアクセスできる
protected internalは、protectedとinternalのどちらかの条件を満たせばアクセスできます。
public class BaseService{protected internal void WriteLog(string message){Console.WriteLine(message);}}WriteLogには、次の場所からアクセスできます。
同じアセンブリ内のコード
同じアセンブリ内の派生クラス
別アセンブリにある派生クラス
「同じアセンブリ内」または「派生クラス」という、比較的広いアクセス範囲になります。
3-6. private protected:同じアセンブリ内の派生クラスからアクセスできる
private protectedは、同じアセンブリ内にある派生クラスからアクセスできます。
public class BaseRepository{private protected string ConnectionString { get; }protected BaseRepository(string connectionString){ConnectionString = connectionString;}
}
internal class UserRepository : BaseRepository{public UserRepository(string connectionString): base(connectionString){}
public void Connect(){Console.WriteLine(ConnectionString);}
}
protected internalが「同じアセンブリ、または派生クラス」であるのに対し、private protectedは「同じアセンブリ、かつ派生クラス」です。
4. public・private・protected・internalの違い
4-1. アクセス範囲を比較表で確認
アクセス修飾子の違いを表にすると、次のようになります。
| アクセス元 | public | private | protected | internal | protected internal | private protected |
| 同じクラス | ○ | ○ | ○ | ○ | ○ | ○ |
| 同じアセンブリの派生クラス | ○ | × | ○ | ○ | ○ | ○ |
| 同じアセンブリの非派生クラス | ○ | × | × | ○ | ○ | × |
| 別アセンブリの派生クラス | ○ | × | ○ | × | ○ | × |
| 別アセンブリの非派生クラス | ○ | × | × | × | × | × |
アクセス範囲の広さだけで並べると、単純な一直線にはなりません。特にprotectedとinternalは制限する方向が異なります。
protected:継承関係を基準に制限するinternal:アセンブリを基準に制限する
4-2. クラス・メソッド・フィールドでの使い分け
トップレベルに宣言するクラスでは、主にpublic、internal、fileを使用します。
public class PublicClass{}internal class InternalClass{}
file class FileLocalClass{}
privateやprotectedは、トップレベルクラスには指定できません。これらは、クラス内部に定義する入れ子型には指定できます。
public class Container{private class InternalWorker{}protected class ExtensionPoint{}
}
メソッドやフィールドでは、クラスの利用者に公開するものをpublic、内部実装をprivateにするのが基本です。
public class OrderService{private readonly OrderRepository _repository;public OrderService(OrderRepository repository){_repository = repository;}public void PlaceOrder(){ValidateOrder();_repository.Save();}private void ValidateOrder(){// 内部の検証処理}
}
4-3. 迷ったときはprivateを基本にする理由
メンバーのアクセス修飾子に迷った場合は、まずprivateを検討します。
アクセス範囲を狭くする主なメリットは次のとおりです。
外部から予期しない値を設定されにくい
内部実装を変更しやすい
クラスの公開APIが分かりやすくなる
影響範囲を限定しやすい
不具合の原因を追跡しやすい
一度publicとして公開したメンバーは、多くのコードから依存される可能性があります。後から名前や引数、戻り値を変更すると、利用側のコードにも影響します。
そのため、最初は必要最小限の範囲に限定し、必要性が明確になった段階でアクセス範囲を広げる考え方が有効です。
4-4. 外部に公開するメンバーだけpublicにする考え方
publicにするのは、クラスの利用者に正式に使ってほしいメンバーだけにします。
public class PasswordService{public bool Verify(string password, string passwordHash){string calculatedHash = CalculateHash(password);return calculatedHash == passwordHash;}private string CalculateHash(string password){// 説明用の簡略化した処理return password;}
}
利用者に必要なのはVerifyメソッドです。内部処理であるCalculateHashまで公開する必要はありません。
公開範囲を必要最小限にする考え方は、カプセル化と呼ばれます。C#のアクセス修飾子は、カプセル化を実現するための基本的な仕組みです。
5. static修飾子の意味と使い方
5-1. staticはインスタンスを作らずに使える
通常のメンバーは、クラスからインスタンスを作成して利用します。
public class Counter{public int Value { get; set; }}var counter = new Counter();counter.Value++;
一方、staticメンバーは特定のインスタンスではなく、型そのものに属します。
public class Counter{public static int TotalCount { get; private set; }public Counter(){TotalCount++;}
}
TotalCountは、クラス名を使ってアクセスします。
var first = new Counter();var second = new Counter();Console.WriteLine(Counter.TotalCount);
この例では、すべてのCounterインスタンスが同じTotalCountを共有します。
5-2. staticメソッドの使い方
staticメソッドは、インスタンスを作らずに呼び出せます。
public class NumberUtility{public static bool IsEven(int value){return value % 2 == 0;}}呼び出すときはクラス名を指定します。
bool result = NumberUtility.IsEven(10);Console.WriteLine(result);staticメソッドからは、同じクラスのインスタンスメンバーに直接アクセスできません。
public class Sample{private int _value;public static void ShowValue(){// コンパイルエラー// Console.WriteLine(_value);}
}
インスタンスメンバーはオブジェクトごとに存在しますが、staticメソッドには対象となるオブジェクトがないためです。
5-3. staticフィールド・staticプロパティの使い方
staticフィールドやstaticプロパティは、型全体で共有するデータに使います。
public class ApplicationState{private static int _activeUserCount;public static int ActiveUserCount{get{return _activeUserCount;}}public static void Login(){_activeUserCount++;}public static void Logout(){_activeUserCount--;}
}
利用側は次のようになります。
ApplicationState.Login();ApplicationState.Login();Console.WriteLine(ApplicationState.ActiveUserCount);
複数スレッドから同時に更新される可能性がある場合は、単純な加算や減算では競合が起こることがあります。スレッドセーフにする必要がある場合は、Interlockedやlockなども検討します。
5-4. staticクラスの使い方
クラス自体にstaticを指定すると、インスタンスを作成できない静的クラスになります。
public static class TemperatureConverter{public static double CelsiusToFahrenheit(double celsius){return celsius * 9 / 5 + 32;}public static double FahrenheitToCelsius(double fahrenheit){return (fahrenheit - 32) * 5 / 9;}
}
次のように利用します。
double fahrenheit =TemperatureConverter.CelsiusToFahrenheit(25);静的クラスには、基本的に静的メンバーだけを定義します。また、静的クラスはインスタンス化できず、ほかのクラスの基底クラスにもできません。
拡張メソッドも静的クラスの中に定義します。
public static class StringExtensions{public static bool IsNotEmpty(this string? value){return !string.IsNullOrEmpty(value);}}5-5. staticを使うべき場面と使いすぎの注意点
staticが適している代表的な場面は次のとおりです。
入力だけで結果が決まる変換処理
数学関数や文字列操作などのユーティリティ
すべてのインスタンスで共有する必要がある値
拡張メソッド
アプリケーション全体で一つだけ管理する状態
ただし、変更可能な状態をstaticで大量に持たせると、次の問題が起こりやすくなります。
どこから値が変更されたのか分かりにくい
テストごとに状態が残る
並列処理で競合が起こる
クラス同士の依存関係が見えにくい
実装を差し替えにくい
単なる処理の共通化にはstaticが便利ですが、外部サービスやデータベースへのアクセスなど、状態や依存関係を持つ処理では、通常のクラスと依存性注入を使う設計も検討します。
6. readonly・constの違いと使い方
6-1. readonly:初期化後に変更できないフィールド
readonlyは、フィールドへの代入を制限する修飾子です。
インスタンスのreadonlyフィールドには、主に宣言時またはコンストラクター内で値を設定できます。
public class User{private readonly int _id;public User(int id){_id = id;}public int GetId(){return _id;}
}
通常のメソッド内で再代入することはできません。
public void ChangeId(int id){// コンパイルエラー// _id = id;}static readonlyフィールドは、宣言時または静的コンストラクターで初期化できます。
public class AppInfo{public static readonly DateTime StartedAt;static AppInfo(){StartedAt = DateTime.Now;}
}
なお、参照型のフィールドをreadonlyにしても、参照先のオブジェクトまで完全に変更不可になるわけではありません。
public class Team{private readonly List<string> _members = new();public void AddMember(string name){_members.Add(name);}
}
この場合、_membersに別のリストを再代入することはできませんが、リストの要素は追加できます。
6-2. const:コンパイル時に決まる定数
constは、コンパイル時に値が決まる定数を宣言するために使います。
public class MathConstants{public const double Pi = 3.141592653589793;public const int DaysInWeek = 7;public const string DefaultLanguage = "ja";}constフィールドは宣言と同時に値を設定する必要があります。
public const int MaxRetryCount = 3;次のように実行時に決まる値は指定できません。
// コンパイルエラー// public const DateTime StartedAt = DateTime.Now;constは暗黙的に静的な値として扱われるため、インスタンスを作らずにクラス名からアクセスします。
Console.WriteLine(MathConstants.DaysInWeek);static constのように明示的にstaticと組み合わせることはできません。
6-3. readonlyとconstの違いを比較
| 比較項目 | readonly | const |
| 値が決まる時期 | 実行時でもよい | コンパイル時 |
| 初期化場所 | 宣言時またはコンストラクター | 宣言時のみ |
| インスタンスごとの値 | 設定可能 | 設定不可 |
staticとの組み合わせ | 可能 | 暗黙的に静的なため不可 |
DateTime.Nowの設定 | 可能 | 不可 |
| 参照型の利用 | 任意の参照型を利用可能 | 原則として文字列またはnullに限定 |
| 利用例 | 起動時刻、設定値、依存オブジェクト | 数学定数、固定文字列、固定数値 |
たとえば、次のように使い分けます。
public class SystemSettings{public const int MaxLoginAttempts = 5;public static readonly DateTime ApplicationStartedAt =DateTime.Now;
}
MaxLoginAttemptsはプログラムをコンパイルした時点で決まるためconst、ApplicationStartedAtは実行時に決まるためstatic readonlyです。
6-4. どちらを使うべきか判断するポイント
値が将来も変わらないコンパイル時定数であれば、constが候補になります。
public const int MonthsInYear = 12;一方、次のような値にはreadonlyが適しています。
コンストラクターの引数から設定する値
実行時に計算する値
DateTimeや配列、独自クラスなどの値将来変更される可能性があるライブラリ公開値
public static readonly TimeSpan DefaultTimeout =TimeSpan.FromSeconds(30);別アセンブリに公開したpublic constの値は、利用側のコンパイル結果に埋め込まれる場合があります。ライブラリ側だけを更新しても利用側に古い値が残る可能性があるため、将来変更する可能性がある公開値にはpublic static readonlyを検討します。
7. 継承に関する修飾子の使い方
7-1. virtual:派生クラスで上書き可能にする
virtualは、メソッドやプロパティなどを派生クラスで上書きできるようにする修飾子です。
public class Animal{public virtual void Speak(){Console.WriteLine("鳴き声");}}virtualメンバーには基底クラス側の実装を記述できます。派生クラスで上書きされなければ、その実装が実行されます。
7-2. override:基底クラスの処理を上書きする
派生クラスではoverrideを使い、基底クラスのvirtualメンバーを上書きします。
public class Dog : Animal{public override void Speak(){Console.WriteLine("ワン");}}基底クラス型の変数で参照しても、実際のインスタンスに対応する処理が呼ばれます。
Animal animal = new Dog();animal.Speak();実行結果は「ワン」です。この仕組みをポリモーフィズム、または多態性と呼びます。
基底クラスの処理も利用したい場合は、baseを使います。
public override void Speak(){base.Speak();Console.WriteLine("ワン");}7-3. abstract:実装を派生クラスに任せる
abstractクラスは、直接インスタンス化できない基底クラスです。
public abstract class Shape{public abstract double GetArea();}abstractメソッドには通常、基底クラス側の実装がありません。非抽象の派生クラスは、そのメソッドを実装する必要があります。
public class Circle : Shape{public double Radius { get; }public Circle(double radius){Radius = radius;}public override double GetArea(){return Math.PI * Radius * Radius;}
}
抽象クラスには、抽象メンバーだけでなく、実装済みの通常メンバーも定義できます。
public abstract class Shape{public string Name { get; }protected Shape(string name){Name = name;}public abstract double GetArea();public void ShowName(){Console.WriteLine(Name);}
}
7-4. sealed:継承やオーバーライドを禁止する
クラスにsealedを指定すると、そのクラスを継承できなくなります。
public sealed class SecurityToken{}次のコードはコンパイルエラーになります。
// コンパイルエラー// public class CustomToken : SecurityToken// {// }オーバーライドしたメンバーにsealedを指定すると、それより下の派生クラスによる再オーバーライドを禁止できます。
public class BaseClass{public virtual void Execute(){}}public class MiddleClass : BaseClass{public sealed override void Execute(){}}
メンバーにsealedを指定する場合は、overrideと組み合わせます。
7-5. new:継承元のメンバーを隠す
派生クラスに基底クラスと同名のメンバーを定義するとき、newを指定すると、意図的に基底クラスのメンバーを隠すことを表せます。
public class BasePrinter{public void Print(){Console.WriteLine("Base");}}public class CustomPrinter : BasePrinter{public new void Print(){Console.WriteLine("Custom");}}
呼び出されるメソッドは、変数のコンパイル時の型によって変わります。
CustomPrinter custom = new CustomPrinter();custom.Print();BasePrinter basePrinter = custom;basePrinter.Print();
実行結果は次のようになります。
CustomBasenewはオーバーライドではありません。多態性を利用したい場合は、基底クラスでvirtual、派生クラスでoverrideを使用します。
8. クラスに付けられる修飾子
8-1. public classとinternal classの違い
トップレベルクラスにアクセス修飾子を明示しない場合、既定ではinternalになります。
class Product{}これは次の宣言と同じアクセス範囲です。
internal class Product{}別アセンブリから利用させたいクラスにはpublicを指定します。
public class Product{}ライブラリ内部だけで使用する実装クラスはinternal、利用者に提供するクラスはpublicという使い分けが基本です。
8-2. abstract classの使い方
abstract classは、共通処理を持ちながら、一部の処理を派生クラスに任せたい場合に利用します。
public abstract class PaymentProcessor{public void ValidateAmount(decimal amount){if (amount <= 0){throw new ArgumentOutOfRangeException(nameof(amount));}}public abstract void Pay(decimal amount);
}
派生クラスは抽象メソッドを実装します。
public class CreditCardProcessor : PaymentProcessor{public override void Pay(decimal amount){ValidateAmount(amount);Console.WriteLine($"カードで{amount}円を支払いました");}}8-3. sealed classの使い方
sealed classは、継承によって振る舞いを変更されたくないクラスに使います。
public sealed class ApplicationConfig{public string EnvironmentName { get; }public ApplicationConfig(string environmentName){EnvironmentName = environmentName;}
}
値オブジェクト、セキュリティ上の制約があるクラス、継承を想定していない完成済みの実装などで利用されます。
8-4. static classの使い方
static classは、インスタンスの状態を持たない処理をまとめる場合に適しています。
public static class TaxCalculator{public static decimal Calculate(decimal price,decimal taxRate){return price * taxRate;}}静的クラスにはインスタンスコンストラクターを定義できず、newによるインスタンス化もできません。
8-5. partial classの使い方
partialを使うと、一つのクラス定義を複数の宣言に分割できます。
// User.Properties.cspublic partial class User{public int Id { get; set; }public string Name { get; set; } = "";
}
// User.Methods.cspublic partial class User{public void ShowProfile(){Console.WriteLine($"{Id}: {Name}");}}コンパイル時には、一つのUserクラスとしてまとめられます。
partialは、次のような場面で利用されます。
自動生成コードと手書きコードを分離する
大きなクラスを役割ごとにファイル分割する
UIデザイナーが生成するコードと処理を分離する
ソースジェネレーターが生成するコードと連携する
ただし、クラスが大きすぎるという理由だけでpartialを使うと、責務の多いクラスを温存してしまう場合があります。クラス分割が適切かどうかも検討しましょう。
9. メソッドに付けられる修飾子
9-1. public・privateなどのアクセス修飾子
メソッドには、利用を許可する範囲に応じてアクセス修飾子を指定します。
public class CustomerService{public void Register(string name){ValidateName(name);Save(name);}private void ValidateName(string name){if (string.IsNullOrWhiteSpace(name)){throw new ArgumentException("名前を入力してください。",nameof(name));}}private void Save(string name){// 保存処理}
}
外部から実行させるRegisterだけをpublicにし、内部処理はprivateにしています。
9-2. staticメソッド
インスタンスの状態を使わず、引数だけで処理できるメソッドにはstaticを指定できます。
public static int Clamp(int value,int minimum,int maximum){if (value < minimum){return minimum;}if (value > maximum){return maximum;}return value;
}
同じクラスのメソッドであっても、インスタンスメンバーを使う必要がなければstaticにすることで、処理がインスタンス状態に依存しないことを明示できます。
9-3. virtual・override・abstract・sealedメソッド
継承に関するメソッド修飾子の関係は次のとおりです。
public abstract class Report{public abstract string CreateTitle();public virtual string CreateBody(){return "共通本文";}
}
public class SalesReport : Report{public override string CreateTitle(){return "売上レポート";}
public sealed override string CreateBody(){return "売上データ";}
}
abstract:基底クラスでは実装せず、派生クラスに実装させるvirtual:既定実装を用意し、必要なら派生クラスで上書きさせるoverride:基底クラスの仮想メンバーまたは抽象メンバーを上書きするsealed override:上書きした処理を、さらに派生したクラスで変更できないようにする
9-4. asyncメソッド
asyncを指定したメソッドでは、awaitを使って非同期処理の完了を待機できます。
public async Task<string> LoadTextAsync(string filePath){string text =await File.ReadAllTextAsync(filePath);return text;
}
戻り値としては、主に次の型を使用します。
TaskTask<T>ValueTaskValueTask<T>非同期ストリームを返す場合の
IAsyncEnumerable<T>イベントハンドラーで使う
void
async voidは呼び出し側が完了や例外を扱いにくいため、原則としてイベントハンドラー以外では避けます。
また、asyncを付けただけで自動的に別スレッドで処理されるわけではありません。asyncは、主にawaitを使った非同期処理を記述するための仕組みです。
9-5. externメソッド
externは、メソッドの実装がC#コードの外部にあることを表します。
たとえば、WindowsのネイティブAPIを呼び出す場合に使用できます。
using System.Runtime.InteropServices;public static class NativeMethods{[DllImport("user32.dll",CharSet = CharSet.Unicode)]public static extern int MessageBox(IntPtr windowHandle,string text,string caption,uint type);}
externメソッドには通常、C#側のメソッド本体を書きません。
ネイティブコードとの連携では、対象OS、文字コード、引数の型、メモリ管理などに注意が必要です。新しい.NETでは、ソース生成を利用するLibraryImportも選択肢になります。
10. フィールド・プロパティに付けられる修飾子
10-1. public・privateフィールドの使い分け
フィールドは、原則としてprivateにするのが基本です。
public class Product{private decimal _price;}publicフィールドを使うと、外部から自由に値を変更できます。
public class Product{public decimal Price;}この場合、負の価格も設定できてしまいます。
var product = new Product();product.Price = -1000;外部へ値を公開するときは、フィールドではなくプロパティを使用すると、読み書きの範囲や検証処理を制御できます。
10-2. privateフィールドとpublicプロパティの基本形
従来の基本形は、privateフィールドをpublicプロパティから操作する形です。
public class Product{private decimal _price;public decimal Price{get{return _price;}set{if (value < 0){throw new ArgumentOutOfRangeException(nameof(value));}_price = value;}}
}
単純に値を保持するだけなら、自動実装プロパティを使えます。
public class Product{public string Name { get; set; } = "";}外部からの変更を禁止したい場合は、private setやinitを利用します。
public class Product{public int Id { get; private set; }public string Name { get; init; } = "";
}
private set:クラス内部からだけ値を変更できるinit:主にオブジェクトの初期化時だけ値を設定できる
10-3. readonlyフィールド
コンストラクターで設定した後に再代入させたくないフィールドには、readonlyを使います。
public class OrderService{private readonly OrderRepository _repository;public OrderService(OrderRepository repository){_repository = repository;}
}
依存性注入で受け取るオブジェクトを保持するフィールドでは、private readonlyがよく使われます。
10-4. constフィールド
プログラム全体で変わらないコンパイル時定数には、constを使います。
public class ValidationRules{public const int MinimumPasswordLength = 12;public const int MaximumUserNameLength = 50;}クラス内部だけで使う定数はprivate constにできます。
private const int DefaultRetryCount = 3;10-5. staticフィールド・staticプロパティ
すべてのインスタンスで共有する値には、staticを使います。
public class User{private static int _createdCount;public static int CreatedCount{get{return _createdCount;}}public User(){_createdCount++;}
}
変更させたくない共有値には、static readonlyを使えます。
public static readonly TimeSpan DefaultTimeout =TimeSpan.FromSeconds(30);変更可能なpublic staticフィールドは、どこからでも値を書き換えられるため、原則として避けます。必要な場合は、セッターを制限したプロパティやメソッド経由で状態を管理します。
11. 修飾子の組み合わせルール
11-1. 複数の修飾子を同時に指定できる例
C#では、対象に応じて複数の修飾子を組み合わせられます。
public static class StringUtility{private static readonly char[] Separators ={',',';'};public static string[] Split(string value){return value.Split(Separators);}
}
よく使う組み合わせには次のようなものがあります。
private readonly UserRepository _repository;public static readonly TimeSpan Timeout =TimeSpan.FromSeconds(30);
public abstract class BaseService{protected abstract void ExecuteCore();}
public sealed override string ToString(){return "Sample";}
11-2. 指定できない組み合わせの例
修飾子は自由に組み合わせられるわけではありません。
abstractとsealedをクラスに同時指定する
// コンパイルエラー// public abstract sealed class Sample// {// }abstractクラスは派生クラスによる実装を前提とし、sealedクラスは継承を禁止するため、目的が矛盾します。
ただし、静的クラスは言語仕様上、インスタンス化も継承もできない特殊なクラスとして扱われます。
staticとconstを同時指定する
// コンパイルエラー// public static const int Count = 10;constは暗黙的に静的な値として扱われるため、staticを追加できません。
privateとpublicを同時指定する
// コンパイルエラー// public private void Execute()// {// }一つの宣言に複数のアクセスレベルを指定することはできません。例外は、言語として定義されているprotected internalとprivate protectedです。
静的クラスにインスタンスメンバーを定義する
public static class Utility{// コンパイルエラー// public int Value { get; set; }}静的クラスのメンバーは静的である必要があります。
11-3. 修飾子を書く順番に決まりはあるか
コンパイラが受け付ける範囲では、修飾子の順番を入れ替えられる場合があります。
public static readonly int Value = 10;static public readonly int Value = 10;ただし、一般的には次のように、アクセス修飾子を先頭に書きます。
public static readonly int Value = 10;よく見られる順番は、概ね次のとおりです。
アクセス修飾子static・extern・newvirtual・abstract・sealed・overridereadonly・unsafe・asyncチームやプロジェクトで順番を統一すると、コードを読みやすくなります。.editorconfigやコード分析ルールを使えば、修飾子の順番を自動的に統一できます。
11-4. よく使う組み合わせパターン
| 組み合わせ | 用途 |
private readonly | コンストラクターで受け取った依存オブジェクトの保持 |
private const | クラス内部だけで使う固定値 |
public const | 外部へ公開するコンパイル時定数 |
public static | インスタンス不要の公開メソッドやプロパティ |
public static readonly | 実行時に決まる共有の読み取り専用値 |
public abstract | 継承を前提とする公開基底クラス |
protected abstract | 派生クラスだけに実装させるメンバー |
public virtual | 外部から呼び出せ、派生クラスで上書きできるメンバー |
public override | 公開された仮想メンバーの上書き |
public sealed override | 上書き後の再オーバーライドを禁止 |
public async | 外部から呼び出す非同期メソッド |
internal sealed | アセンブリ内部だけで使い、継承も禁止するクラス |
12. C#修飾子のよくあるエラーと対処法
12-1. privateメンバーに外部からアクセスしてエラーになる
次のコードでは、_nameがprivateなので、外部からアクセスできません。
public class User{private string _name = "";}var user = new User();
// コンパイルエラー// user._name = "山田";
外部へ公開する必要がある場合は、プロパティやメソッドを用意します。
public class User{private string _name = "";public string Name{get{return _name;}private set{_name = value;}}public void Rename(string name){if (string.IsNullOrWhiteSpace(name)){throw new ArgumentException("名前を入力してください。",nameof(name));}Name = name;}
}
単にフィールドをpublicに変更するのではなく、どの操作を外部に許可すべきかを考えることが重要です。
12-2. staticメンバーとインスタンスメンバーを混同する
staticメンバーはクラス名から呼び出します。
public class Calculator{public static int Add(int x, int y){return x + y;}}正しい呼び出し方は次のとおりです。
int result = Calculator.Add(10, 20);また、静的メソッドからインスタンスフィールドへ直接アクセスすることはできません。
public class Counter{private int _value;public static void Increment(){// コンパイルエラー// _value++;}
}
インスタンスの状態を操作するなら、通常のインスタンスメソッドにします。
public void Increment(){_value++;}12-3. overrideに必要なvirtual・abstractがない
基底クラスの通常メソッドは、overrideできません。
public class BaseClass{public void Execute(){}}public class DerivedClass : BaseClass{// コンパイルエラー// public override void Execute()// {// }}
上書きを許可する場合、基底クラス側にvirtualを指定します。
public class BaseClass{public virtual void Execute(){}}public class DerivedClass : BaseClass{public override void Execute(){}}
基底クラス側で実装を用意しない場合は、クラスとメソッドをabstractにします。
12-4. readonlyとconstを間違えて使う
実行時に決まる値をconstにすると、コンパイルエラーになります。
// コンパイルエラー// public const DateTime StartedAt = DateTime.Now;この場合はstatic readonlyを使用します。
public static readonly DateTime StartedAt =DateTime.Now;コンストラクター引数から設定する値にもreadonlyを使います。
public class User{private readonly int _id;public User(int id){_id = id;}
}
判断に迷ったら、「コンパイル時に値が完全に決まるか」を確認しましょう。決まる場合はconst、実行時に決まる場合はreadonlyが候補です。
12-5. アクセス修飾子を省略したときの既定値を誤解する
アクセス修飾子を省略した場合の既定値は、宣言する場所によって異なります。
class Sample{int _value;void Execute(){}
}
この例では、次のアクセス範囲になります。
トップレベルクラス
Sample:internalクラスのフィールド
_value:privateクラスのメソッド
Execute:private
明示すると次のコードに相当します。
internal class Sample{private int _value;private void Execute(){}
}
主な既定のアクセス範囲は次のとおりです。
| 宣言対象 | 既定のアクセス範囲 |
| トップレベルのクラス、構造体 | internal |
| クラスのメンバー | private |
| 構造体のメンバー | private |
| インターフェースのメンバー | public |
| 列挙型のメンバー | public |
既定値に頼りすぎると意図が伝わりにくいことがあります。特にチーム開発では、重要なメンバーのアクセス修飾子を明示すると読みやすくなります。
13. C#修飾子の使い分け早見表
13-1. アクセス範囲で選ぶ修飾子
| 実現したいこと | 選ぶ修飾子 |
| どこからでも利用させたい | public |
| 宣言したクラス内部だけで使いたい | private |
| 派生クラスから使わせたい | protected |
| 同じアセンブリ内だけで使いたい | internal |
| 同じアセンブリ内または派生クラスから使わせたい | protected internal |
| 同じアセンブリ内の派生クラスだけで使わせたい | private protected |
| 同じソースファイル内だけで型を使いたい | file |
基本的には、必要な範囲に限定できる最も狭いアクセス修飾子を選びます。
13-2. インスタンス化の有無で選ぶ修飾子
| 実現したいこと | 選択 |
| オブジェクトごとに異なる状態を持たせたい | 通常のインスタンスメンバー |
| インスタンスを作らずに呼び出したい | staticメンバー |
| クラスのインスタンス化自体を禁止したい | static class |
| 継承を前提とし、直接インスタンス化させたくない | abstract class |
staticにするか迷ったときは、その処理やデータが個々のオブジェクトに属するものか、型全体に属するものかを考えます。
13-3. 値の変更可否で選ぶ修飾子
| 実現したいこと | 選ぶ仕組み |
| コンパイル時に決まる完全な定数 | const |
| コンストラクターで設定後、再代入を禁止したい | readonly |
| 型全体で共有する読み取り専用値 | static readonly |
| オブジェクト作成時だけプロパティを設定したい | init |
| 作成時のプロパティ設定を必須にしたい | required |
| 外部からのプロパティ変更を禁止したい | private set |
readonlyはフィールドへの再代入を制限するものであり、参照先オブジェクトの全状態を不変にするものではない点に注意しましょう。
13-4. 継承・オーバーライドの可否で選ぶ修飾子
| 実現したいこと | 選ぶ修飾子 |
| 派生クラスでメンバーを上書き可能にしたい | virtual |
| 基底クラスのメンバーを上書きしたい | override |
| 実装を派生クラスに任せたい | abstract |
| クラスの継承を禁止したい | sealed class |
| それ以上のオーバーライドを禁止したい | sealed override |
| 基底クラスの同名メンバーを隠したい | new |
特にoverrideとnewの違いは重要です。多態性を利用する設計では、通常はvirtualとoverrideを使用します。newは、基底クラスのメンバーを意図的に隠す必要がある場合に限定して使いましょう。
まとめ
C#の修飾子は、クラスやメソッド、フィールド、プロパティなどのアクセス範囲や性質を指定するキーワードです。
アクセス範囲を指定する主な修飾子には、次のものがあります。
public:どこからでもアクセスできるprivate:同じ型の内部からのみアクセスできるprotected:同じ型と派生型からアクセスできるinternal:同じアセンブリ内からアクセスできるprotected internal:同じアセンブリ内、または派生型からアクセスできるprivate protected:同じアセンブリ内の派生型からアクセスできる
そのほかにも、インスタンスを作らずに利用するstatic、値の変更を制限するreadonlyとconst、継承を制御するvirtual、override、abstract、sealedなどがあります。
修飾子を使うときは、次の考え方を基本にすると判断しやすくなります。
アクセス範囲は必要最小限にする
外部に公開する正式なメンバーだけを
publicにするクラス内部の実装は原則として
privateにする型全体で共有すべき処理や値にだけ
staticを使うコンパイル時定数には
const、実行時に決まる読み取り専用値にはreadonlyを使う多態性が必要なら
virtualとoverrideを使う継承や上書きを許可しない場合は
sealedを使う
C#の修飾子を適切に使い分けることで、コードの設計意図が明確になり、変更に強く、安全で読みやすいプログラムを作れるようになります。

