C# の**フィールド(Field)**は、クラスや struct が持つ変数です。プロパティと異なりアクセサ(get / set)を持たず、値をそのまま格納します。
公開メンバーにはプロパティを使うのが C# の慣例ですが、private / protected なデータ保持や定数の定義にはフィールドが欠かせません。本記事ではフィールドの種類と修飾子を整理します。
インスタンスフィールド
各インスタンスごとに独立した値を持つ、もっとも基本的なフィールドです。
public class Person
{
private string _name;
private int _age;
public Person(string name, int age)
{
_name = name;
_age = age;
}
}
フィールド初期化子
フィールドは宣言時に初期値を設定できます。コンストラクタの実行前に評価されます。
public class Config
{
private int _retryCount = 3;
private string _locale = "ja-JP";
private List<string> _errors = new();
}
初期化子はコンストラクタの base() 呼び出しよりも先に実行されます。実行順序は次のとおりです。
- フィールド初期化子(宣言順)
- 基底クラスのコンストラクタ(
base()) - 自クラスのコンストラクタ本体
public class Base
{
public Base() => Console.WriteLine("2. Base コンストラクタ");
}
public class Derived : Base
{
private int _value = Log("1. フィールド初期化子");
public Derived() => Console.WriteLine("3. Derived コンストラクタ");
private static int Log(string msg) { Console.WriteLine(msg); return 0; }
}
// 出力:
// 1. フィールド初期化子
// 2. Base コンストラクタ
// 3. Derived コンストラクタ
静的フィールド(static field)
static を付けると、インスタンスではなく型に属するフィールドになります。全インスタンスで共有されます。
public class Counter
{
private static int _count = 0;
public Counter()
{
_count++;
}
public static int Count => _count;
}
var a = new Counter();
var b = new Counter();
Console.WriteLine(Counter.Count); // 2
静的フィールドの初期化は、その型に最初にアクセスした時点で一度だけ実行されます。明示的な初期化が必要な場合は静的コンストラクタを使います。
public class AppConfig
{
private static readonly string _version;
static AppConfig()
{
// 静的コンストラクタで初期化
_version = LoadVersionFromFile();
}
private static string LoadVersionFromFile() => "1.0.0";
}
const(コンパイル時定数)
const はコンパイル時に値が確定する定数を定義します。
public class MathConstants
{
public const double Pi = 3.14159265358979;
public const int MaxRetry = 5;
public const string DefaultLocale = "ja-JP";
}
Console.WriteLine(MathConstants.Pi); // 3.14159265358979
Console.WriteLine(MathConstants.MaxRetry); // 5
const の特徴
- コンパイル時に値が埋め込まれる — 呼び出し側のアセンブリに値がコピーされる
- 暗黙的に
static— インスタンスからはアクセスできない - 使える型はプリミティブ型(
int、double、boolなど)、string、enum、nullに限られる newを使う型(DateTime、配列、クラスなど)は使えない
public class Constants
{
public const int MaxSize = 100; // OK
public const string AppName = "MyApp"; // OK
public const double Rate = MaxSize * 0.1; // OK(const 同士の演算は可能)
// public const DateTime Epoch = new(2000, 1, 1); // コンパイルエラー
// public const int[] Values = { 1, 2, 3 }; // コンパイルエラー
}
コンパイル時埋め込みの注意点
const の値は参照先ではなく呼び出し側のアセンブリに直接埋め込まれます。そのため、ライブラリの const を変更した場合、呼び出し側も再コンパイルしないと古い値が使われ続けます。
// Library.dll
public class Settings
{
public const int Timeout = 30; // v1.0
}
// App.exe(Library.dll を参照)
Console.WriteLine(Settings.Timeout); // コンパイル時に「30」が埋め込まれる
Settings.Timeout を 60 に変更して Library.dll だけ再ビルドしても、App.exe は再コンパイルしない限り 30 を表示し続けます。この問題を避けるには static readonly を使います。
readonly(実行時定数)
readonly フィールドは、宣言時またはコンストラクタでのみ値を設定できるフィールドです。以降は変更できません。
public class Circle
{
private readonly double _radius;
public Circle(double radius)
{
_radius = radius; // コンストラクタ内は OK
}
public double Area()
{
// _radius = 10; // コンパイルエラー(コンストラクタ外での変更は不可)
return Math.PI * _radius * _radius;
}
}
readonly の特徴
- 実行時に値が決まる —
new、メソッド呼び出し、構成ファイルの読み取り結果なども使える - 宣言時の初期化子またはコンストラクタ内でのみ設定可能
- 任意の型に使える(
constと異なりDateTimeや配列も可)
public class AppConfig
{
private readonly DateTime _startedAt = DateTime.UtcNow; // OK
private readonly int[] _defaultPorts = { 80, 443, 8080 }; // OK
private readonly HttpClient _client = new(); // OK
}
readonly は「参照」が不変
参照型の readonly フィールドは、参照先の変更(再代入)は禁止されますが、参照先オブジェクトの中身の変更は防げません。
public class Team
{
private readonly List<string> _members = new();
public void AddMember(string name)
{
_members.Add(name); // OK(リストの中身を変更するのは許可される)
}
public void Reset()
{
// _members = new List<string>(); // コンパイルエラー(参照の再代入は不可)
_members.Clear(); // OK(中身のクリアは許可される)
}
}
中身も含めて不変にしたい場合は、IReadOnlyList<T> や ImmutableArray<T> などの不変コレクションを使います。
const vs readonly vs static readonly
3 つの「変更不可」なフィールドの違いを整理します。
public class Comparison
{
// コンパイル時定数 — 値がアセンブリに埋め込まれる
public const int CompileTimeConst = 100;
// インスタンスごとの実行時定数
private readonly DateTime _createdAt = DateTime.UtcNow;
// 型レベルの実行時定数(全インスタンスで共有)
public static readonly DateTime AppStartedAt = DateTime.UtcNow;
}
| 特徴 | const |
readonly |
static readonly |
|---|---|---|---|
| 値の決定タイミング | コンパイル時 | 実行時 | 実行時 |
| 暗黙の static | ✅ | ❌ | ✅(明示) |
| 使える型 | プリミティブ / string / enum / null | 任意 | 任意 |
| 設定可能な場所 | 宣言のみ | 宣言 / コンストラクタ | 宣言 / 静的コンストラクタ |
| アセンブリ間の影響 | 値が埋め込まれる(要再コンパイル) | 参照経由(影響なし) | 参照経由(影響なし) |
| パフォーマンス | 最速(リテラル扱い) | 通常のフィールドアクセス | 通常のフィールドアクセス |
使い分けの目安
// 絶対に変わらないリテラル値 → const
public const double Pi = 3.14159265358979;
public const int BitsPerByte = 8;
// 実行時に決まる or 別アセンブリから参照される → static readonly
public static readonly DateTime AppStartedAt = DateTime.UtcNow;
public static readonly string Version = Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "0.0.0";
// インスタンスごとに異なる不変値 → readonly
public class Order
{
private readonly string _orderId;
private readonly DateTime _createdAt;
public Order(string orderId)
{
_orderId = orderId;
_createdAt = DateTime.UtcNow;
}
}
volatile(揮発性フィールド)
volatile は、複数のスレッドからアクセスされるフィールドに対して、コンパイラやCPUの最適化による並べ替え・キャッシュを抑止する修飾子です。
public class Worker
{
private volatile bool _stopRequested = false;
public void Run()
{
while (!_stopRequested)
{
// 作業を続ける
}
Console.WriteLine("停止しました");
}
public void Stop()
{
_stopRequested = true; // 別スレッドから呼ぶ
}
}
volatile がないと、コンパイラが _stopRequested の値をレジスタにキャッシュして、別スレッドからの変更を検知できない可能性があります。
volatile の制約
- 使える型は参照型、
int、bool、byte、shortなど限定的(long、doubleは不可) volatileは単純な読み書きの可視性を保証するだけで、アトミック性は保証しない- 複合操作(インクリメントなど)には
Interlockedクラスやlockを使う
// volatile だけではスレッドセーフではない例
private volatile int _count = 0;
// _count++ は「読み取り → 加算 → 書き込み」の3ステップなので競合する
// → Interlocked.Increment(ref _count) を使う
一般的なマルチスレッドプログラミングでは lock や Interlocked を優先し、volatile はフラグ変数などの限定的な場面で使います。
フィールドの命名規則
C# の一般的な命名規則(.NET ランタイム / Microsoft のガイドライン)では、フィールドに以下の規則を適用します。
| 種類 | 命名規則 | 例 |
|---|---|---|
private / protected インスタンスフィールド |
_camelCase(アンダースコアプレフィックス) |
_name, _count |
private static フィールド |
s_camelCase または _camelCase |
s_instance, _instance |
const |
PascalCase |
MaxRetry, DefaultTimeout |
public static readonly |
PascalCase |
Empty, MinValue |
public class Example
{
// const — PascalCase
public const int MaxItems = 100;
// static readonly — PascalCase
public static readonly Example Default = new();
// private フィールド — _camelCase
private readonly string _name;
private int _count;
// private static — s_camelCase or _camelCase
private static int s_globalCount;
public Example(string name = "default")
{
_name = name;
}
}
フィールドとプロパティの使い分け
プロパティの記事でも比較しましたが、フィールド側の視点から改めて整理します。
| フィールドを使う場面 | プロパティを使う場面 |
|---|---|
private / protected なデータ保持 |
外部に公開するメンバー |
const 定数 |
バリデーションが必要 |
readonly でイミュータブルな内部状態 |
インターフェースの実装 |
volatile でスレッド間フラグ |
データバインディング |
| パフォーマンスが極めて重要な struct 内部 | シリアライズ対象 |
原則: public フィールドは避け、プロパティを使う。 const と public static readonly は例外的にフィールドのまま公開するのが慣例です。
まとめ
- フィールドはアクセサを持たない変数で、主に
private/protectedなデータ保持に使う constはコンパイル時定数。高速だが、別アセンブリとの間で値が埋め込まれる点に注意readonlyは実行時定数。宣言時またはコンストラクタでのみ設定でき、任意の型に使えるstatic readonlyは型レベルの実行時定数。constの埋め込み問題を回避しつつ不変性を保てるvolatileはスレッド間の可視性を保証するが、アトミック性は保証しないpublicフィールドは避け、constとstatic readonly以外はプロパティで公開するのが C# の慣例