はじめに
日々、プログラムを書く中で、変数を宣言しない日はほぼ無いと思います。
一方で、readonly や const などの修飾子について、
- 名前は知っている
- なんとなく使ったことはある
- でも「なぜ使うのか」を説明できない
という状態になっていないでしょうか。
実際のところ、型さえ合っていれば、
int value = 10;
のような宣言でプロブラムは大抵は動きます。
そのため、気づくと同じような宣言ばかりを書いてしまいがちです。
(少なくとも、私はそうでした)
この記事では、
使用頻度は低いが、理解すると設計の質が上がる修飾子を中心に整理してみます。
前提として知っておきたい修飾子
アクセス修飾子
アクセス範囲の概要のみを整理します。
| 修飾子 | アクセス範囲 |
|---|---|
| public | どこからでもアクセス可能 |
| private | クラス内部のみ(最も安全、迷ったらこれ) |
| protected | 派生クラスからのみアクセス可能 |
| internal | 同一アセンブリ内のみアクセス可能 |
| protected internal | 同一アセンブリ または 派生クラス |
| private protected | 同一アセンブリ かつ 派生クラス |
継承用修飾子
クラスやメンバーの 継承・上書き可否 を制御する修飾子です。
クラスに付ける修飾子
| 修飾子 | 意味 |
|---|---|
| abstract | インスタンス化できない。派生クラスでの実装を強制 |
| sealed | 継承を禁止する |
メンバーに付ける修飾子
| 修飾子 | 意味 |
|---|---|
| virtual | 派生クラスでの上書きを許可 |
| abstract | 派生クラスでの実装を強制 |
| override | 基底クラスの virtual / abstract メンバーを上書き |
| sealed | override をこれ以上許可しない |
| (修飾子なし) | 継承されるが override は不可(暗黙的に上書き不可) |
変更可否・意味を表す修飾子(本題)
readonly
readonly は、変数の値が実行中に変更されないことを保証する修飾子です。
代入できるタイミングは、次の2箇所のみです。
- 宣言時
- コンストラクタ内
public class Sample {
// OK(宣言時)
readonly int value = 1;
public Sample() {
// OK(コンストラクタ内)
value = 10;
}
public void Func() {
// NG(実行中の再代入は不可)
value = 20;
}
}
readonly と参照型の注意点
readonly は 再代入を禁止する 修飾子であり、
オブジェクトの 状態そのものを不変にするものではありません。
readonly List<int> list = new();
list.Add(1); // OK
list = new(); // コンパイルエラー
const
constは、設定値や定数を表し、コンパイル時に値が決定される修飾子です。
値そのものがコード中に埋め込まれるため、
実行時に決まる値や外部から与えられる値を扱うことはできません。
public class Sample {
// OK
const int value1 = 10;
// OK
const int value2 = value1;
public void XXX( int x ) {
// OK
const int value3 = 10;
// OK
const int value4 = value1;
// NG(コンパイル時に決定されないため)
const int value5 = x;
}
}
const と readonly の比較
| 項目 | const | readonly |
|---|---|---|
| 値の決定 | コンパイル時 | 実行時(宣言時 or コンストラクタ) |
| 初期化可能な場所 | 宣言時のみ | 宣言時 / コンストラクタ |
| 型 | プリミティブ / string / enum | 制限なし |
| 再代入 | 不可 | 不可 |
static
static は、フィールドやメソッドを クラスに1つだけ存在させる 修飾子です。
インスタンスを生成しなくてもアクセスできます。
static int count;
static は便利な反面、状態を持たせると設計が壊れやすくなります。
特に、複数スレッドからアクセスされる場合は注意が必要です。
static readonly int MaxRetryCount = 3;
このように、static と readonly を組み合わせることで、
「グローバルに共有されるが変更されない値」を安全に表現できます。
volatile(使用率低・重要)
volatile は、複数スレッド間での値の可視性を保証する 修飾子です。
volatile bool _running;
volatile を付けることで、
あるスレッドで変更された値が、他のスレッドからも必ず最新の状態で参照されます。
ただし、排他制御を行うものではありません。
そのため、CancellationToken など、
より安全で明確な仕組みが使われることが多いです。
ref / in / out
ref / in / out は、引数の渡し方(参照の扱い) を制御する修飾子です。
| 修飾子 | 役割 | 主な用途 |
|---|---|---|
| ref | 入出力 | 値を更新する |
| in | 入力 | コピー回避・意図明示 |
| out | 出力 | 結果を返す |
ref(入出力)
ref は、メソッド内で変更した値が呼び出し元に反映される修飾子です。
注意点
- 呼び出し側・呼び出され側の両方に ref が必要
- 状態変更が見えにくくなるため多用は非推奨
void Increment(ref int value)
{
value++;
}
int x = 10;
Increment(ref x);
Console.WriteLine(x); // 11
in(読み取り専用参照)
in は、読み取り専用の参照渡しを行うための修飾子です。
ref と同様に参照渡しですが、メソッド内での変更が禁止されます。
in は ref readonly と同じ意味になります。
void Print(in int value)
{
value++; // ❌ コンパイルエラー
Console.WriteLine(value);
}
int x = 10;
Print(in x);
in を使う場面
- コピーを避けたい
- 「変更しない」ことを明示できる
int 型など小さな値型では、引数渡し時にコピーが発生しても
パフォーマンス上の問題になることはほとんどありません。
一方、struct 型(複数の値をまとめた値型)などの場合、引数渡し時にコピーが発生すると、
このコピーコストが無視できなくなります。
そのような場合に in を使って参照渡しにすることで、
パフォーマンス改善につながる場面があります。
out(出力専用)
out は、メソッドから呼び出し元へ値を返すための修飾子です。
引数として渡しますが、役割は「入力」ではなく 出力専用 になります。
特徴
- メソッド内で 必ず代入しなければならない
- メソッドの戻り値とは別に、追加の結果を返せる
基本例
void SetValue(out int x)
{
x = 10;
}
SetValue(out int value);
Console.WriteLine(value); // 10
実務よりの例
bool TryCreateValue(string text, out int value)
{
if (int.TryParse(test, out int v))
{
value = v;
return true;
}
return false;
}
if (TryCreateValue("123", out int value))
{
Console.WriteLine(value); // 123
}
タプル型 との比較
C# 7.0以降では、戻り値にタプル型が使えるようになり、
複数値を返すことが容易になりました。
そのため、先程の「実務よりの例」は以下のように記載することもできます。
(bool, int) TryCreateValue(string text)
{
if (int.TryParse(text, out int v))
{
return (true, v);
}
return (false, 0);
}
var (success, value) = TryCreateValue("123");
if (success)
{
Console.WriteLine(value); // 123
}
タプル型の登場により、out を使う場面は減りましたが、
- Tryパターンとの相性の良さ
- .NET 標準 APIが大量に
outを使用
により、今もまだ使う場面はあります。
最後に
readonly や const などの修飾子は使用しなくても
プログラムを動作させることは可能です。
しかし、しっかりと修飾子を使用することで、
保守性やコードの読みやすさの向上が期待できます。