試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

C#言語とVala言語のプロパティについて

プロパティについて

C#言語とVala言語では、クラスの中でメンバ関数とメンバ変数の両方の性質を併せ持つプロパティというものが使用できる。
これはクラスのメンバをクラスの外から読み書きするときに

  • クラスの中からはメンバ関数のように記述して
  • クラスの外からはメンバ変数のように読み書きする

ものとなっている。
オブジェクト指向の考え方で「オブジェクト内部の処理/実装などは(オブジェクトの)外からは見えなくして、操作を行うための方法とデータに関する情報だけを外に公開する」というものがあり、そのためにオブジェクトに対する変更はメンバ関数によって行うことが望ましいことが多いのだが、外から使用するときの記述が関数の呼び出しという形になるので見づらいコードになる。

(メンバ関数の呼び出しの形の例)
win.set_title ("title");
title = win.get_title ();

プロパティはその問題を解決し、メンバ変数の代入/値の取り出しに近い感覚でクラス内のメンバ関数の呼び出しに近いことを行えるようにしたものとみることができる。

(プロパティの形の例)
win.title = "title";
title = win.title;

書式

基本的な書式

通常のメンバ変数(プロパティに対してフィールドと呼ばれることがある)との違いは波括弧で読み書きそれぞれの処理を行う関数のようなもの(アクセサ)を囲む形であることとなり、読み書きする値を持つメンバ変数は別途用意する。
getアクセサには呼び出し側に返る値をreturn文で指定し、setアクセサでは「value」という名前の引数をメンバ変数に代入する。

int __a;
public int a
{
  get
  {
    return __a;
  }
  set
  {
    __a = value;
  }
}

値のチェックなどの処理が必要な場合はアクセサの中に記述する。

  set
  {
    if (value > 0)
      __a = value;
  }

読み書きのどちらかだけが行えるようにしたい場合はそのアクセサだけを記述する。

public int a
{
  get
  {
    return __a;
  }
}

setアクセサのないプロパティに値を代入しようとするとコンパイル時にエラーが出る。

構造体型プロパティのメンバの代入について

プロパティで構造体*1への読み書きを行うときに

[オブジェクト].[構造体型プロパティ].[構造体メンバ] = [値];

という代入を行うことはできない(コンパイルエラーになる)。ただし、「[オブジェクト].[構造体型プロパティ].[構造体メンバ]」として読み込むことはできる。
書き込みを行いたい場合は構造体のコンストラクタによる一括代入を行うなどの方法を使用する。

[オブジェクト].[構造体型プロパティ] = [構造体名] ([コンストラクタ引数...]);

もちろん、構造体の定義の中ではコンストラクタを別途定義しておく必要がある。下は構造体定義の例。

  struct Pos
  {
    public int x;
    public int y;
    public Pos (int x, int y)
    {
      this.x = x;
      this.y = y;
    }
  }

省略記法(自動実装プロパティ)

上の「基本的な書式」の中で書いた例のようにプロパティのアクセサの内容がそれぞれ単なるreturn文と引数valueの値の代入だけという形の場合、実際に使用されるメンバ変数(上の例では「__a」)の定義とアクセサの内容の記述をまとめて省略して

public int a { get; set; }

のように記述することができ、Vala言語とバージョン3以上のC#言語で使用できる。初期値は設定できない。
C#言語ではコンストラクタでのみ代入可能(それ以外は書き込み不可)にしたいときの書き方がない*2ので使いどころが限られるかもしれない。
Vala言語の場合はコンストラクタ内でのみ書き込み可能にしたいプロパティは

(Vala言語)
public int b { get; construct; }

のように定義できる。ただしクラスがGLib.Objectクラスを継承している必要がある。*3
例としてはGTK+のウィンドウタイプ(Gtk.Windowオブジェクトのプロパティtype)*4などがあり、実際にコンストラクタ内以外では書き込めない。
C#言語では自動実装プロパティを用いずにreadonly修飾子*5の付いたメンバ変数を用いることになる。

テスト

今回の例はVala言語のみとなる。
[任意]ファイル名: propertytest.vala

using GLib;

namespace PropertyTest
{
  struct Pos
  {
    public int x;
    public int y;
    public Pos (int x, int y)
    {
      this.x = x;
      this.y = y;
    }
  }
  class PosClass
  {
    public int x;
    public int y;
    public PosClass (int x, int y)
    {
      this.x = x;
      this.y = y;
    }
  }
  class TestClass : GLib.Object
  {
    int __a = 8;  // 実際に読み書きされるメンバ変数
    /* 書き込み・読み込みともにそのままの値 */
    public int raw_a
    {
      /*
       * getアクセサ
       * クラスの外からメンバ変数のように取り出すためのメンバ関数のようなもの
       * return文で戻り値を記述する
       */
      get
      {
        /* プロパティ自体は変数ではないので
         * 別途変数(フィールド)を用意する・ここではint型のフィールド__aを用いる
         * 多くはプロパティに対応した(privateな)フィールド
         */
        return __a;
      }
      /*
       * setアクセサ
       * クラスの外からメンバ変数のように代入するためのメンバ関数のようなもの
       * 代入する値がvalueという引数として入ってくる
       */
      set
      {
        __a = value;
      }
    }
    /*
     * プロパティが中で「処理」をすることを示すための例
     * 書き込みはそのままの値で読み込みは10倍・負の値は受け付けない
     */
    public int a
    {
      get
      {
        return __a * 10;
      }
      set
      {
        /* アクセサの中では関数のように分岐などの処理を書くことができる */
        if (value >= 0)
          __a = value;
      }
    }
    /*
     * 上のraw_aのように入力/出力ともにそのまま行う場合は省略記法で記述できる
     *  (自動実装プロパティ)
     * 初期値は設定できない
     * C#ではバージョン3以上のみ
     * 「get; set;」では読み書き両方が可能となる
     * C#でコンストラクタ内でのみ書き込み可能・読み取りは常にOKにするための
     *  省略記法はない(private readonlyなフィールドをコンストラクタで書き込む)
     * Valaでコンストラクタ内でのみ書き込み可能・読み取りは常にOKにするには
     * 「get; construct;」と書く(クラスはGLib.Objectクラスを継承する必要がある)
     */
    public Pos p { get; set; }
    public PosClass pc { get; set; }
    /* ***Vala言語のみ*** コンストラクタ内でのみ代入可能な指定 */
    public int b { get; construct; }
    public TestClass (int b)
    {
      this.b = b;
    }
  }
  class MainClass
  {
    public static void main (string[] args)
    {
      TestClass t;
      print ("> t = new TestClass (9999);\n");
      t = new TestClass (9999);  // bに9999を指定

      print ("> t.a = -2;\n");
      t.a = -2;                 // 負の値なので代入されない(初期値のまま)
      print ("t.a:%d t.raw_a:%d\n", t.a, t.raw_a);  // 初期値x10と初期値

      print ("> t.a = 5;\n");
      t.a = 5;
      print ("t.a:%d t.raw_a:%d\n", t.a, t.raw_a);  // 5x10と5

      /*
       * プロパティの代入値は関数の引数に相当するので
       * 構造体メンバへの代入はできない
       */
      //t.p.x = 100;  // コンパイルエラー
      //t.p.y = 200;  // コンパイルエラー
      print ("> t.p = Pos(100, 200);\n");
      t.p = Pos (100, 200);  // 構造体コンストラクタによる代入
      /* (publicな)構造体メンバを読み取ることはできる */
      print ("t.p: x=%d y=%d\n", t.p.x, t.p.y);

      /* クラスの場合 */
      print ("> t.pc = new PosClass(10, 20);\n");
      t.pc = new PosClass (10, 20);
      print ("t.pc: x=%d y=%d\n", t.pc.x, t.pc.y);
      print ("> t.pc.x = 11;\n");
      t.pc.x = 11;  // OK
      print ("> t.pc.y = 22;\n");
      t.pc.y = 22;  // OK
      print ("t.pc: x=%d y=%d\n", t.pc.x, t.pc.y);

      /* 「get; construct;」指定のプロパティ */
      print ("b:%d\n", t.b);
      //t.b = 8888;  // コンパイルエラー
    }
  }
}

下は実行結果。

> t = new TestClass (9999);
> t.a = -2;
t.a:80 t.raw_a:8
> t.a = 5;
t.a:50 t.raw_a:5
> t.p = Pos(100, 200);
t.p: x=100 y=200
> t.pc = new PosClass(10, 20);
t.pc: x=10 y=20
> t.pc.x = 11;
> t.pc.y = 22;
t.pc: x=11 y=22
b:9999

関連記事:

参考URL:

使用したバージョン:

  • Vala 0.7.4

*1:複数のメンバを持ったデータ型で、クラスと比べると機能に制約があり、また、内部的には値として扱われる

*2:C#言語の将来のバージョンではどうなるか分からない

*3:継承しないとコンパイル時に「construct properties require GLib.Object」というエラーが出る

*4:public Gtk.WindowType type { get; construct; }

*5:コンストラクタ内でのみ書き込み可能にするための修飾子