C#を勉強しなおしてみた(6)
readonlyとconst

どもです。

本投稿は、以下の投稿の続きモノです。

今回は、「const」と「readonly」について書きます。

1. 「const」とは

MSDNの解説によれば、constは、

定数フィールドまたはローカル定数を宣言するために使用する

と解説されています。Equalsの場合同様、この1文だけで完結されても、ピンときませんが、MSDNのページにもう少し説明がありました。それによれば、

  • 定数フィールドと(定数?)ローカルは変数ではないため、変更できない。
  • いずれかの時点で変わることが予想される情報を表すために、定数を作成指定はいけない。
  • (C# 10以降)使用される全ての式も定数文字列である場合には、補完された文字列が定数になることがある。

ということです。

これらの内容を、1つずつ、簡単ではありますが確認していきます。

1.1. 変更できない

まず、定数フィールドと(定数?)ローカルは変数ではないため、変更できないについてです。

「const」は「定数の、値の変更ができない」といった意味です。そのため、「const」が付与された「定数フィールドと定数ローカル」は「定数」であり、「変更できない」ということになります。

…そのままんま、ですね。

1.2. いずれかの時点で変わることが予想される情報を…

次に、いずれかの時点で変わることが予想される情報を表すために、定数を作成指定はいけないについてです。

「const」が付与された「定数」(定数式)は、コンパイル時に評価されます。即ち、ビルド時に「定数」の「値」が決定します。仮にコンパイル/ビルドを実行した後に、「定数」の「値」を変更することになった場合、定数を宣言するコードを変更し、再ビルドが必要になります。ビルド、およびテストの再実施が短時間で完了する、影響範囲が狭いといった場合は特に問題にならないと思います。しかし現実は、そんなことはありません。(ビルドは時間がかかったりしますし、影響範囲は広い、最悪測りきれず、全テスト再実施になることもあるとかないとか…。)そういった事態に陥らないように、注意が必要です。

…スミマセン、一部私情が入った記載になってしまいました。

1.3. 補完された文字列が定数になることがある。

次に、(C# 10以降)使用される全ての式も定数文字列である場合には、補完された文字列が定数になることがあるについてです。

…いや、「定数になることがある」と言われても…。「なる」場合と「ならない」場合の条件を明確にしてくれよ…。

ここで「補完された文字列」とあるので、「$を使用した文字列補完」が対象となっていることが考えられます。例として、以下のようなコードが紹介されています。

const string Language = "C#";
const string Platform = ".NET";
const string FullProductName = $"{Platform} - Language: {Language}";

このコードでは、"FullProductName"という名前の定数を作成するに当たり、"Language"と"Platform"の値を使用しています。そのため、"FullProductName"の一部を変更する際には、"Language"または"Platform"のいずれか変更したい箇所を変更することで、"FullProductName"も変更することができる、という内容のようです。

2. 「readonly」とは

MSDNでは、以下のように解説されています。

  • フィールドの宣言では、readonly は、フィールドへの割り当てが、宣言の一部として、または同じクラスのコンストラクター内でのみ可能であることを示します。
  • readonly struct 型定義では、readonly は構造体型が変更不可であることを示します。
  • 構造体型内のインスタンス メンバー宣言では、readonly は、インスタンス メンバーによって構造体の状態が変更されないことを示します。
  • ref readonly メソッドの戻り値では、readonly 修飾子は、メソッドが参照を返し、その参照への書き込みが許可されないことを示します。

これらの解説について、簡単ではありますが確認してみます。

2.1. readonlyについて

「readonly」の解説のうち、最初の解説の中の「宣言の一部として、または同じクラスのコンストラクター内でのみ可能であることを示します」という部分が最も重要かと思います。

これはつまり、「readonly」の「フィールド」は、「宣言」あるいは「コンストラクタ」の中でしか値を割り当てられない、ということです。もっと簡単に言うと、一度インスタンス化したら、それ以降は変更ができない、ということです。

実際にコードを書いて、確認してみます。

以下のような、readonlyフィールドを持つクラスについて考えます。

class Person
{
	readonly string family = "suzuki";
	readonly string name = "taro";
	readonly int age = 1;

	public Person() { }

	public Person(string family, string name, int age)
	{
		this.family = family;
		this.name = name;
		this.age = age;
	}

	public void Display()
	{
		Console.WriteLine($"family = {family}");
		Console.WriteLine($"name = {name}");
		Console.WriteLine($"age = {age}");
	}
};

このクラスについて、何も設定せずにインスタンス化し、Display()メソッドを実行します。

var person = new Person();
person.Display();

このコードをビルド、実行すると以下のようになります。



このように、フィールドの宣言時に初期化した値が表示されることが分かります。

次に、コンストラクタで値を指定してみます。コードは、以下です。

var person = new Person("Yamada", "Jiro", 12);
person.Display();

このコードをビルド、実行した結果が以下です。



このように、コンストラクタの引数で指定した値が表示されることが分かります。

では最後に、"family"フィールドの値を変更するメソッドを追加して、インスタンスの値を変更できるようにしてみます。追加するメソッドは、以下。

class Person
{
	readonly string family = "suzuki";
	readonly string name = "taro";
	readonly int age = 1;

	public Person() { }

	public Person(string family, string name, int age)
	{
		this.family = family;
		this.name = name;
		this.age = age;
	}

	public void Display()
	{
		Console.WriteLine($"family = {family}");
		Console.WriteLine($"name = {name}");
		Console.WriteLine($"age = {age}");
	}

    public void SetFamily(string family)
    {
        this.family = family;	// ここでコンパイルエラー!
    }
};

上述のコード内にも書いたように、フィールドに値を代入する処理で、エラーが発生します。発生するエラーの内容は、以下の通りです。

読み取り専用フィールドに割り当てることはできません (フィールドが定義されている型のコンストラクターか init 専用セッター、または変数初期化子では可)



以上から、readonlyが付与されたフィールドは、インスタンス化後は変更することができない、ということが確認できました。

2.2. プロパティやメソッドのreadonly

readonlyは、条件付きですが、メソッドにも付与することができます。この条件とは、refと併せて使用するということです。メソッドを具体例に挙げて見てみます。

class Person
{
	readonly string family = "suzuki";
	readonly string name = "taro";
	readonly int age = 1;

	public Person() { }

	public Person(string family, string name, int age)
	{
		this.family = family;
		this.name = name;
		this.age = age;
	}

	public void Display()
	{
		Console.WriteLine($"family = {family}");
		Console.WriteLine($"name = {name}");
		Console.WriteLine($"age = {age}");
	}

    /// 
	/// 追加したメソッド
	/// 
	public ref readonly string GetName()
	{
		return ref name;
	}
};

次に、GetNameメソッドを使用して、nameフィールドの値を取得してみます。コードは、以下の通りです。

var person = new Person("Yamada", "Jiro", 12);
string name = person.GetName();
Console.WriteLine(name);

このコードの実行結果は、以下のとおりです。



“ref readonly"を付与したメソッドでも、値が取得できることが確認できました。では、"ref readonly"を付与したメソッドと付与していないメソッドでは、何が違うのでしょう?違いは、「readonlyが付与されたフィールドを返すか否か」です。「ref readonly」が付与されたメソッドで、readonlyが付与されていない値、もしくはフィールドを返そうとした場合、ビルドエラーが発生します。実際に、「ref readonly」が付与されたメソッドで、readonlyが付与されていないフィールドの値を返す場合、以下のようなエラーが発生します。

参照渡しによって渡したり返したりすることができないため、このコンテキストで使用できない式があります



3. まとめ

今回は、「const」と「readonly」の内容を確認してみました。どちらも変更できないようにする、という事では同じですが、値が割り当てられるタイミング、目的など細かい点で違いがあります。そのため、定数を扱う際には「const」と「readonly」のどちらを使用することが適切かは、注意して検討する必要があります。

ちなみに私は、なかなか「定数」を扱うプログラムを書かないので、自分なりの使い分けの基準を持ち合わせていません。どなたか、方針を持っている方、教えてください。

この記事が誰かの助けになれば幸いです。ではっ!