目次

キーワード

概要

プロパティ(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;
    }
}

このコードは、以下のような意味になります。 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;
    }
}

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 相互運用時の特別処理」を参照。

演習問題

問題1

クラス問題 1Point 構造体および Triangle クラスの各メンバー変数に対して、 プロパティを使って実装の隠蔽を行え。

解答例1

using System;

/// <summary>
/// 2次元の点をあらわす構造体
/// </summary>
struct Point
{
  double x; // x 座標
  double y; // y 座標

  #region 初期化

  /// <summary>
  /// 座標値 (x, y) を与えて初期化。
  /// </summary>
  /// <param name="x">x 座標値</param>
  /// <param name="y">y 座標値</param>
  public Point(double x, double y)
  {
    this.x = x;
    this.y = y;
  }

  #endregion
  #region プロパティ

  /// <summary>
  /// x 座標。
  /// </summary>
  public double X
  {
    get { return this.x; }
    set { this.x = value; }
  }

  /// <summary>
  /// y 座標。
  /// </summary>
  public double Y
  {
    get { return this.y; }
    set { this.y = value; }
  }

  #endregion

  public override string ToString()
  {
    return "(" + x + ", " + y + ")";
  }
}

/// <summary>
/// 2次元空間上の三角形をあらわす構造体
/// </summary>
class Triangle
{
  Point a;
  Point b;
  Point c;

  #region 初期化

  /// <summary>
  /// 3つの頂点の座標を与えて初期化。
  /// </summary>
  /// <param name="a">頂点A</param>
  /// <param name="b">頂点B</param>
  /// <param name="c">頂点C</param>
  public Triangle(Point a, Point b, Point c)
  {
    this.a = a;
    this.b = b;
    this.c = c;
  }

  #endregion
  #region プロパティ

  /// <summary>
  /// 頂点A。
  /// </summary>
  public Point A
  {
    get { return a; }
    set { a = value; }
  }

  /// <summary>
  /// 頂点B。
  /// </summary>
  public Point B
  {
    get { return b; }
    set { b = value; }
  }

  /// <summary>
  /// 頂点C。
  /// </summary>
  public Point C
  {
    get { return c; }
    set { c = value; }
  }

  #endregion

  /// <summary>
  /// 三角形の面積を求める。
  /// </summary>
  /// <returns>面積</returns>
  public double GetArea()
  {
    double abx, aby, acx, acy;
    abx = b.X - a.X;
    aby = b.Y - a.Y;
    acx = c.X - a.X;
    acy = c.Y - a.Y;
    return 0.5 * Math.Abs(abx * acy - acx * aby);
  }
}

/// <summary>
/// Class1 の概要の説明です。
/// </summary>
class Class1
{
  static void Main()
  {
    Triangle t = new Triangle(
      new Point(0, 0),
      new Point(3, 4),
      new Point(4, 3));

    Console.Write("{0}\n", t.GetArea());
  }
}

更新履歴

ブログ