Groovy の GString
Groovy では、文字列の中に「${ ~ }」のように記述することで式を埋め込むことができます。このとき、Groovy はその文字列リテラルを GString
オブジェクトとして扱います。ただし、String
が必要な箇所で GString
を渡すと勝手に String
に変換してくれるので、あまり意識することはないでしょう。また、「${ ~ }」の代わりに「${-> ~ }」とすると、式を遅延評価させることができます。
さて、以下のコードを見てください。
// array プロパティをもつオブジェクトを作成
def test = new Expando(array: [0])
// String として生成
String string = "string: ${test.array}"
// GString として生成
def direct = "direct: ${test.array}"
// GString として遅延評価によって生成
def lazy = "lazy: ${-> test.array}"
test.array << 1 // (1)
test.array = [2] // (2)
println(string)
println(direct)
println(lazy)
これを実行すると、以下のようになります。
string: [0]
direct: [0, 1]
lazy: [2]
全部結果が違います。これについて説明したいと思います。
まず大前提として、GString
に埋め込まれた遅延評価でない式の中で出てくる変数は、全てその参照として保持されます。したがって、 "direct: ${user.array}"
が実行された時点で、user
オブジェクトの array
プロパティの参照が、GString
クラスの内部に保持されます。この参照を仮に A としましょう。
その後、コードの (1) の部分で、それと同じ参照 A をもつ変数に要素が加えられます。さらにコード (2) の部分で array
プロパティに [2]
というオブジェクトが代入されていますが、この [2]
はこの場で作られた新たなオブジェクトなので、[2]
の参照と A は異なります。したがって、direct
に代入された GString
が内部に保持している参照 A
には何の影響もありません。したがって、最終的に println(direct)
の実行結果は、要素が 1 つ追加されて「direct: [0, 1]」となるわけです。
GString
は String
に変換された時点で、内部に保持している参照に対して toString
メソッドを呼び、文字列として確定させます。したがって、上のコードの string
に "string: ${user.array}"
が代入された時点で、user
オブジェクトの array
プロパティに対して toString
が呼び出されます。この結果は当然「[0]」なので、string
には "string: [0]"
という String
オブジェクトが代入されることになります。この時点で String
オブジェクトとして固定されたため、他の変数に変更が加えられたり代入し直されたりしても何の影響も受けず、最終的に println(string)
の実行結果は「string: [0]」となります。
最後に、遅延評価にした GString
の場合ですが、これは変数の参照を保持するのではなくクロージャとして保持します。そして、toString
が呼び出されるたびに、保持しているクロージャを評価し、その結果に置き換えます。したがって、println(lazy)
を実行すると、その時点で "lazy: ${-> user.array}"
内の user.array
が実行され、その結果に置き換えられた文字列が表示されます。この時点で user.array
は (2) の箇所で [2]
に置き換えられているので、これが評価されて「lazy: [2]」と表示されるわけです。
ちなみに Ruby では
Ruby にも文字列への式の埋め込みができるのですが、こちらはその場で評価されて文字列に展開されます。したがって、後から変数が変更されたり再代入されても影響を受けません。
test = Struct.new(:array).new([0])
string = "string: #{test.array}"
test.array << 1
p(string)
test.array = 2
p(string)
これの実行結果は以下のようになります。
"string: [0]"
"string: [0]"