Groovy の型変換

型変換についてのメモ。
プログラムの挙動がおかしいと思ったときに読み返すためのもの。

Scala の implicit conversions

implicit conversions が試されるのは

  • 要求された型への変換
    1. 変数宣言の型への変換
    2. メソッド呼び出しの引数の型への変換
  • レシーバの変換

Groovy では use キーワードによるカテゴリがこの機能に対応する。
2 は Groovy では引数の型でメソッドを選択するので対応するメソッドを用意することになる。
1 はカテゴリでは対応できない。

Groovy の型変換

強制型変換

演算によって型が変わる

  1. 演算で Double か Float を使用すると Double になる
  2. 演算で BigDecimal を使用すると BigDecimal になる
  3. 演算で BigInteger を使用すると BigInteger になる
  4. 演算で Long を使用すると Long になる
  5. 上記以外の場合は Integer *1
キャスト
  • as によるもの asType メソッドが呼ばれる
  • () によるものは org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation が呼ばれる*2


Groovy と Java 間での Wrapper 型と Primitive 型の変換は自動的に行ってくれる。

Groovy の暗黙の型変換

Java のように型を意識して使用している場合には関係のない話。
Groovy には short や byte、char などのリテラルがないのでそれらを宣言する場合は右辺は int や String になる。これらは Groovy が自動的に変換してくれる。

char  c = 'A' // as char を省略。シングルクォートで囲んであるが Groovy では String
short s = 1   // as short を省略。narrowing だが明示しなくても可能
int   i = 'A' // 65

Groovy ではこのような場合 ClassCastException が発生しないので注意。


おそらく Groovy 側で () によるキャストを埋め込んでくれている。
これが as に変われば Scala の implicit conversions の 1 と同じ機能を実現できる。
DefaultTypeTransformation に TODO が書かれていた。

    public static Object castToType(Object object, Class type) {
        if (object == null) {
            return null;
        }

        if (type == Object.class)
          return object;

        final Class aClass = object.getClass();
        if (type == aClass) return object;
        // TODO we should move these methods to groovy method, like g$asType() so that
        // we can use operator overloading to customize on a per-type basis
        if (ReflectionCache.isArray(type)) {

いつ対応されるか分からないので気長に待つとする。
そう言えば半年前は nested class で enum を使えなかったが今は利用できる *3 ので案外早いかもしれない。

さらなる疑問

Groovy でリストのかけ算 を確認しているときにタイプミスして気づいたのだがずっと理由が分からなかった。

assert 1 * [2] == 2
  • 宣言上は multiply の引数は List をとらない
  • List は宣言されている引数の型にはキャストできない
  • 素数が 0 や 2 以上ではエラーになる


intdiv の場合

assert 1.intdiv([1,2,3,4,5,6,7,8,9]) == 1


これ以降の処理を調べてきれていないが実行時にうまく処理されているようだ。

import org.codehaus.groovy.runtime.callsite.*

def acallsite = new CallSiteArray(this.class, ["runScript", "println", "multiply"] as String[]).array
assert  acallsite[2].call(1, [[2]] as Object[]) == 2
// acallsite[1].callCurrent(this, acallsite[2].call(1, [2]));
アノテーションや型パラメータ

Groovy 1.8 から Closure に戻り値の型パラメータが追加されていた。
でも Groovy でコーディングするときは @Override や型パラメータはコメント代わりにしか使っていない*4

@Override def test() {
  List<Long, Double> list = new ArrayList<Integer>()
  list.add("s")
}

test を Override しているわけではないし List の型パラメータは意味不明だ。
これでも通ってしまう。

型チェックが欲しい場合 ???
def seq = new Sequence(Integer)  // extends ArrayList
seq.add(1)
seq.add(2G)  // IllegalArgumentException

まとめ

  • 型を明示するとコンパイラに頼れるのでバグの早期発見につながる
  • Groovy は動的言語なので Generics の効果がない
  • Groovy は @Override のチェックをしていない

追記 2011-05-14

強制型変換の例外のことを書くのを忘れていたので追記

groovy:000> 5.0d ** 2
===> 25

25.0d ではない。


べき乗の計算は整数になると整数型で返している。

    public static Number power(Number self, Number exponent) {
        double base, exp, answer;
        base = self.doubleValue();
        exp = exponent.doubleValue();

        answer = Math.pow(base, exp);
        if ((double) ((int) answer) == answer) {
            return (int) answer;
        } else if ((double) ((long) answer) == answer) {
            return (long) answer;
        } else {
            return answer;
        }
    }

強制型変換は +, -, * だけで他の演算は例外あるので注意する。
あと今は戻り値の型へのキャストが行われていないということもわかる。


この問題も計算結果を一度 double に代入しておくかキャストを明示しておけばエラーにならない*5

*1:1i のように i を付けるリテラルが存在することを知った

*2:org.codehaus.groovy 以下は Groovy 内部のクラスなので明示的に使用するものではない

*3:inner class では使えない。そうしなければならない用途も思いつかないが

*4:Java との連携用?

*5:def という手もある