如何在Ruby中进行“后期”字符串插值

29
>> string = '#{var}'
=> "\#{var}"

>> proc = Proc.new { |var| string }
=> #<Proc:0xb717a8c4@(pry):6>

>> proc.call(123)
=> "\#{var}"

这并不是我想要的。在string周围放双引号会导致明显的undefined local variable错误。


那个有什么用例?那听起来像是一个非常奇怪的想法。 - C. K. Young
1
是的...我以我的奇怪需求而闻名。这是为了让这两行更加DRY https://github.com/pjg/dotfiles/blob/5ae2e0c5a80be8b31d729d0739eb667fa99e5212/.pryrc#L46 - Paweł Gościcki
我认为类似的东西在Ruby Facets中存在。 - Andrew Grimm
6个回答

33
在我的情况下,我需要将配置存储在一个包含插值的yml文件中,但只有当需要时才进行插值。对于那个使用Proc的被接受答案,我认为它过于复杂了。 在Ruby 1.8.7中,您可以使用以下%语法:
"This is a %s verb, %s" % ["nice", "woaaaah"]

如果至少使用 ruby 1.9.x(或带有 i18n 的 ruby 1.8.7),则有一个更清晰的替代方案:

my_template = "This is a %{adjective} verb, %{super}!"

my_template % { adjective: "nice", super: "woah" }
=> "This is a nice verb, woah!"

我正在寻找一个像erb一样安全的替代品,用于非常小的使用情况。这真的很有帮助! - Daniel

21
虽然这是可以做到的,但如果没有使用eval的话,它不会按照你的意图正常工作,通常情况下这是一个不好的主意。好消息是你有几个选择。
最直接的方法是使用sprintf格式化,使用String#%方法甚至更容易:
string = '%s'

proc = Proc.new { |var| string % var }

proc.call(123)
# => "123"

这是一种非常可靠的方法,因为任何支持 .to_s 方法的内容都可以使用,且如果它包含可执行代码也不会导致宇宙坍塌。


11
String#% 也支持使用 Hash 作为参数的命名参数,这使得它非常接近本地字符串插值的功能。 - Mladen Jablanović
通常我使用C风格,因为这样更紧凑,但正如你所指出的,它可以做更多的事情。 - tadman

5

它可以通过eval进行工作:

proc = Proc.new { |var| eval(%Q{"#{string}"}) }

(如果您信任 字符串 的值。)

1
当你引用引号时,最好使用单引号或%q[]来表达,例如:%q["#{string}"],以避免所有可怕的转义。 - tadman
嗯...看起来 eval 可以用。但是也许还有其他方法? - Paweł Gościcki
如果你需要的话,我不知道为什么你不直接使用eval(string)。这很可怕、很危险。我曾经在ActiveRecord的自定义查找SQL中看到过这种用法,它总是让我感到非常糟糕。大致上来说,你可以使用gsub函数将模式替换为%s,然后进行插值操作,而不必采取核心选项。 - tadman
对于gsub,仅支持变量插值,而不是任何表达式插值。 - Arnaud Le Blanc
4
%Q 会进行插值,所以如果你愿意的话,可以使用 %Q{#{string}} - mu is too short
显示剩余2条评论

4
您可以通过创建一个“柯里化”函数(即返回Proc的Proc)来实现您所寻求的DRY,其中内部函数包含具有变量的基本字符串,每个变量表示不同的部分。
如果我理解不正确,请纠正我,但在您注释链接后面的代码中,两个字符串之间的唯一区别是结尾处的一个字符。(即使不是这样,您仍然可以使用此技术来实现相同的目标。)您可以创建一个返回包含字符串的Proc的Proc,然后为您的两个尾随字符调用两次外部Proc:
rails_root = "whatever" # Not variant for the string
rails_env_prompt = "whatever" #not variant for the string

spec = Proc.new { |tail_char| 
  Proc.new {|obj, nest_level, *| 
    "#{rails_root} #{rails_env_prompt} #{obj}:#{nest_level}#{tail_char} "
  }
}

Pry.config.prompt = [ spec.call(">"), spec.call("*") ]  

Pry.config.prompt[0].call("My obj", "My Nest Level")
# result: "whatever whatever My obj:My Nest Level> "

这太复杂了 :) 接受答案中的字符串插值对我很有效。 - Paweł Gościcki
1
在我看来,为了那些因标题而来到这个问题的人们的利益,这个答案应该被接受。当前被接受的答案(由nathanvda提供)和当前最受欢迎的答案(由Arnaud Le Blanc提供)都没有使用字符串插值(一种编译时机制)。相反,它们建议使用字符串格式化(一种运行时机制)。对于那些关心它们之间差异的人来说,这里的柯里化技术是一个很好的解决方案。(如果一个人不关心,那也没关系,但这不是这个问题标题所问的。) - Mickalot

1

你必须在Proc之外定义插值字符串吗?

proc = Proc.new { |var| "#{var}" }
proc.call(123) # "123"

这应该是我认为最干净的方式。

我看到这对你的特定示例不起作用...但它仍然可能适用于其他类似情况。我会单独发布一个更好的答案。 - Craig Walker

1
为了澄清,那些寻找此问题标题答案(如何进行延迟字符串插值)的人应该明白没有所谓的延迟字符串插值
这个问题的发布者实际上并不关心字符串插值本身,只是关心格式。对于那些也不关心Ruby中字符串插值和字符串格式之间区别的人来说,这里的许多答案都是可以接受的。
如果您不知道自己是否关心,请查看stackoverflow问题的许多响应,解释了微妙的区别--这里有一个更多的解释:
一般来说,字符串插值是编译时的语法糖:格式字符串在生成字节码时“编译”一次。使用字符串格式化,格式字符串在每次调用时重新解析(请参见Kernel::sprintf)。
为了看到这一点,请考虑以下程序:
x=1
s="#{x}"

RubyVM将其编译为字节码,其中包含对x的硬引用(请参见指令0005):
$ ruby --dump=insns <<EXAMPLE
x=1
s="#{x}"
EXAMPLE
== disasm: #<ISeq:<main>@-:1 (1,0)-(2,8)> (catch: FALSE)
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] x@0        [ 1] s@1
0000 putobject_INT2FIX_1_                                             (   1)[Li]
0001 setlocal_WC_0                x@0
0003 putobject                    ""                                  (   2)[Li]
0005 getlocal_WC_0                x@0
0007 dup
0008 checktype                    T_STRING
0010 branchif                     17
0012 dup
0013 opt_send_without_block       <callinfo!mid:to_s, argc:0, FCALL|ARGS_SIMPLE>, <callcache>
0016 tostring
0017 concatstrings                2
0019 dup
0020 setlocal_WC_0                s@1
0022 leave

请注意,插值字符串未显示,因为它已编译为对 x.to_s (指令 0013) 和 concatstrings VM 指令 (指令 0017) 的一次调用。
另一方面,像以下解决方案:
x=1
s="%{foo}" % {foo: x}

每次调用都会将一个格式字符串推入堆栈(指令0003),并调用String#%(指令0007)以重新解析它,从而触发与Kernel::sprintf文档中描述的相同的格式解析代码。

$ ruby --dump=insns <<EXAMPLE
> x=1
> s="%{foo}" % {foo: x}
> EXAMPLE
== disasm: #<ISeq:<main>@-:1 (1,0)-(2,21)> (catch: FALSE)
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] x@0        [ 1] s@1
0000 putobject_INT2FIX_1_                                             (   1)[Li]
0001 setlocal_WC_0                x@0
0003 putstring                    "%{foo}"                            (   2)[Li]
0005 getlocal_WC_0                x@0
0007 opt_send_without_block       <callinfo!mid:%, argc:1, kw:[foo], KWARG>, <callcache>
0010 dup
0011 setlocal_WC_0                s@1
0013 leave

大多数情况下,不需要关注这种区别,但如果你来到这个问题是因为你关心这个问题,那么就可以了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接