1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】constとstatic readonlyの違い

Posted at

概要

定数を宣言する際に使用する、C#のconstとstatic readonlyの違いについてまとめてみました。

目次

const

「コンパイル時定数」ともいわれ、ビルド時に値が埋め込まれます (インライン展開)。また、暗黙的にstaticとなります。
C# 1.0 (.NET Framework 1.0) から存在していますが、const自体はもっと古くCやC++の時代からの伝統的なキーワードです。

内部的にstaticとなるため、static constという記述はエラーになります。

インライン展開とは

コンパイル時に値がコードへ直接埋め込まれることです。以下にイメージを記載します。

例:メソッドのインライン展開のイメージ

実際のコード
using System;
using System.Runtime.CompilerServices;

public static class MathEx
{
    public static int Add(int x, int y)
    {
        return x + y;
    }
}

public class Program
{
    public static void Main()
    {
        int a = 10;
        int b = 20;

        int s = MathEx.Add(a, b);  // ← メソッド呼び出しで a + b を実施
        Console.WriteLine(s);
    }
}

上記がコンパイル後、下記のようなイメージになります。

インライン展開後 (イメージ)
// 実際のコンパイラ出力ではありません。説明用のイメージです。
public class Program
{
    public static void Main()
    {
        int a = 10;
        int b = 20;

        int s = a + b;             // ← 呼び出しが消え、メソッド本体がここに展開されたイメージ
        Console.WriteLine(s);
    }
}

このようにインライン展開を行うと、関数の呼び出し (関数がおかれているメモリに移動して関数を実行する) 過程がなくなるため、プログラムの高速化に繋がります。
上記の例では関数呼び出しが一回だけですが、ループ内で数千回呼ばれる場合などはインライン展開しておくと、関数呼び出しのオーバーヘッド (本質的な処理以外の追加コスト) を削減できます。

上記を踏まえて、constも以下のようにインライン展開されています。

例:constのインライン展開のイメージ

実際のコード
public class Config
{
    public const int BufferSize = 1024;
}

public class App
{
    public void Run()
    {
        int size = Config.BufferSize;
        Console.WriteLine(size);
    }
}
インライン展開後 (イメージ)
public void Run()
{
    int size = 1024; // ← Config.BufferSize が「値」に置き換わっている
    Console.WriteLine(size);
}

constで使用できる型

constで使用できる型は限られています。

  • OK:数値、bool、char、string、enum、decimal (特例)
  • NG:DateTime、Guid、List、配列など (newやメソッド呼び出しが必要なもの)

constの初期化子は「コンパイル時に完全評価できる定数式」でなければならないためです。つまり、コンストラクタ呼び出し (new) やメソッド呼び出し・プロパティ参照を伴うものはconstでは使用不可になります。DateTimeなどは構造体でも作成にコンストラクタが必要なのでconstにできません。

static readonly

static readonlyは実行時に一度だけ値を決定します。値は埋め込まれず、参照される形になります。また、staticが付いた静的コンストラクタ内で動的に値を決定できます。
C# 1.0 (.NET Framework 1.0) から存在していますがconstと違い、C#固有の設計思想 (.NETの型安全性や実行時初期化のため) のためにC#から追加されたものです。

readonlyとは

readonlyとstatic readonlyはどちらも「再代入できないフィールド」を作るための修飾子ですが、どこに属するか (インスタンス or 型全体) が違います。
まずreadonlyはインスタンスごとに別々の値を持てますが、static readonlyはそのクラスで一つの値のみを持てます。
代入する方法も、readonlyでは宣言時かインスタンスのコンストラクタ内、static readonlyは宣言時か、静的コンストラクタ内で値を代入できます。
インスタンスごとに異なる値はreadonly、どのインスタンスでも共通な値はstatic readonlyを使用すると良さそうです。

public class Point
{
    // インスタンスごとに不変にしたいなら readonly
    public readonly int X;
    public readonly int Y;

    // 共有の不変インスタンスは static readonly
    public static readonly Z;

    public Point(int x, int y)
    {
        X = x;  // XとYはreadonlyなのでコンストラクタ内で代入可 (以後は不可)
        Y = y;
    }

    static Point()  // 静的CLRが自動的に呼び出すため、アクセス修飾子は付けられない (引数も付けられない)
    {
        Z = 1;  // Zはstatic readonlyなので静的コンストラクタ内で代入可 (以後は不可)
    }
}

使い分け

値がコンパイル時に決まる場合はconst、実行時に値が決まる場合は、static readonlyがよいです。そのほかにも外部公開で将来変わる可能性がある場合もstatic readonlyがよさそうです。
ただし、constを使用する際は以下の注意点があります。

注意点

constは値が埋め込まれるため、ライブラリ更新時に再ビルドが必要です。static readonlyはフィールド参照なので、ライブラリ差し替えで即反映されます。

// ライブラリ側(Assembly A)
public static class Api
{
    public const int VersionConst = 1;
    public static readonly int VersionRo = 1;
}

// アプリ側(Assembly B)
public class App
{
    public void Run()
    {
        // const は「値そのもの」がビルド時に埋め込まれる
        // そのため、ライブラリ(Assembly A)のVersionConstを 2 に上げても、アプリ(Assembly B)を再ビルドしない限り 1 のままになる
        int v1 = Api.VersionConst;

        // static readonly はフィールド参照
        // そのため、ライブラリ更新で 2 になれば、アプリ再ビルドなしでも 2 を見る
        int v2 = Api.VersionRo;
    }
}

以下に、パターン別にどれを使用すればよいか記載してみました。

1) 固定リテラル (将来も絶対に変わらない値) → const

public static class Config
{
    public const int BufferSize = 1024;
    public const string AppName = "SampleApp";
    public const decimal TaxRate = 0.10m; // decimalもconst可
}

2) 属性引数 / switch の case ラベル → const

switchのcaseではstatic readonlyは使用できません。caseはコンパイル時定数が必要なため、実行時に値が決まるstatic readonlyは使えないです。
ただif文の条件式は実行時に評価される仕組みなので、static readonlyでも使用できます。

swich文はコンパイル時にジャンプテーブルと呼ばれる分岐先の一覧を作成します。このためswich文は、実行時では値をみて即ジャンプできるので高速になります。
例えば10個の条件があれば、if-elseは条件を順番に評価するので最高10回も比較が発生します。
switchのジャンプテーブルを使用すると、条件の数にとらわれず1回の計算で目的の分岐に飛べます。

using System;

public static class Messages
{
    public const string Title = "Welcome";
}

public static class Demo
{
    public const int One = 1;

    public static string Label(int n)
    {
        switch (n)
        {
            case One:              // ← caseはコンパイル時定数が必要
                return "one";
            default:
                return "other";
        }
    }
}

3) 公開APIで“将来変わるかもしれない値” → static readonly(バージョン不整合を避ける)

注意点で記載した内容ですが、public constで定義すると、呼び出し側に値が埋め込まれるため、public constの値が更新されてそのDLLだけ差し替えても反映されません。
そこで値を埋め込ませないstatic readonlyを使います。

// constは呼び出し側に値が埋め込まれる
public static class ApiV1
{
    public const int Version = 1;
}

// readonlyはフィールド参照になる (呼び出し側に値は埋め込まれない)
public static class ApiV2
{
    public static readonly int Version = 1;
}

4) 実行時に決まる値(時刻・GUID など) → static readonly (readonly)

DateTime.UtcNowやGuid.NewGuid()はコンパイル時に決まらないので、static readonlyを使用します (constは使用できません) 。また、インスタンスごとに値が違うならstatic readonlyではなくreadonlyが良いです。

using System;

public static class RuntimeValues
{
    public static readonly DateTime BuildAt = DateTime.UtcNow;
    public static readonly Guid AppId = Guid.NewGuid();
}

5) パース結果や環境変数、計算で決まる設定 → static readonly

実行環境に依存する値もstatic readonlyを使用します (constは使用できません) 。

using System;

public static class Settings
{
    public static readonly int Port =
        int.Parse(Environment.GetEnvironmentVariable("APP_PORT") ?? "8080");

    public static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30);
}

6) Guid / Version / Regex など“new が必要”な型 → static readonly

new (コンストラクタ呼び出し) がある時点でconstにはできません。

using System;
using System.Text.RegularExpressions;

public static class Types
{
    public static readonly Version ApiVersion = new Version(1, 2, 0);
    public static readonly Regex IdPattern =
        new Regex(@"^[A-Z]{2}\d{4}$", RegexOptions.Compiled);
}

7) 配列・コレクション → static readonly (readonly)

static readonly は参照の再代入を禁止するだけで、中身の変更は可能です(ミュータブル)。これも、インスタンスごとに値が違うならstatic readonlyではなくreadonlyが良いです。

using System.Collections.Generic;

public static class Collections
{
    public static readonly int[] Sizes = new[] { 16, 32, 64 };
    public static readonly List<string> Names = new() { "A", "B" };

    // 参照の再代入は不可だが、中身の変更は可能 (必要なら不変コレクションを使用)
}

参考

ミュータブル (mutable) とは、「オブジェクトの状態 (中身) を後から変更できる」という意味です。逆にイミュータブル (immutable) は「一度作ったら中身を変更できない」オブジェクトです。
ミュータブルな型は以下のようなものがあります。

  • 配列
  • List
  • Dictionary
  • StringBuilder
  • DataTable
  • DataSet

イミュータブルな型は以下のようなものがあります。

  • int
  • double
  • decimal
  • string
  • bool
  • DateTime
  • Guid

public static class CollectionsDemo
{
    public static readonly int[] Sizes = new[] { 16, 32, 64 };
    public static readonly List<string> Names = new() { "A", "B" };

    public static void MutateContents()
    {
        // 中身の変更は可能 (配列の要素を書き換え / List に追加)
        Sizes[0] = 99;
        Names.Add("C");
    }

    public static void TryReassign()
    {
        // フィールド自体の再代入は不可 (コンパイルエラー)
        // Sizes = new[] { 1, 2, 3 };
        // Names = new List<string>();
    }
}

readonlyは「参照を変えない」だけを保証しているため、オブジェクトの中身がミュータブルなら変更可能です。完全に不変にしたい場合はイミュータブル型 (ImmutableArray<T>など) を使用します。

ちなみにconstはコンパイル時に完全に決まる値しか指定できないため、そもそもミュータブルな型を指定できません。なので「中身を変えられる」ことは発生しません。

8) 他の定数から組み立てる“本当に固定の式” → const

「+」や「*」など定数式だけで組み立てられるならconstが使用できます。ただメソッド呼び出し(Math.Absなど)が入ると使用できなくなります。

public static class MathConst
{
    public const int BaseValue = 10;
    public const int DoubleBase = BaseValue * 2; // 定数式ならOK
}

9) 文字列の固定キーやメッセージ → const

固定文字列はconstが最適です。文字列補間も、式がすべて定数なら大丈夫です。ただ公開APIの “ブランド名” のように変わる可能性がある文字列はstatic readonlyが安全です。

public static class Keys
{
    public const string CacheKeyUser = "cache:user";
    public const string LogTag = "APP";
}

10) 同一プロセス内で“起動時に一度だけ決める”値 → static readonly + 静的コンストラクタ

静的コンストラクタを使うと、初期化処理を一度だけ実行できます。

public static class PathConfig
{
    public static readonly string BaseDir;

    static PathConfig()
    {
        BaseDir = Environment.CurrentDirectory; // 起動環境に依存して一度だけ確定
    }
}

補足

上記のようにケースによってどちらを使用するかという判断はありますが、実務的には「迷ったらstatic readonly」が安全だと感じます。ただし、swich文のcaseのようなコンパイル時定数が必要な場面ではconst一択になります。
constは「この値は絶対に変わらない」という強い意味を示せますが、万が一想定外でconstにしていたところの値が変わってしまった場合、その値を参照していたモジュールも再ビルドが必要になってしまいます。

違いの一覧表 (参考まで)

修飾子 再代入 中身変更 初期化タイミング 型の制約
const 不可 不可 コンパイル時 定数式のみ
readonly 不可 型次第 宣言時 or コンストラクタ 制約なし
static readonly 不可 型次第 宣言時 or 静的コンストラクタ 制約なし

終わりに

const と static readonly は似ているようで役割が違います。仕組みを理解して正しく使い分けることで、コードの安全性と保守性が少し高まるかと思いました。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?