2011年10月26日水曜日

Scalaのアクセサメソッドの挙動

Scalaのアクセサメソッド(getterとsetter)について調べたことをまとめます。

下記ページをかなり参考にさせていただきました。
[Scala][Java] Scalaの統一アクセス(プロパティ構文)がなかなかイカしてる件

Scalaはvarかvalを使ってプロパティを宣言すると、自動的にアクセサメソッドを生成してくれます。
さらに、プロパティを直接参照するような書き方をすると、自動的にアクセサメソッドが実行されるようになっています。
そのため、

オブジェクト名.name で、自動生成されたゲッターが実行される
オブジェクト名.name = "abc" で自動生成されたゲッターが実行される

となります。

コンパイルした後に、javapを使って中身を覗いてみると宣言していないメソッドが生成されていることが分かります。

サンプルソース
class Hoge1 {
    var name:String = _
}

object Hoge1Runner {
    def main(args:Array[String]) {
        val obj = new Hoge1
        obj.name = "Tarou";

        // 実行結果は、「Hoge1:Tarou」となる
        println("Hoge1:" + obj.name);
    }
}
javap -private Hoge1の実行結果
  public class Hoge1 extends java.lang.Object implements scala.ScalaObject{
      private java.lang.String name;
      public java.lang.String name();
      public void name_$eq(java.lang.String);
      public Hoge1();
  }

自動的に生成されたアクセサメソッド使ってるって言ってもこれじゃJavaのpublicなプロパティと一緒じゃん。
値のチェックとか必要な場合は結局アクセサメソッド自前で作らなきゃいけないんでしょ?オーバライドできるの?
ということでアクセサをオーバライドしてみました。(オーバライドって言い方が正しいかどうかは分かりませんが)
getterでは、先頭に「Mr.」を、setterでは末尾に「.」を付与するようにしました。

Hoge1Runnerのrun()と、Hoge2Runerのrun()を見比べてみてください。呼び出し側の実行方法は全く変わっていません。
つまり、サクっと開発しておいて、後々呼び出されるクラスの方を修正してしまえば、呼び出し側を変更する必要が無いのです。
Javaだとこうはいきません。オブジェクト名.プロパティ名と書いたコードが山ほどあった場合、呼び出されているクラスでアクセサメソッドを追加した際、呼び出しているプログラムを全て修正する必要があります。
IDEを使っていればアクセサメソッドを自動生成してくれたり、一括置換など便利な機能があるので、特に困ることは無いかもしれませんが、ちょっとしたツールを作りたいという時にはこう言った機能が真価を発揮すると思います。

問題点として、以下のjavap実行結果を見て分かる通り、自動生成されたgetterであるname_()が定義されています。
ということは、実用上問題はないでしょうが、Hoge2#name_ = "aaa" などが実行されると意図したgetterが利用されないということになります。

サンプルソース
class Hoge2 {
    var name_ :String = _

    // getter
    def name  = {
        "Mr." + name_
    }

    // setter
    def name_= (value:String) = {
        name_ = value + "."
    }
}

object Hoge2Runner {
    def main(args:Array[String]) {
        val obj = new Hoge2
        obj.name = "Tarou";

        // 実行結果は「Hoge2:Mr.Tarou.」となる
        println("Hoge2:" + obj.name);

        // 以下の方法だと、用意したgetterが使われないので、実行結果は「Hoge2:Tarou」となります
        //(用意したsetterを使ってないのでピリオドがつかない)
        // val obj2 = new Hoge2
        // obj2.name_ = "Tarou"
        // println("Hoge2:" + obj2.name);
    }
}
javap -private Hoge2の実行結果
public class Hoge2 extends java.lang.Object implements scala.ScalaObject{
    private java.lang.String name_;
    public java.lang.String name_();
    public void name__$eq(java.lang.String);
    public java.lang.String name();
    public void name_$eq(java.lang.String);
    public Hoge2();
}

Hoge2の、意図しないgetterが利用できる問題を解決するためには、プロパティ自体をprivateにする必要があります。
プロパティをprivateにすると、自動生成されるgetterとsetterもprivateになります。
そのため、自動生成されたアクセサには、外部からアクセスでできなくなるため、意図しないアクセサ(自動生成されたアクセサ)が利用できてしまう問題を解消できます。
ただ、いきなりこの形で記述してしまうとScalaでプロパティが自動生成されるメリットが無くなってしまいます。
まずはpublicでプロパティを作っておいて、問題が出たりするタイミングで修正した方が良いかも。
setter自体を用意しなければ外部からの更新を制御することもできます。

サンプルソース
class Hoge3 {
    private var name_ :String = _

    // getter
    def name  = {
        "Mr." + name_
    }

    // setter
    def name_= (value:String) = {
        name_ = value + "."
    }
}

object Hoge3Runner {
    def main(args:Array[String]) {
        val obj = new Hoge3
        obj.name = "Tarou";

        // 実行結果は「Hoge3:Mr.Tarou.」となる
        println("Hoge3:" + obj.name);
        // 以下の方法だと、name_はprivateなのでコンパイルエラー
        // (error: variable name_ in class Hoge3 cannot be accessed in Hoge3)

        // val obj2 = new Hoge3
        // obj2.name_ = "Tarou"
        // println("Hoge3:" + obj2.name);
    }
}

javap -private Hoge3の実行結果
public class Hoge3 extends java.lang.Object implements scala.ScalaObject{
    private java.lang.String name_;
    private java.lang.String name_();
    private void name__$eq(java.lang.String);
    public java.lang.String name();
    public void name_$eq(java.lang.String);
    public Hoge3();
}

Scalaではプロパティのアクセサは自動生成されるなら、そのアクセサはどうやって修正すればいいんだ?と思って調べた結果をまとめました。
間違っている点などあればご指摘頂頂けるとありがたいです。
ちなみに、プロパティをvalで宣言すると、Javaのfinalになり、setterは自動生成されず、getterのみ自動生成されます。
Javaのfinalなので、同じクラス内からも当然値を変更できなくなります。
なんでプロパティ名にアンダーバーが必要なの?というのはまだ理解できていません。
アクセサ自体の書き方も、普通の関数とは違うようなのでまだまだ勉強しなければという感じです。

0 件のコメント: