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

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

Vala言語におけるデストラクタについてと動作テスト

クラス名と同名で戻り値のない特殊なメンバ関数をコンストラクタと呼び、クラスをもとにして生成された各オブジェクト(インスタンス)の初期化処理に用いるが、クラス名の手前に「~」を付けた特殊なメンバ関数も存在し、オブジェクトが破棄される直前に呼ばれるものでデストラクタと呼ばれる。
Vala言語の文法はC#言語に近いが、Mono/.NETでは内部的なオブジェクト破棄のタイミングが分からないのに対して、オブジェクトが参照されなくなったときにデストラクタは確実に呼び出され、その後オブジェクトは破棄される。そのため、実行されるタイミングを当てにした「後始末」的な処理を記述することもできる。*1
オブジェクトが破棄されるのは、オブジェクト変数に別の新しいオブジェクトをnew演算子で生成して入れた場合や、どのオブジェクトも参照しない状態としてnullを代入した場合、オブジェクト変数の有効範囲の関係で寿命となって破棄される場合など。
下は簡単なテスト例。
[任意]ファイル名: destructortest.vala

/*
 * valac -o destructortest destructortest.vala
 */

namespace DestructorTest
{
  class ClassA
  {
    ClassB objb;
    ClassC objc;
    ClassD objd;
    /* コンストラクタ(オブジェクト生成*直後*に呼ばれる) */
    public ClassA (int b, string c, double d)
    {
      print ("ClassA ()\n");
      this.objb = new ClassB (b);
      this.objc = new ClassC (c);
      this.objd = new ClassD (d);
    }
    /* デストラクタ(オブジェクト破棄*直前*に呼ばれる) */
    ~ClassA ()
    {
      print ("~ClassA ()\n");
      /* この後各メンバも破棄され、B,C,Dの各クラスのデストラクタが呼ばれる */
    }
  }
  class ClassB
  {
    int b;
    public ClassB (int b)
    {
      print ("ClassB ()\n");
      this.b = b;
    }
    ~ClassB ()
    {
      print ("~ClassB ()\n");
    }
  }
  class ClassC
  {
    string c;
    public ClassC (string c)
    {
      print ("ClassC ()\n");
      this.c = c;
    }
    ~ClassC ()
    {
      print ("~ClassC ()\n");
    }
  }
  class ClassD
  {
    double d;
    public ClassD (double d)
    {
      print ("ClassD ()\n");
      this.d = d;
    }
    ~ClassD ()
    {
      print ("~ClassD ()\n");
    }
  }
  class MainClass
  {
    public static void main (string[] args)
    {
      ClassA obja;

      print (">>> obja = new ClassA ()\n");
      obja = new ClassA (10, "test", 1.1);
      /*
       * 同一のオブジェクト変数に新しいオブジェクトを入れると
       * (他から参照されていない限り)古いオブジェクトは
       * *このタイミングで*自動的に破棄される
       * C#ではいつデストラクタが呼ばれるかは分からず使われにくい
       * Vala言語では厳密にこのタイミングでデストラクタが呼ばれるので
       * リソース解放の記述をデストラクタに記述できる(C++言語に近い)
       * メモリ管理には内部的に参照カウント方式が用いられている
       */
      print ("\n>>> obja = new ClassA ()\n");
      obja = new ClassA (9, "foo", 3.14);

      /* null代入でオブジェクト破棄・何も参照しない状態 */
      print ("\n>>> obja = null\n");
      obja = null;

      print ("\n-- end of main () --\n");
    }
  }
}

以下が実行結果。

>>> obja = new ClassA ()
ClassA ()
ClassB ()
ClassC ()
ClassD ()

>>> obja = new ClassA ()
ClassA ()
ClassB ()
ClassC ()
ClassD ()
~ClassA ()
~ClassB ()
~ClassC ()
~ClassD ()

>>> obja = null
~ClassA ()
~ClassB ()
~ClassC ()
~ClassD ()

-- end of main () --

下は線形リストにオブジェクトを入れた場合と関数の中でオブジェクト変数を定義した場合の例。
[任意]ファイル名: destructortest2.vala

/*
 * valac -o destructortest2 destructortest2.vala
 */

namespace DestructorTest2
{
  public static void func ()
  {
    ClassA obja;  // 関数の中だけで有効
    obja = new ClassA (9999);
  }
  class ClassA
  {
    ClassB objb;
    public ClassA (int b)
    {
      print ("ClassA ()\n");
      this.objb = new ClassB (b);
    }
    ~ClassA ()
    {
      print ("~ClassA ()\n");
    }
  }
  class ClassB
  {
    int b;
    public ClassB (int b)
    {
      print ("ClassB ()\n");
      this.b = b;
    }
    ~ClassB ()
    {
      print ("~ClassB ()\n");
    }
  }
  class MainClass
  {
    public static void main (string[] args)
    {
      SList<ClassA> list;

      print (">>> list = new SList<ClassA> ();\n");
      list = new SList<ClassA> ();
      print ("\n>>> list.prepend (new ClassA ());\n");
      list.prepend (new ClassA (1));
      print ("\n>>> list.prepend (new ClassA ());\n");
      list.prepend (new ClassA (2));
      print ("\n>>> list.prepend (new ClassA ());\n");
      list.prepend (new ClassA (3));

      /*
       * 新しいリストを作成すると
       * 古いリストとその中の全てのオブジェクトが破棄される
       */
      print ("\n>>> list = new SList<ClassA> ();\n");
      list = new SList<ClassA> ();

      print ("\n>>> list.prepend (new ClassA ());\n");
      list.prepend (new ClassA (4));
      print ("\n>>> list.prepend (new ClassA ());\n");
      list.prepend (new ClassA (5));

      /* リストへのnull代入でも同様に破棄される */
      print ("\n>>> list = null;\n");
      list = null;

      /* 関数 */
      print ("\n>>> func ();\n");
      func ();
      print ("-- after func () --\n");

      print ("\n-- end of main () --\n");
    }
  }
}

以下が実行結果。

>>> list = new SList<ClassA> ();

>>> list.prepend (new ClassA ());
ClassA ()
ClassB ()

>>> list.prepend (new ClassA ());
ClassA ()
ClassB ()

>>> list.prepend (new ClassA ());
ClassA ()
ClassB ()

>>> list = new SList<ClassA> ();
~ClassA ()
~ClassB ()
~ClassA ()
~ClassB ()
~ClassA ()
~ClassB ()

>>> list.prepend (new ClassA ());
ClassA ()
ClassB ()

>>> list.prepend (new ClassA ());
ClassA ()
ClassB ()

>>> list = null;
~ClassA ()
~ClassB ()
~ClassA ()
~ClassB ()

>>> func ();
ClassA ()
ClassB ()
~ClassA ()
~ClassB ()
-- after func () --

-- end of main () --

使用したバージョン:

  • Vala 0.7.9

*1:GIOライブラリを用いたサンプルなどで分かるように、手動で後始末処理を記述することなく、オブジェクトの寿命とともに自動的に後始末処理が行われるという設計(RAII)ができる