概要
「定数」で、読み取り専用のフィールドが作れるという話をしました。
この時点ではまだクラスや構造体、値型と参照型の違いなどについて触れていなかったのでreadonly
修飾子の簡単な紹介だけに留めましたが、
本項で改めてreadonly
について説明します。
整数などの基本的な型に対して使う分には特に問題は起きないんですが、構造体やクラスなど、複合型に対して使うときには注意が必要です。
参照型のフィールドに対して readonly
readonly
に関して最も注意が必要な点は、readonly
は再帰的には働かないという点です。
readonly
を付けたその場所だけが読み取り専用になり、参照先などについては書き換えが可能です。
例えば以下のコードを見てください。Program
クラスのフィールドc
にはreadonly
が付いていますが、
c
が普通に書き換え可能なクラスのフィールドなので、クラスの中身は自由に書き換えられます。
// 書き換え可能なクラス
class MutableClass
{
// フィールドを直接公開
public int X;
// 書き換え可能なプロパティ
public int Y { get; set; }
// フィールドの値を書き換えるメソッド
public void M(int value) => X = value;
}
class Program
{
static readonly MutableClass c = new MutableClass();
static void Main()
{
// これは許されない。c は readonly なので、c 自体の書き換えはできない
c = new MutableClass();
// けども、c の中身までは保証してない
// 書き換え放題
c.X = 1;
c.Y = 2;
c.M(3);
}
}
クラスを書き換えできないように作る場合、クラス自体を書き換え不能に作りましょう。
(クラスの方で、フィールドをreadonly
にしたり、プロパティをget-onlyにします。)
値型のフィールドに対して readonly
クラス(参照型)とは対照的に、構造体(値型)の場合はデータを直接持ちます。
そのため、構造体のフィールドに対してreadonly
を付けると、構造体の中身も読み取り専用になります。
ただし、メソッドの呼び出しなどを行う際、コピーが発生するという別の注意が必要です。
例えば以下のように、readonly
が付いたフィールドc
自体に加えて、c
のフィールドも書き換えできません。
using System;
// 書き換え可能なクラス
struct MutableStruct
{
// フィールドを直接公開
public int X;
// フィールドの値を書き換えるメソッド
public void M(int value) => X = value;
}
class Program
{
static readonly MutableStruct c = new MutableStruct();
static void Main() => Allowed();
private static void NotAllowed()
{
// これはもちろん許されない。c は readonly なので、c 自体の書き換えはできない
c = new MutableStruct();
// 構造体の場合、フィールドに関しては readonly な性質を引き継ぐ
c.X = 1;
}
private static void Allowed()
{
// でも、メソッドは呼べてしまう
c.M(3); // X を 3 で上書きしているはず?
Console.WriteLine(c.X); // でも、X は 0 のまま
//↑のコードは、実はコピーが発生している
// 以下のコードと同じ意味になる
var local = c;
local.M(3);
Console.WriteLine(c.X); // 書き換わってるのは local (コピー)の方なので、c は書き換わらない(0)
Console.WriteLine(local.X); // もちろんこっちは書き換わってる(3)
}
}
この例の後半を見ての通り、メソッドは呼べてしまいます。
フィールドX
は書き換えれないはずなのに、そのX
を書き換えているメソッドM
を呼んでもエラーになりません。
C# では、こういう場合に、readonly
であることを保証しつつメソッドを呼び出せるように、フィールドを一度コピーしてから、そのコピーに対してメソッドを呼ぶということをしています。
すなわち、コピーが発生してまずいような場合(例えば構造体のサイズが大きくてコピーにコストが掛かるとか)には、readonly
なフィールドを使うことで問題が発生することがあります。
この問題は、in
引数などでも発生しまえます。
後述するreadonly struct
を使えばこの問題は少し緩和するので、そちらも参照してください。
構造体の this 書き換え
C# のreadonly
フィールドには少し片手落ちなところがあって、実は、構造体の場合にちょっとした問題を起こせたりします。
構造体のメソッドの中ではthis
が「自分自身の参照」の意味なんですが、このthis
参照は書き換えできてしまいます。
そのため、以下のように、readonly
で一見書き換えができなさそうなフィールドを書き換えてしまうことができます。
using System;
struct Point
{
// フィールドに readonly を付けているものの…
public readonly int X;
public readonly int Y;
public Point(int x, int y) => (X, Y) = (x, y);
// this の書き換えができてしまうので、実は X, Y の書き換えが可能
public void Set(int x, int y)
{
// X = x; Y = y; とは書けない
// でも、this 自体は書き換えられる
this = new Point(x, y);
}
}
class Program
{
static void Main()
{
var p = new Point(1, 2);
// p.X = 0; とは書けない。これはちゃんとコンパイル エラーになる
// でも、このメソッドは呼べるし、X, Y が書き換わる
p.Set(3, 4);
Console.WriteLine(p.X); // 3
Console.WriteLine(p.Y); // 4
}
}
わざわざこんな紛らわしいことをしようとは思わないのでめったに問題になることはないんですが、一応は注意が必要です。 また、この問題は、次節で説明する通り、C# 7.2で少し緩和されます。
readonly struct
Ver. 7.2
C# 7.2で、構造体自体にreadonly
修飾を付けられるようになりました。
readonly
を付けた構造体は以下のような状態になります。
-
全てのフィールドに対して
readonly
を付けなければならなくなる- get-onlyプロパティは使えます(自動生成されるフィールドが
readonly
なので問題ない)
- get-onlyプロパティは使えます(自動生成されるフィールドが
this
参照もreadonly
扱いされる
this
がreadonly
扱いになるので、前節のようなthis
書き換えの問題は起きません。
using System;
// 構造体自体に readonly を付ける
readonly struct Point
{
// フィールドには readonly が必須
public readonly int X;
public readonly int Y;
public Point(int x, int y) => (X, Y) = (x, y);
// readonly を付けない場合と違って、以下のような this 書き換えも不可
//public void Set(int x, int y) => this = new Point(x, y);
}
class Program
{
static void Main()
{
var p = new Point(1, 2);
// p.X = 0; とは書けない。これはちゃんとコンパイル エラーになる
// p.Set(3, 4); みたいなのもダメ
Console.WriteLine(p.X); // 1 しかありえない
Console.WriteLine(p.Y); // 2 しかありえない
}
}
readonly struct によるコピー回避
前述の通り、(無印の)構造体のreadonly
フィールドに対してメソッドを呼ぶとコピーが発生するという問題があります。
これに対して、readonly struct
であれば、このコピーを回避できます。
例えば以下のように、ほぼ同じ構造・どちらも書き換え不能な構造体を作ったとして、readonly struct
になっているかどうかでコピー発生の有無が変わります。
using System;
// 作りとしては readonly を意図しているので、何も書き換えしない
// でも、struct 自体には readonly が付いていない
struct NoReadOnly
{
public readonly int X;
public void M() { }
}
// NoReadOnly と作りは同じ
// ちゃんと readonly struct
readonly struct ReadOnly
{
public readonly int X;
public void M() { }
}
class Program
{
static readonly NoReadOnly nro;
static readonly ReadOnly ro;
static void Main()
{
// readonly を付けなかった場合
// フィールド参照(読み取り)は問題ない
Console.WriteLine(nro.X);
// メソッド呼び出しが問題。ここでコピー発生
// (呼び出し側では、「M の中で特に何も書き換えていない」というのを知るすべがないので、防衛的にコピーが発生)
nro.M();
// readonly を付けた場合
// これなら、M をそのまま呼んでも何も書き換わらない保証があるので、コピーは起きない
ro.M();
}
// これも問題あり(コピー発生)
// in を付けたので readonly 扱い → M を呼ぶ際にコピー発生
static void F(in NoReadOnly x) => x.M();
// こちらも、readonly struct であれば問題なし(コピー回避)
static void F(in ReadOnly x) => x.M();
}
C# 7.2 以降では、書き換えを意図していない構造体に対してはreadonly
修飾を付けるのが無難でしょう。
また、「フィールド直接参照なら大丈夫だけど、メソッドを(プロパティも)呼ぶとコピー発生」という性質上、
書き換えを最初から意図している構造体の場合は、プロパティよりも、フィールドを直接public
にしてしまう方が都合がいいことがあります。