Edited at

constとreadonlyとstatic readonly、それぞれの特徴と使い分け方


最初に

本記事はconstreadonlystatic readonlyの違いと使い方に対して解説したものとなります。

また本記事は一度昔に個人ブログにあげていたものに修正を加えて再掲したものとなります。

そもそもどんな時に使うのか?

定数や、それに似た数を宣言する際に使用します。定数とはその名の通り、値が固定されて今後変更しないとわかっている数のことで、定数に格納された値は、アプリケーションの実行中に変わることはありません。なので、例えば時間が経つにつれて変更される可能性がある情報を表すために定数を使用するべきではないです。定数には、数字、bool、文字列などのプリミティブ型などが含まれています。

また、定数を使用する目的としては、マジックナンバー・文字列定数入力ミスの削減があげられます。

宣言をするに当たって、C#ではconst , readonlyが使用され、C++ではdefine , const , constexprが使用されます。

今回はC#の機能にフォーカスを当てて解説を進めていきます。

 

const



コンパイル時に値が埋め込む定数を宣言できます(コンパイル時定数と呼ぶ)。

コンパイル時定数は、コードがコンパイルされる時点で計算されて値が決定されると規格によって定められた定数です。なので定数の型はコンパイル時に確定できる値でなければなりません(プリミティブ型や文字列型だけでnewはできない)。任意の参照型を定数にすることも可能ですが、インスタンスはコンパイル時に作成できないのでnull参照以外は定数にできません。

主に固定長配列のサイズや列挙型の定義などに使用します。

以下がconstの特徴です。


  • constは固定の値が埋め込まれるため、インスタンスが異なっても常に同じ値となり、インスタンスごとに値を格納する必要がないので暗黙的にstaticとなる

  • ローカル変数にも使える。


  • 宣言時にのみ初期化ができる。

  • 埋め込みなので実行ファイルサイズは大きくなるが実行速度は速い。

  • switchやデフォルト引数で使用可能。

  • 静的に生成されているのでクラス名からしか取れない。

  • 埋め込みなので例えば自分で作ったアセンブリをクラスライブラリとして配布し、後ほどconstの値を変えた場合は、アセンブリを差し替えても、リビルドしなければ参照するアセンブリ側に変更した値は反映されない。つまり、参照先のDLLで古いconstの値が使用されるバグが発生する場合がある。これを、バージョンアップした時の挙動が怪しくなるという意味でバージョニング問題と呼ぶ。

  • コンパイル時定数はコードがコンパイルされる際、定数の値と同じ値に置き換えらえるので、結果としてリテラルを直接書いたものと同様になる。なので単純な計算の代入式は問題ないが、メソッドの実行を伴うような値の代入は許されない。

class Hoge

{
public const double PI = 3.14; // OK
public const double piyo = PI * PI; // OK
public const double payo = Math.Sqrt(10); // NG

void Piyo(){
//コンパイルで生成される中間言語では下の条件式はmyData == 3.14となる
if(Moge == PI)
//処理
}


メソッドのデフォルト値

メソッドのデフォルト値もconstと同じ挙動となります。

public static void Hoge (int Fuga = 100)

{
//なんか
}

public static void Piyo ()
{
// 実際にはコンパイル時にHoge(100);となっている
Hoge();
}

こういったケースが困る場合は、オーバーロードを複数個定義したほうが幸せになれます。

 

readonly



実行時に値を取得する読み取り専用の変数を宣言できます(実行時定数扱い)。

定数は宣言時に値が確定している必要があります。

しかし、アプリの実行後の値を取得し、定期的に使いたい時にはreadonlyを用います。これは実行時まで確定はできないが、初期化後は不変な値となります。定数と意味合いも似ていますが、定数ではなく読み取り専用な変数なので特徴も異なります。

以下がreadonlyの特徴です。


  • readonlyは実行時に値が決定するので、必ずしも同じ値になるとは限らない。つまり、デフォルトではstaticとして宣言されていないのでインスタンスの中にある変数の一つであると見なすことができる。


  • 宣言時もしくはコンストラクタ内で初期化・変更が可能

  • ローカル変数には使えない(クラスのメンバ変数のみ)。


  • 普通の変数と同等の扱いをされる。変数は共有されるのでconstに比べるとサイズは小さい。

  • switchやデフォルト引数で使用不可。

  • 埋め込みではないのでアセンブリを差し替えればロードし直され、変更した値が使用される

  • 実行時定数は実行時に評価されるため、値そのものではなくreadonly変数を参照する中間言語(IL)が生成される。つまりconstのようにリテラルに置き換わらない。

  • インスタンスをnewした結果やメソッドの実行を伴うような値を割り当てられる。

  • .NETではJIT(Just In Time Compile)が行われるので、ソースコード中で値の変わる変数に見えても実質的に値が変わらないようなら定数としてコンパイルが行われる

  • C#7.2からは構造体自体にreadonlyをつけられるようになった。readonlyの構造体のフィールドも全てreadonlyとしなければならない。また、thisもreadonly扱いになる。

ちなみにJITについては以下で解説しています。

Unity開発するにあたって知っておきたいコンパイラのすゝめ(Part0.7~JITとAOT概要編~)

class Hoge

{
public readonly int Piyo;
public readonly Dog taro;
Hoge (int huga)
{
//コンストラクタ内で書き換え可能
this.Piyo = huga;
this.taro = new Dog();
}

void Func(int mohu)
{
int muga = this.Piyo;
// 書き込み不可でエラー
this.Piyo = mohu;
}
}

 

static readonly



定数値が必要だが、その値の型がconst宣言では使用できない場合、またはその値をコンパイル時に計算できない場合はstatic readonlyフィールドが役に立ちます。

Effective C#では、バージョニング問題の観点からconstよりもstatic readonlyの使用が推奨されています。


結論

readonlyは読み取り専用であることを表明するのに使用すると良いです。

constconst対象がprivateである場合、又は高いパフォーマンスが求められていて、 なおかつ将来にわたって変更されることがないことが明らかな場合にのみ使用するべきでしょう。

そうでなく、将来変更される可能性のある値を定数として公開する場合にはstatic readonlyの使用をお勧めします。


参考