1610


例えば、なんらかの名前を受け取り、その名前に対して挨拶するようなクラスとして、以下のようなものを定義したとします。

[code]

     class SimpleGreeting
       def initialize name
         @name = name
       end
    
       def say
         "Hello, #{@name}"
       end
     end
    
[/code]

このとき、`say`の内部にある文字列を変えることができれば、もっと柔軟な挨拶ができるのではないか、と思いました。しかし、最初に文字列を渡して、あとから式展開を行うスマートな方法がわかりません。それに近い挙動は再現できたのですが、とてもスマートとはいえない気がします。

[code]

     class Greeting
       def initialize name, template
         @name = name
         @template = "\"#{template}\""
       end
    
       def say
         eval <<-EOF
     name = '#{@name}'
     #{@template}
         EOF
       end
     end
    
[/code]

このようなクラスを定義してあげれば、確かに下のように利用することは可能です。

[code]

     puts Greeting.new('esehara', 'Hello, #{name}').say
     #output: Hello, esehara
     puts Greeting.new('esehara', 'こんにちは, #{name}').say
     #output: こんにちは, esehara
    
[/code]

とはいえ、このような挙動を、もっと簡単に実現する方法がありそうな気がしましたが、どうでしょうか。それとも、このような文字列の使い方そのものが余りスマートではないと考えるべきなのでしょうか?


CC BY-SA 3.0
2014-12-15T15:10:42.337
1606
2014-12-18T08:19:47.017
2014-12-16T00:32:42.217
939
939
question
ruby
Rubyで文字列の式展開を後から行うには?


敢えて `eval` を使いつつスッキリさせるならば、式中に`{}`を含められない制限はできてしまいますが、こんな感じでしょうか。

[code]

    class Greeding
      def initialize name, template
        @name = name
        @template = template
      end
    
      def say
        @template.gsub(/#\{([^}]*)\}/) { eval($1).to_s }
      end
    end
    
[/code]

文字列中の `#{EXPR}` を正規表現で抜き出して `eval(EXPR).to_s` の結果に置き換えています。

ただしその例のように単に変数を参照するだけならば [Kernel#format](http://docs.ruby-
lang.org/ja/2.1.0/class/Kernel.html#M_FORMAT) を使って、`say`
メソッドは以下のように書き換えて、与えるテンプレートを `'Hello, %{name}'` のように変えてしまうのも良いのではないでしょうか。

[code]

    def say
      @template % { name: @name }
    end
    
[/code]


CC BY-SA 3.0
2014-12-15T16:21:06.187
1608
2014-12-15T16:21:06.187
208
1606
answer


templateの適用部分は @KoRoNさんの回答を参考にされるといいと思うのですが、

templateをコンストラクタに渡すよりは別のリソースから取得できる方が柔軟性は高くなるように思います。
たとえば国際化対応ではテンプレート部分は外部ファイルから取得することが多いです。


CC BY-SA 3.0
2014-12-15T16:43:58.060
1609
2014-12-15T16:43:58.060
728
1606
answer


initialize にブロックを渡すのがスマートかなと思います。

[code]

    class Greeting
      def initialize(name, &block)
        @name = name
        @block = block
      end
    
      def say
        @block.call(@name)
      end
    end
    
    puts Greeting.new("Fate") { |name| "Hello, #{name}" }.say
    puts Greeting.new("フェイトちゃん") { |name| "こんにちは, #{name}" }.say
    
[/code]

結果:

[code]

    Hello, Fate
    こんにちは, フェイトちゃん
    
[/code]

attr_accessor を定義すると、さらに汎用的に使えそうです。


CC BY-SA 3.0
2014-12-15T16:58:21.020
1610
2014-12-15T16:58:21.020
76
1606
answer


文字列の式展開は式が書かれた場所で文字列が展開することを意図して使ったほうが混乱が少ないと思います。

値の展開を後から行うという目的には `ERB` という値を後から組み込んで展開するためのライブラリが標準で準備されています。

  * <http://docs.ruby-lang.org/ja/2.1.0/class/ERB.html>

  * <http://magazine.rubyist.net/?0017-BundledLibraries>

これを使いましょう。

[code]

    require 'erb'
    
    class Greeting
      def initialize(name)
        @name = name
      end
    
      def say(template = 'Hello, <%= @name %>')
        ERB.new(template).result(binding)
      end
    end
    
    
    if __FILE__ == $0 then
      g = Greeting.new('似非原')
      puts g.say
      puts g.say('こんにちは <%= @name %>')
    end
    
[/code]

テンプレートエンジンのようにいくつかの文字列に特殊な意味がありますので、それに留意が必要ですが、後から文字列を展開したいという目的に対して自然な表現ができます。


CC BY-SA 3.0
2014-12-16T02:54:54.573
1640
2014-12-16T02:54:54.573
982
1606
answer


<https://ja.stackoverflow.com/a/1610/2599> の回答とほぼ同じです。

最初に文字列を渡して、あとから式展開を行うためには、文字列自体の評価を遅延させるとよさそうです。  
文字列をブロックで囲うとブロックが呼び出されるまで文字列の評価を遅延させることができます。

ブロックを評価する際に[BasicObject#instance_eval](http://docs.ruby-
lang.org/ja/2.1.0/method/BasicObject/i/instance_eval.html)や[BasicObject#instance_exec](http://docs.ruby-
lang.org/ja/2.1.0/method/BasicObject/i/instance_exec.html)を使えば、`Greeting`オブジェクト自身のコンテキストでブロックを実行することができます。

`instance_eval`や`instance_exec`を使うことにより、文字列の式展開中からGreetingオブジェクトのインスタンス変数やインスタンスメソッドに自由にアクセスできるようになります。

[code]

    class Greeting
      attr_reader :name
    
      def initialize(name, &template)
        @name = name
        @template = template
      end
    
      def say
        instance_eval(&@template)
      end
    end
    
    # ブロックがGreetingオブジェクトのコンテキストで評価されるのでインスタンス変数に自由にアクセスできる
    Greeting.new('世界') { "こんにちは, #{@name}" }.say
    # => "こんにちは, 世界"
    
    # Greetingオブジェクトがselfになるのでnameメソッドが呼べる
    Greeting.new('世界') { "こんにちは, #{name}" }.say
    # => "こんにちは, 世界"
    
[/code]


CC BY-SA 3.0
2014-12-16T03:34:01.890
1652
2014-12-16T03:34:01.890
2017-04-13T12:52:38.920
-1
2599
1606
answer


そのままevalで、以下ではどうでしょうか。

[code]

    class Greeting
       def initialize(name, template)
         @name = name
         @template = template
       end
    
       def say
         name = @name
         eval("<<XXX\n" + @template + "\nXXX\n") # XXXは適当に決める
       end
    end
    
    puts Greeting.new('esehara', 'Hello, #{name}').say
    puts Greeting.new('esehara', 'Hello, #{@name}').say
    
[/code]

以前、外部設定ファイルの一行を実行時展開したくなり、一行だけの展開だと ERBを持ってきたり展開用のクラスを入れるのは大げさすぎるので上に落ち着きました。
テンプレートに入る文字列をある程度制限できるなら上で足りるかと思います。


CC BY-SA 3.0
2014-12-16T05:20:19.973
1670
2014-12-16T05:20:19.973
321
1606
answer


Koronさんの回答に補足ですが、`String`クラスの`%`メソッドは`Kernel.sprintf`の第一引数に文字列自身を渡した場合と同じ挙動になります。（Rubyでは`%`演算子は`%`メソッドの呼び出しを意味します）。

[code]

    'Hello, %s' % 'John' #=> "Hello, John"
    sprintf('Hello, %s' , 'John') #=> "Hello, John"
    
[/code]

もし、実行時にテンプレートと文字列を合成したいのであれば、`%`メソッドを使うのが一番シンプルかと思います。

`sprintf`メソッドの挙動については公式ドキュメントを参照ください（C言語のsprintfと同じです）。

<http://docs.ruby-lang.org/ja/2.1.0/doc/print_format.html>


CC BY-SA 3.0
2014-12-18T08:19:47.017
2014
2014-12-18T08:19:47.017
2213
1606
answer