PG日誌

受託系 PG が C# の事を書いています

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

本来プロパティは、setter/getterのシンタックスシュガーです。c#は(自動実装)プロパティが便利すぎてsetter/getterだという事を高頻度で忘れてしまうので改めて基本的な事を確認したいと思います。

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

まず、書き方のおさらいです。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 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";
    }
}

このコード、コンパイルすると裏で全然違う構文に展開されています。

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

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

このため別のDLLでフィールド変数がプロパティに変わってしまうと利用者側でコードが変わるため変数が見つかりませんというエラーが起きるため公開する、もしくは公開する可能性がある変数はあらかじめプロパティ化しておいた方が幸せになれると思います。

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

これが一番主張したい事なんですが、現場でこんなコードを見かけることがたまにあります。

元の変数が全然保護できていない
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へ変更します。一番焦ったのはオブジェクトがシリアライズできないでデータが転送できないとかありましたね。。(その時は、型が変更されることを想定していなかったのでオブジェクトの属性のチェックをsetで実施していませんでしたがそういう事されることもあるんだ・・・と思い知りました)

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型)は特に問題ないのですがオブジェクトの参照型の場合、自分の管理するインスタンスを変更されないようにしましょう。特に自分で作ったクラスの派生クラスなんか設定されて動かないといわれてもこっちも困りますね。