使用include?方法检查数组中是否存在符号。

4

使用Ruby Koans学习Ruby技术。这里有一个测试如下:

def test_method_names_become_symbols
  symbols_as_strings = Symbol.all_symbols.map { |x| x.to_s }
  assert_equal __, symbols_as_strings.include?("test_method_names_become_symbols")
end

# THINK ABOUT IT:
#
# Why do we convert the list of symbols to strings and then compare
# against the string value rather than against symbols?

我尝试在irb控制台中进行相同的操作,结果对于未定义的方法返回了“false”。但是,我在一些test.rb文件中进行了相同的操作,对于已存在和不存在的方法都返回了“true"。示例代码:
def test_method
end
symbols = Symbol.all_symbols.map { |x| x }
puts symbols.include?(:test_method) # returns true in both cases
puts symbols.include?(:test_method_nonexistant) # returns false in irb, true if executed directly

问题是:为什么在这种情况下我们要将符号转换为字符串,以及为什么在irb和普通文件中会有不同的结果?
谢谢!

可能是重复的问题:Ruby Koans:为什么要将符号列表转换为字符串 - Andrew Grimm
2个回答

9

让我们来看一下稍微修改后的测试代码,它在irb中和独立脚本中的表现:

def test_method;end
symbols = Symbol.all_symbols # This is already a "fixed" array, no need for map
puts symbols.include?(:test_method)
puts symbols.include?('test_method_nonexistent'.to_sym)
puts symbols.include?(:test_method_nonexistent)
eval 'puts symbols.include?(:really_not_there)'

当您在irb中尝试此操作时,每行都会被解析和评估,在下一行之前。当您到达第二行时,symbols将包含:test_method,因为def test_method;end已经被评估过了。但是,:test_method_nonexistent这个符号在第二行之前没有出现过,所以第4行和第5行会显示“false”。当然,第6行会再次给我们一个错误,因为:really_not_thereeval返回之前不存在。所以irb会显示如下内容:
true
false
false
false

如果您将此作为Ruby脚本运行,事情会以稍微不同的顺序发生。首先,Ruby会将脚本解析为Ruby VM可以理解的内部格式,然后返回到第一行并开始执行脚本。当解析脚本时,:test_method符号将在解析第一行后存在,并且在解析了第五行后:test_method_nonexistent将存在。因此,在脚本运行之前,我们已经知道了我们感兴趣的两个符号。当我们到达第6行时,Ruby只看到了一个eval和一个字符串,但它还不知道eval导致符号出现。

现在我们有了两个符号(:test_method:test_method_nonexistent)以及一个简单的字符串,当输入eval时,将创建一个符号(:really_not_there)。然后我们回到开头,VM开始运行代码。当我们运行第2行并缓存我们的符号数组时,由于解析器创建了它们,因此:test_method:test_method_nonexistent都将存在并出现在symbols数组中。因此,第3到5行:

puts symbols.include?(:test_method)
puts symbols.include?('test_method_nonexistent'.to_sym)
puts symbols.include?(:test_method_nonexistent)

将打印“true”。然后我们到达第6行:

eval 'puts symbols.include?(:really_not_there)'

由于:really_not_there是在运行时被eval创建而不是在解析期间创建,因此打印出“false”。结果是Ruby会输出:

true
true
true
false

如果我们在最后添加这个:
symbols = Symbol.all_symbols
puts symbols.include?('really_not_there'.to_sym)

然后,我们会从irb和独立脚本中再次得到"true",因为eval将创建:really_not_there并且我们将获得符号列表的新副本。


1
常规解释器和irb之间不同行为的出色分析。+1 - coreyward

3

当检查符号是否存在时,您必须将符号转换为字符串,否则它将始终返回true。传递给include?方法的参数首先得到评估,因此如果您传递一个符号,那么会实例化一个新符号并添加到堆中,所以Symbol.all_symbols实际上有一个符号的副本。

Symbol.all_symbols.include? :the_crow_flies_at_midnight #=> true

然而,将数千个符号转换为字符串进行比较(使用符号更快)是一个不好的解决方案。更好的方法是改变这些语句的评估顺序:

symbols = Symbol.all_symbols
symbols.include? :the_crow_flies_at_midnight #=> false 

这个词典中的符号“快照”是在我们测试的符号被插入堆栈之前拍摄的,因此即使我们的参数在调用include?方法时存在于堆栈上,我们也能得到预期的结果。我不知道为什么在你的IRB控制台中无法工作。可能是你打错了。

谢谢,那很有道理。但是为什么我的symbols数组中会出现这个新符号呢?它在某个时候被形成了,为什么会改变呢?而且我确定我没有打错,你可以自己试试。 - Ilya Tsuryev

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