プロパティ

プロパティとは

C#のクラス内にはフィールド(変数)とメソッド(関数)が持てる、と説明しました。
もうひとつ、プロパティ(Property)というものも持つことができます。

プロパティはフィールドに似ています。
しかしただの変数ではなく、関数的な動作をさせることもできます。

以下にまず簡単な例を示します。


private class SimpleClass
{
    //フィールド
    int privateNum;

    //プロパティ
    int Num {
        get
        {
            return privateNum;
        }
        set
        {
            privateNum = value;
        }
    }
}

プロパティは名前の定義の後に「get」「set」というものがあります。
これはアクセサというものです。
(アクセサを書かなければフィールドになります)
getはgetアクセサ、setはsetアクセサと呼びます。
(getter(ゲッター)、setter(セッター)ともいいます)

getには値の読み取りがあったときの動作を記述します。
値を取得する必要があるので、必ずreturn文で値を返します。
「return privateNum;」とすれば、フィールドprivateNumの値が返されます。

setには値の書き込みがあったときの動作を記述します。
setブロック内ではvalueキーワードが使用でき、これにアクセス元から渡された値が格納されています。
「privateNum = value;」とすれば、フィールドprivateNumに値が書き込まれます。

実際にプロパティを使用する場合は以下のようになります。


private class SimpleClass
{
    //フィールド
    int privateNum;

    //プロパティ
    int Num {
        get
        {
            return privateNum;
        }
        set
        {
            privateNum = value;
        }
    }

    public void TestMethod()
    {
        //プロパティから読み取り
        Console.WriteLine(Num);

        //プロパティに書き込み
        Num = 2;

        Console.WriteLine(Num);
    }
}

static void Main(string[] args)
{
    var sc = new SimpleClass();
    sc.TestMethod();

    Console.ReadLine();
}
0
2

このNumプロパティは、フィールドprivateNumへのデータの橋渡しをしているだけのごく単純なものです。
これはprivateNumを直接読み書きするのとまったく同じです。

動作を少し変更してみましょう。


private class SimpleClass
{
    //フィールド
    int privateNum;

    //プロパティ
    int Num {
        get
        {
            return privateNum;
        }
        set
        {
            //0未満の値の場合は0を代入
            privateNum = value < 0 ? 0 : value;
        }
    }
    
    public void TestMethod()
    {
        //-2を代入する
        //プロパティの処理により0に置き換えられる
        Num = -2;

        Console.WriteLine(Num); //0
    }
}

プロパティは、アクセスする側から見ればフィールドと全く同じように扱えます。
プロパティ側ではまるでメソッドのようにデータを加工することができます。

これと同じことはメソッドでも可能ですが、プロパティを用いれば手軽に実現できるわけです。
ちなみにメソッドで同様の処理をする場合は以下のようになります。


private class SimpleClass
{
    int privateNum;

    int Num()
    {
        return privateNum;
    }

    void Num(int value)
    {
        privateNum = value < 0 ? 0 : value;
    }
}

自動実装プロパティ

プロパティは自動実装プロパティという記述方法を用いればより簡潔に書けます。
(C#3.0以上)


private class SimpleClass
{
    int Num { get; set; }
}

記述はこれだけです。
自動実装プロパティは、getやsetの処理を書かずにいきなりセミコロンで行を終わらせます。
このようにすると自動的にint型のフィールドが作られ、値の読み書きが可能になります。
(勝手にフィールドが生成されるので容量が節約できるわけではない。あくまでも記述が簡単になるだけ)


private class SimpleClass
{
    int Num { get; set; }
}

//↑これは
//↓これと同じ

private class SimpleClass
{
    int _num;
    int Num
    {
        get { return _num; }
        set { _num = value; }
    }
}

実際に自動生成されるフィールド名は上記とは異なります。

自動実装プロパティの初期値

自動実装プロパティは初期値を与えることができます。


class SimpleClass
{
    int Num { get; set; } = 10;
}

get/setでアクセスレベルを変える

上記のプロパティはgetとsetで同じアクセスレベルでしたが、個別に設定することもできます。


private class SimpleClass
{
    public int Num1 { get; private set; }

    private int _num2;
    public int Num2
    {
        get { return _num2; }
        private set { _num2 = value < 0 ? 0 : value; }
    }
}

static void Main(string[] args)
{
    var sc = new SimpleClass();

    //読み取りは可能
    Console.WriteLine(sc.Num1);
    Console.WriteLine(sc.Num2);

    //書き込みは不可
    sc.Num1 = 1; //エラー
    sc.Num2 = 1; //エラー
}

まずプロパティ自体のアクセス修飾子で、privateよりも制限の広いアクセス許可を与えます。
次に、getかsetのどちらかに、先頭に指定したアクセス修飾子よりも狭い範囲のアクセス修飾子を記述します。

上記のコードだと、「値の代入はprivate」「値の取得はpublic」というプロパティになります。
この形は、値は外部に公開したいが不用意に値を変更されると困るような場合によく用いられます。

アクセス制限を個別に設定する場合は、先頭に与えたアクセス修飾子よりも狭い範囲のものしか使用できません。
また、getとsetの両方にアクセス修飾子を与えることはできません。

getのみのプロパティ

プロパティはgetのみを記述することもできます。


private class SimpleClass
{
    int Num { get; }
}

このプロパティは値を代入することはできなくなります。
同じクラス内からであっても代入できません。
初期値を与えるか、コンストラクター内でのみ値を初期化できます。

getのみの自動実装プロパティは以下と同じような意味になります。


private class SimpleClass
{
    //読み取り専用
    readonly int _num;

    int Num { get { return _num; } }
}

そもそも代入ができない場合もgetのみを記述します。


class SimpleClass
{
    string str = "test";

    int StrLength
    {
        get { return str == null ? 0 : str.Length; }
    }
}

ラムダ式を与える

プロパティにはラムダ式を指定することもできます。
ただしここで使用できるのは一行で書けるラムダ式です。
(式形式という)
複数行、つまりブロック{}を使用するラムダ式は書けません。


class SimpleClass
{
    private readonly float tax = 1.08f;

    public int Price = 1980;

    //ラムダ式
    public int PriceIncludeTax => (int)(Price * tax);
}

C#7.0からはsetもラムダ式で書くことができます。


class SimpleClass
{
    private readonly float tax = 1.08f;

    public int Price = 1980;
    public int PriceIncludeTax
    {
        //ラムダ式
        get => (int)(Price * tax);
        set => price = (int)Math.Ceiling(value / tax);
    }
}

Math.Ceilingは小数点以下を切り上げるメソッドです。

インデクサー(添え字演算子)

クラスのフィールドやプロパティへのアクセスはドット演算子を使用しますが、インデクサー(添え字演算子)を使用してアクセス可能にもできます。


private class ArrayClass
{
    private List<int> lst = new List<int>() {
        0, 1, 2, 3, 4
    };

    //インデクサー
    public int this[int x]
    {
        get
        {
            return 0 <= x && x < lst.Count ?
                lst[x] : 0;
        }
        set
        {
            if(0 <= x && x < lst.Count)
                lst[x] = value;
        }
    }
}

static void Main(string[] args)
{
    ArrayClass ac = new ArrayClass();

    //自作クラスに添え字でアクセス可能
    int num = ac[0];
    ac[1] = 99;

    Console.ReadLine();
}

インデクサーの定義はプロパティの定義とほぼ同じで、プロパティ名の代わりにthisを指定します。
続いて角括弧(添え字演算子)を記述し、データ型と引数名を添え字名します。

添え字には整数値以外も指定できます。
つまり辞書クラスのようにも使用できます。


private class DictClass
{
    private string[] keys = new string[] {
        "apple",
        "orange",
        "grape"
    };
    private string[] values = new string[] {
        "リンゴ",
        "オレンジ",
        "グレープ"
    };

    //インデクサー
    public string this[string x]
    {
        get
        {
            int index = Array.IndexOf(keys, x);
            if (index >= 0)
                return values[index];
            return "";
        }
        set
        {
            int index = Array.IndexOf(keys, x);
            if (index >= 0)
                values[index] = value;
        }
    }
}

static void Main(string[] args)
{
    DictClass dc = new DictClass();

    string str = dc["apple"];
    dc["orange"] = "みかん";

    Console.ReadLine();
}