C# のプロパティ構文のアクセス修飾子の指定

C#のプロパティは、setter/getterのシンタックスシュガーです。C#はプロパティがかなり便利なので基本的な事を紹介したいと思います。

まずはプロパティの書き方

まず、書き方のおさらいです。setterはset、getterはgetキーワードがコンテキストに依存した予約語になっています。それぞれおキーワードの前にアクセス修飾子を指定することができます。

// 値の取得、設定ともに公開する時の書き方
public string Str { get; set; }

// 値の取得だけ公開、設定はクラス内のみとする場合
public string Str { get; private set; }

// 値の取得も設定もクラス内部のみに制限する場合
private string Str { get; set; }

宣言の頭で宣言したアクセス修飾子より広い範囲への指定はできません。

// これはエラーになる
private string Str { public get; set; }

指定可能なアクセス修飾子は変数と同じで以下の通りです。

  • public : 全員に公開
  • protected : 派生クラスからのみアクセス可能
  • protected internal : 同じアセンブリ(DLLとかEXE)内の派生クラスからのみアクセス可能
  • internal : 同じアセンブリ内からのみアクセス可能
  • private : クラス内でのみアクセス可能

従って、以下のような変わったな宣言も可能です。ライブラリ作るときはこういった宣言も使うかもしれません。

// 設定はパッケージ内の派生クラスからのみ、取得は外部からできない
public string Str { private get; protected internal set; }

自動実装ではないプロパティの書き方

自分で宣言した変数へのアクセスをプロパティ経由で操作する場合以下のように記述ができます。

public class Sample
{
    private string _Str;

    public string Str
    {
        get { return _Str; }
        set { _Str = value; }
    }
}

setの直後の中括弧内ではvalueが引数の予約語として使えるので、クラス内の変数に設定するためにイコールの右側に指定することができます。このvalueの型はプロパティ宣言と同じ型です。

使いどころ

基本的に「クラス外へ公開する変数は全てプロパティを経由したほうがいい」と思います。呼び出し側のコードは記述方法は、変数を参照する場合もプロパティを参照する場合も変わらないのですが後から相互に変更ができません。

// 以下のコードは一見同じようにアクセスできます。

// 変数をそのまま公開しているクラス
public class Sample
{
    public string Str;
}

// プロパティで宣言しているクラス
public class SampleProp
{
    public string Str { get; set; }
}

public class User 
{
    public static void ClientMethod()
    {
        // 同じようにアクセス可能
        var s1 = new Sample();
        s1.Str = "Test";

        var s2 = new SampleProp();
        s2.Str = "Test";
    }
}

このコード、コンパイル後にはおおむね以下のような構文に展開されます。setterがメソッドに置き換えになっている。

public class User 
{
    public static void ClientMethod()
    {
        // コンパイルすると裏で違うアクセス方法になっている
        var s1 = new Sample();
        s1.Str = "Test";

        var s2 = new SampleProp();
        s2.set_Str("Test");
    }
}

この性質故に、別のDLLでフィールド変数がプロパティに変更された場合、利用者側では「変数が見つからない」というエラーが発生する可能性があります。このため、あらかじめ公開予定の変数はプロパティ化しておいた方がトラブルが少ないとです。

現場で見かける変なコード

稀によく見かけるヘンテココードのご紹介です。アンチパターンなのでこうならないように気を付けましょう。

代入の意味が変わってしまっている

プロパティが代入と取り出し以上の意味で実装されているケースです。利用者の視点に立って、代入時に値の範囲をチェックして例外を投げる、や、状況に応じて設定できる値が変わる、などは問題ないのですが、代入するときに値を3倍にして保存する操作など、代入のというより処理的な行為がプロパティで実行されると、混乱のもとになるのでそういったことはしないようにしましょう。

例えば以下のようなコードです。

public class Sample
{
    private int _num;
    public string Num
    {
        get { return _num * 10; } // 取り出すときは10倍にして返す
        set
        {
            _num = value * 3; // 入れる時は3倍にして保存する
        }
    }
}
元の変数が全然保護できていない

只のミスなのかもしれませんが、稀によく見かけます。

public class Sample
{
    // ★元の変数も公開しちゃってる
    public string _Str;

    public string Str
    {
        get { return _Str; }
        set
        {
            if(string.IsNullOrEmpty(value))
            {
                throw new ArgumentException("value");
            }
            _Str = value; 
        }
    }
}

たぶん、nullと空白を入れてほしくないローカル変数と思われますが元の変数を公開しちゃってるせいでまったく意味が無い感じです。ただこうなっている場合、下手に後から_Strをprivateに変更するとコンパイルエラーが出まくる & 今まで動いていた箇所が動かなくなるなど事故につながる可能性があります。

インスタンスの保護がおろそか
public class Sample
{
    private IList<string> _StrList;

    // setもgetもpublicで公開しちゃってる
    public IList<string> StrList { get; set; }

    public Sample() { this._StrList = new List<string>(); }
}

こういったコードけっこう見かけますが危ないですね。オブジェクト参照ごと変更できるコードです。この場合、クラス外部でプロパティに最初に設定されているインスタンスを外部から入れ替えられてしまいます。IListを継承した自作クラスをStrListプロパティへ設定される事は大抵の場合、不完全にしか想定されないためオブジェクトをカスタムされた自作クラスなどに変更されるとトラブルのもととなります。

public static class Client
{
    public static void ClentMethod(Sample s)
    {
        // クラス外部でインスタンスを変更できてしまう
        s.StrList = new SortedList<string>();
        s.StrList.Add("AA");
        s.StrList.Add("A");
    }
}

上記例のように、どこかでインスタンスをごっそり変更されると今までの値が消滅 & インスタンスの実体が変わったことによる挙動の違いでトラブルが起きます。こういう場合正しくは set は private へ変更します。一番焦ったのはオブジェクトがシリアライズできなくなって、データが転送できないとかありました。。。

public class Sample
{
    private IList<string> _StrList;

    // setはprivateに変更する
    public IList<string> StrList { get; private set; }

    public Sample() { this._StrList = new List<string>(); }
}

プリミティブ型(int, double etc... + string型)は特に問題ないのですがオブジェクトの参照型の場合、自分の管理するインスタンスを変更されないようにしましょう。特に自分で作ったクラスの派生クラスなんか設定されて動かないといわれてもこっちも困りますね。