Quantcast
Channel: ++C++; // 未確認飛行 C
Viewing all articles
Browse latest Browse all 75

readonly の注意点

$
0
0

概要

定数」で、読み取り専用のフィールドが作れるという話をしました。 この時点ではまだクラス構造体値型と参照型の違いなどについて触れていなかったので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を付ける例

クラスを書き換えできないように作る場合、クラス自体を書き換え不能に作りましょう。 (クラスの方で、フィールドを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)
    }
}

値型のフィールドに対してreadonlyを付ける例

この例の後半を見ての通り、メソッドは呼べてしまいます。 フィールド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 を付けなければならなくなる
  • this参照もreadonly扱いされる

thisreadonly扱いになるので、前節のような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にしてしまう方が都合がいいことがあります。


Viewing all articles
Browse latest Browse all 75

Trending Articles