目次

キーワード

概要

プロパティ(property:所有物、特性)とは、JavaやC++にはない(Visual Basicにはある)機能で、 クラス外部から見るとメンバー変数のように振る舞い、 クラス内部から見るとメソッドのように振舞うものです。

JavaやC++がこの機能を持ってないことからも分かると思いますが、 プロパティはオブジェクト指向言語に必須の機能ではありません。 しかし、これから説明していくように、あると便利なものです。

ポイント
  • プロパティ: 中(実装側)からはメソッドのように扱え、外(利用側)からはメンバー変数のように見えるもの。

  • 実装の隠蔽(カプセル化)の原則を崩すことなく、 アクセサー関数の煩雑さを解消。

プロパティとは

実装の隠蔽」で、 メンバー変数はクラス外部から直接アクセス出来ないようにして、 オブジェクトの状態の変更はすべてメソッドを通して行うべきだと書きました。 これを忠実に実行すると、クラスを利用する側のコードは以下の例のように少々見栄えの悪いものになってしまいます。

using System;

// 「実装の隠蔽」で作った複素数クラス
class Complex
{
  // 実装は外部から隠蔽(privateにしておく)
  private double re; // 実部を記憶しておく
  private double im; // 虚部を記憶しておく

  public double Re(){return this.re;}    // 実部を取り出す
  public void Re(double x){this.re = x;} // 実部を書き換え

  public double Im(){return this.im;}    // 虚部を取り出す
  public void Im(double y){this.im = y;} // 虚部を書き換え

  public double Abs(){return Math.Sqrt(re*re + im*im);}  // 絶対値を取り出す
}

// クラス利用側
class ConcealSample
{
  static void Main()
  {
    // x = 5 + 1i
    Complex x = new Complex();
    x.Re(5);  // x.re = 5
    x.Im(1);  // x.im = 1

    // y = -2 + 3i
    Complex y = new Complex();
    y.Re(-2); // y.re = -2
    y.Im( 3); // y.im =  3

    Complex z = new Complex();
    z.Re(x.Re() + y.Re()); // z.re = x.re + y.re
    z.Im(x.Im() + y.Im()); // z.im = x.im + y.im

    Console.Write("|{0} + {1}i| = {2}\n", z.Re(), z.Im(), z.Abs());
    // |3 + 4i| = 5 と表示される
  }
}

void Re(double x)double Re()などの、 メンバー変数の値の取得・変更を行うためのメソッドのことをアクセサー(accessor)といいます。 C++やJavaなどの言語では、下手をするとメンバー変数の数だけアクセサーが存在するという状態になることもあります。 C++やJavaではアクセサーのメソッド名はvoid SetRe(double x)double GetRe()というように、メンバー変数名に Set/Get をつけた物を使うことが多く、メンバ変数の数だけ Set/Get で始まるメソッドのペアができ、ちょっと見苦しいものになります。 (参考: 「Set / Get とプロパティ」)

また、クラス作成側からすると、オブジェクトの状態の取得・変更はすべてメソッドを通して行ったほうがいいのですが、 クラス利用側からすると、メンバー変数に値を直接代入するほうが見た目がすっきりします。

このような理由から、 C#では クラス内部から見るとメソッドのように振る舞い、 クラス利用側から見るとメンバー変数のように振舞う プロパティという機能を用意しました。 プロパティの定義の仕方は以下のような書式になります。

アクセスレベル 型名 プロパティ名
{
    set
    {
        // setアクセサー(setter とも言う)
        //  ここに値の変更時の処理を書く。
        //  value という名前の変数に代入された値が格納される。
    }
    get
    {
        // getアクセサー (getter とも言う)
        //  ここに値の取得時の処理を書く。
        //  メソッドの場合と同様に、値はreturnキーワードを用いて返す。
    }
}

set 以降のブロックに値の変更用の処理を、 get 以降のに値の取得用の処理を書きます。 これらを、set アクセサー、get アクセサーと呼びます。 あるいは、通称では settergetter と呼んだりします。

例えば先ほどの複素数クラスのアクセサーをプロパティを使って書き換えると以下のようになります。

using System;

// クラス定義
class Complex
{
    // 実装は外部から隠蔽(privateにしておく)
    private double re; // 実部を記憶しておく
    private double im; // 虚部を記憶しておく

    // 実部の取得・変更用のプロパティ
    public double Re
    {
        set { this.re = value; }
        get { return this.re; }
    }
    /* ↑のコードは意味的には以下のコードと同じ。
    public void SetRe(double value){this.re = value;}
    public double GetRe(){return this.re;}
    メソッドと同じ感覚で使える。
    */

    // 実部の取得・変更用のプロパティ
    public double Im
    {
        set { this.im = value; }
        get { return this.im; }
    }

    // 絶対値の取得用のプロパティ
    public double Abs
    {
        // 読み取り専用プロパティ。
        // setブロックを書かない。
        get { return Math.Sqrt(re * re + im * im); }
    }
}

// クラス利用側
class PropertySample
{
    static void Main()
    {
        Complex c = new Complex();
        c.Re = 4; // Reプロパティのsetアクセサーが呼び出される。
        c.Im = 3; // Imプロパティのsetアクセサーが呼び出される。
        Console.Write("|{0} + ", c.Re); // Reプロパティのgetアクセサーが呼び出される。
        Console.Write("{0}i| =", c.Im); // Imプロパティのgetアクセサーが呼び出される。
        Console.Write(" {0}\n", c.Abs); // Absプロパティのgetアクセサーが呼び出される。
    }
}

実装の隠蔽」のときと同様に、 このコードの実装方法を 「実部と虚部をメンバー変数に記憶しておく」方法から 「絶対値と偏角をメンバー変数に記憶しておく」方法に変更しても、 以下のように、クラス利用側のコードに手を加える必要は一切ありません。

using System;

// クラス定義
class Complex
{
    // 実装は外部から隠蔽(privateにしておく)
    private double abs; // 絶対値を記憶しておく
    private double arg; // 偏角を記憶しておく

    // 実部の取得・変更用のプロパティ
    public double Re
    {
        set
        {
            double im = this.abs * Math.Sin(this.arg);
            this.abs = Math.Sqrt(value * value + im * im);
            this.arg = Math.Atan2(im, value);
        }
        get
        {
            return this.abs * Math.Cos(this.arg);
        }
    }

    // 実部の取得・変更用のプロパティ
    public double Im
    {
        set
        {
            double re = this.abs * Math.Cos(this.arg);
            this.abs = Math.Sqrt(value * value + re * re);
            this.arg = Math.Atan2(value, re);
        }
        get
        {
            return this.abs * Math.Sin(this.arg);
        }
    }

    // 絶対値の取得用のプロパティ
    public double Abs
    {
        get { return this.abs; }
    }
}

// クラス利用側
class PropertySample
{
    static void Main()
    {
        Complex c = new Complex();
        c.Re = 4; // クラス利用側は一切変更せず
        c.Im = 3;
        Console.Write("|{0} + ", c.Re);
        Console.Write("{0}i| =", c.Im);
        Console.Write(" {0}\n", c.Abs);
    }
}

set/get で異なるアクセスレベルを設定

Ver. 2.0

C# 2.0 の新機能で、 プロパティの set/get アクセサーそれぞれ異なるアクセスレベルを設定できるようになりました。

class A
{
  private int n;

  public int N
  {
    get{ return this.n; }
    protected set{ this.n = value; }
  }
}

自動プロパティ

Ver. 3.0

C# 3.0 では、プロパティの get/set の中身の省略もできるようになりました。 この機能を自動プロパティ(auto-property, auto-implemented property)といいます。

例えば、

public string Name { get; set; }

というように、 get; set; とだけ書いておくと、

private string __name;
public string Name
{
  get { return this.__name; }
  set { this.__name = value; }
}

というようなコードに相当するものが自動的に生成されます。 (__name という変数名はプログラマが参照できるものではありません。) ちなみに、このコンパイラーによって生成されるフィールド(この例で言うと __name)は、バック フィールド(baking field: 後援フィールド)と呼ばれます。

C# プログラミングでは、 この手のコード(メンバー変数 name をプロパティ Name で覆う)は定型文的によく使います。 また、クラス内からであっても、private のメンバー変数には直接アクセスせず、 プロパティを通してアクセスする方が後々の保守がしやすかったりします。 ということで、自動プロパティのような省略記法が導入されました。

複素数の例でも、直交座標による実装のものは、以下のようにだいぶシンプルに書けるようになります。

using System;

class Complex
{
    public double Re { get; set; }
    public double Im { get; set; }

    public double Abs
    {
        get { return Math.Sqrt(Re * Re + Im * Im); }
    }
}

ちなみに、元々 C# 2.0 以前でも、 「プロパティの「デリゲート」版」にあたる「イベント」では自動プロパティを同じような省略が可能でした。 (デリゲート、イベントについては後述。 参考: 「デリゲート」、「イベント」。) その省略機能がプロパティにも実装されたということになります。

get-only プロパティ

Ver. 6

C# 6 では、get アクセサーだけのプロパティを定義できるようになりました。 この場合、コンストラクターでだけ値を代入できて、以降は書き換え不能になります。

using System;

class Complex
{
    public double Re { get; }
    public double Im { get; }

    public Complex(double re, double im)
    {
        // コンストラクター内でだけ代入可能。
        Re = re;
        Im = im;
    }
}

このように get アクセサーのみを持つプロパティは通称 get-only プロパティ(get-only property)と呼ばれています。

「コンストラクターでだけ値を代入できる」という挙動は readonly フィールドと同じです。 実際、上記の get-only プロパティからは以下のように、readonly なバック フィールドが作られます。

using System;

class Complex
{
    public double Re { get { return _re; } }
    private readonly double _re;
    public double Im { get { return _im; } }
    private readonly double _im;

    public Complex(double re, double im)
    {
        // コンストラクター内でだけ代入可能。
        _re = re;
        _im = im;
    }
}

プロパティ初期化子

Ver. 6

同じくC# 6.0から、自動プロパティに対して初期化子を与えられるようになりました。

class Point
{
    public int X { get; set; } = 10;
    public int Y { get; set; } = 20;
}

これで、コンストラクターを書かなくてもプロパティに対して初期値を与えることができます。

expression-bodied なプロパティ

get-only のプロパティに限りますが、他のいくつかの関数メンバーと同様に、expression-bodied (本体が式の)形式でプロパティを定義できます。 (参考: 「expression-bodied な関数」)

先ほどから例に挙げている複素数クラスでいうと、Abs プロパティの定義が楽になります。

using static System.Math;

class Complex
{
    public double Re { get; set; }
    public double Im { get; set; }

    public double Abs => Sqrt(Re * Re + Im * Im);
}

余談: C# にインデックス付きプロパティはありません

VB にはある「インデックス付きプロパティ」は、C# にはありません。 C# の流儀的には、「インデックス付きプロパティ」よりも、「コレクションクラスを返す普通のプロパティ」推奨です。 (その方が、foreach が使えたり、色々便利だから。)

int[] x;
// ↓これは文法違反。
public int X[int i]
{
    get { return x[i]; }
    private set { x[i] = value; }
}
int[] x;
// ↓これなら OK。
public int[] X
{
    get { return x; }
}

C# 2.0 や C# 3.0 を見こすなら、以下のように、配列や ICollection ではなく、IEnumerable を返すようにする方がいいかもしれません。 (詳細は「イテレーター」参照。)

int[] x;
public IEnumerable<int> X
{
    get { foreach (var item in x) yield return item; }
}

ちなみに、VB にはあることからわかるように、.NET 的にはインデックス付きプロパティを認めています。 C# から呼び出す場合は、get_*** というような名前のメソッド呼び出しになります。 例えば、VB で X と言う名前で、int を引数にとるインデックス付きプロパティを定義した場合、 C# からは get_X(0) というように呼び出します。

さらに特殊事情として、対 COM の場合だけ、普通に X[0] というような呼び出し方が認められます。 詳しくは「COM 相互運用時の特別処理」を参照。

init-only プロパティ

Ver. 9

C# 9.0 では、set に代わって、init という名前のアクセサーを定義できるようになりました。 例えば以下のように書けます(ちなみに、setinit は同時には書けません。排他です)。

class Complex
{
    public double Re { get; init; }
    public double Im { get; init; }
}

init アクセサーを持っているプロパティは通称 init-only プロパティ(init-only property)と呼ばれます。

用途としては get-only プロパティreadonly フィールドとほとんど同じです。 ただ、readonly の制限が厳しすぎるので、問題ない範囲でちょっとだけ制限を緩めたものが init アクセサーです。 (歴史的経緯で init という新キーワードが使われていますが、もし C# をフルスクラッチで作り直せるなら readonly が最初から init 相当の仕様になっていたと思います。)

まず、readonly と同じ点として、コンストラクター内での書き換えはできます。

class Complex
{
    public double Re { get; init; }
    public double Im { get; init; }
 
    public Complex(double re, double im)
    {
        // この2行は OK。
        Re = re;
        Im = im;
    }
}

一方、readonly では認められてないことで、init であればできることが3つあります。

例えば、以下のコード(get-only プロパティを利用)はコンパイルできませんが、

var p = new Point { X = 1, Y = 2 };
 
class Point
{
    public int X { get; }
    public int Y { get; }
}

以下のように init-only プロパティに書き換えるとコンパイルできます。

var p = new Point { X = 1, Y = 2 };
 
class Point
{
    public int X { get; init; }
    public int Y { get; init; }
}

初期化子の外で書き換えようとすると、readonlyと同じくコンパイル エラーになります。

var p = new Point { X = 1, Y = 2 };
p.X = 3; // ダメ。

with 式については別途解説予定(トラッキング issue: C# 9.0)ですが、 例えば以下のようなコードが書けます。

var p0 = new Point(1, 2);
var p1 = p0 with { X = 3 }; // p0 のクローンを作った上で、X だけ 3 で上書き。
 
record Point(int X, int Y);

他の init アクセサーからの書き換えは、例えば以下のようなコードを書けます。

using System;
 
var x = new Squared { ValueSquared = 4 };
Console.WriteLine(x.Value); // 2
 
class Squared
{
    public double Value { get; init; }
 
    public double ValueSquared
    {
        get => Value * Value;
        init => Value = Math.Sqrt(value);
    }
}

ちなみに、init アクセサー内では readonly フィールドも書き換え可能です。

class Squared
{
    public readonly double Value;
 
    public double ValueSquared
    {
        get => Value * Value;
        init => Value = Math.Sqrt(value); // OK。
    }
}

init-only プロパティの中身

ちなみに、init-only プロパティコンパイル結果としては単に publicset アクセサーと readonly フィールドになっています。 C# コンパイラーのレベルで「初期化子など以外からの書き換えを禁止する」というような解析をしています。

この解析に対応していない古い C# コンパイラーから set を呼ばれるとかなりまずい(本来書き換えられないはずの readonly フィールドが書き換わる)ので、それを禁止するために modreq という修飾機能を使っています。

modreq については別途説明予定です。トラッキング issue:

更新履歴

ブログ